backtest-kit 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +841 -6
- package/build/index.cjs +3418 -200
- package/build/index.mjs +3408 -201
- package/package.json +3 -2
- package/types.d.ts +2266 -119
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, queued } from 'functools-kit';
|
|
3
|
+
import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, iterateDocuments, distinctDocuments, queued } from 'functools-kit';
|
|
4
4
|
import fs, { mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import path, { join } from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
@@ -116,6 +116,8 @@ const connectionServices$1 = {
|
|
|
116
116
|
frameConnectionService: Symbol('frameConnectionService'),
|
|
117
117
|
sizingConnectionService: Symbol('sizingConnectionService'),
|
|
118
118
|
riskConnectionService: Symbol('riskConnectionService'),
|
|
119
|
+
optimizerConnectionService: Symbol('optimizerConnectionService'),
|
|
120
|
+
partialConnectionService: Symbol('partialConnectionService'),
|
|
119
121
|
};
|
|
120
122
|
const schemaServices$1 = {
|
|
121
123
|
exchangeSchemaService: Symbol('exchangeSchemaService'),
|
|
@@ -124,6 +126,7 @@ const schemaServices$1 = {
|
|
|
124
126
|
walkerSchemaService: Symbol('walkerSchemaService'),
|
|
125
127
|
sizingSchemaService: Symbol('sizingSchemaService'),
|
|
126
128
|
riskSchemaService: Symbol('riskSchemaService'),
|
|
129
|
+
optimizerSchemaService: Symbol('optimizerSchemaService'),
|
|
127
130
|
};
|
|
128
131
|
const globalServices$1 = {
|
|
129
132
|
exchangeGlobalService: Symbol('exchangeGlobalService'),
|
|
@@ -131,6 +134,8 @@ const globalServices$1 = {
|
|
|
131
134
|
frameGlobalService: Symbol('frameGlobalService'),
|
|
132
135
|
sizingGlobalService: Symbol('sizingGlobalService'),
|
|
133
136
|
riskGlobalService: Symbol('riskGlobalService'),
|
|
137
|
+
optimizerGlobalService: Symbol('optimizerGlobalService'),
|
|
138
|
+
partialGlobalService: Symbol('partialGlobalService'),
|
|
134
139
|
};
|
|
135
140
|
const commandServices$1 = {
|
|
136
141
|
liveCommandService: Symbol('liveCommandService'),
|
|
@@ -154,6 +159,7 @@ const markdownServices$1 = {
|
|
|
154
159
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
155
160
|
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
156
161
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
162
|
+
partialMarkdownService: Symbol('partialMarkdownService'),
|
|
157
163
|
};
|
|
158
164
|
const validationServices$1 = {
|
|
159
165
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -162,6 +168,10 @@ const validationServices$1 = {
|
|
|
162
168
|
walkerValidationService: Symbol('walkerValidationService'),
|
|
163
169
|
sizingValidationService: Symbol('sizingValidationService'),
|
|
164
170
|
riskValidationService: Symbol('riskValidationService'),
|
|
171
|
+
optimizerValidationService: Symbol('optimizerValidationService'),
|
|
172
|
+
};
|
|
173
|
+
const templateServices$1 = {
|
|
174
|
+
optimizerTemplateService: Symbol('optimizerTemplateService'),
|
|
165
175
|
};
|
|
166
176
|
const TYPES = {
|
|
167
177
|
...baseServices$1,
|
|
@@ -174,6 +184,7 @@ const TYPES = {
|
|
|
174
184
|
...logicPublicServices$1,
|
|
175
185
|
...markdownServices$1,
|
|
176
186
|
...validationServices$1,
|
|
187
|
+
...templateServices$1,
|
|
177
188
|
};
|
|
178
189
|
|
|
179
190
|
/**
|
|
@@ -875,6 +886,12 @@ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
|
875
886
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
876
887
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
877
888
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
889
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
|
|
890
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
|
|
891
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
|
|
892
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
|
|
893
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
|
|
894
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
|
|
878
895
|
const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
|
|
879
896
|
const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
|
|
880
897
|
const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
|
|
@@ -940,7 +957,7 @@ const PersistBase = makeExtendable(class {
|
|
|
940
957
|
* Creates new persistence instance.
|
|
941
958
|
*
|
|
942
959
|
* @param entityName - Unique entity type identifier
|
|
943
|
-
* @param baseDir - Base directory for all entities (default: ./
|
|
960
|
+
* @param baseDir - Base directory for all entities (default: ./dump/data)
|
|
944
961
|
*/
|
|
945
962
|
constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
|
|
946
963
|
this.entityName = entityName;
|
|
@@ -1199,7 +1216,7 @@ class PersistSignalUtils {
|
|
|
1199
1216
|
this.PersistSignalFactory = PersistBase;
|
|
1200
1217
|
this.getSignalStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
|
|
1201
1218
|
strategyName,
|
|
1202
|
-
`./
|
|
1219
|
+
`./dump/data/signal/`,
|
|
1203
1220
|
]));
|
|
1204
1221
|
/**
|
|
1205
1222
|
* Reads persisted signal data for a strategy and symbol.
|
|
@@ -1295,7 +1312,7 @@ class PersistRiskUtils {
|
|
|
1295
1312
|
this.PersistRiskFactory = PersistBase;
|
|
1296
1313
|
this.getRiskStorage = memoize(([riskName]) => `${riskName}`, (riskName) => Reflect.construct(this.PersistRiskFactory, [
|
|
1297
1314
|
riskName,
|
|
1298
|
-
`./
|
|
1315
|
+
`./dump/data/risk/`,
|
|
1299
1316
|
]));
|
|
1300
1317
|
/**
|
|
1301
1318
|
* Reads persisted active positions for a risk profile.
|
|
@@ -1372,6 +1389,192 @@ class PersistRiskUtils {
|
|
|
1372
1389
|
* ```
|
|
1373
1390
|
*/
|
|
1374
1391
|
const PersistRiskAdapter = new PersistRiskUtils();
|
|
1392
|
+
/**
|
|
1393
|
+
* Utility class for managing scheduled signal persistence.
|
|
1394
|
+
*
|
|
1395
|
+
* Features:
|
|
1396
|
+
* - Memoized storage instances per strategy
|
|
1397
|
+
* - Custom adapter support
|
|
1398
|
+
* - Atomic read/write operations for scheduled signals
|
|
1399
|
+
* - Crash-safe scheduled signal state management
|
|
1400
|
+
*
|
|
1401
|
+
* Used by ClientStrategy for live mode persistence of scheduled signals (_scheduledSignal).
|
|
1402
|
+
*/
|
|
1403
|
+
class PersistScheduleUtils {
|
|
1404
|
+
constructor() {
|
|
1405
|
+
this.PersistScheduleFactory = PersistBase;
|
|
1406
|
+
this.getScheduleStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistScheduleFactory, [
|
|
1407
|
+
strategyName,
|
|
1408
|
+
`./dump/data/schedule/`,
|
|
1409
|
+
]));
|
|
1410
|
+
/**
|
|
1411
|
+
* Reads persisted scheduled signal data for a strategy and symbol.
|
|
1412
|
+
*
|
|
1413
|
+
* Called by ClientStrategy.waitForInit() to restore scheduled signal state.
|
|
1414
|
+
* Returns null if no scheduled signal exists.
|
|
1415
|
+
*
|
|
1416
|
+
* @param strategyName - Strategy identifier
|
|
1417
|
+
* @param symbol - Trading pair symbol
|
|
1418
|
+
* @returns Promise resolving to scheduled signal or null
|
|
1419
|
+
*/
|
|
1420
|
+
this.readScheduleData = async (strategyName, symbol) => {
|
|
1421
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
|
|
1422
|
+
const isInitial = !this.getScheduleStorage.has(strategyName);
|
|
1423
|
+
const stateStorage = this.getScheduleStorage(strategyName);
|
|
1424
|
+
await stateStorage.waitForInit(isInitial);
|
|
1425
|
+
if (await stateStorage.hasValue(symbol)) {
|
|
1426
|
+
return await stateStorage.readValue(symbol);
|
|
1427
|
+
}
|
|
1428
|
+
return null;
|
|
1429
|
+
};
|
|
1430
|
+
/**
|
|
1431
|
+
* Writes scheduled signal data to disk with atomic file writes.
|
|
1432
|
+
*
|
|
1433
|
+
* Called by ClientStrategy.setScheduledSignal() to persist state.
|
|
1434
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1435
|
+
*
|
|
1436
|
+
* @param scheduledSignalRow - Scheduled signal data (null to clear)
|
|
1437
|
+
* @param strategyName - Strategy identifier
|
|
1438
|
+
* @param symbol - Trading pair symbol
|
|
1439
|
+
* @returns Promise that resolves when write is complete
|
|
1440
|
+
*/
|
|
1441
|
+
this.writeScheduleData = async (scheduledSignalRow, strategyName, symbol) => {
|
|
1442
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1443
|
+
const isInitial = !this.getScheduleStorage.has(strategyName);
|
|
1444
|
+
const stateStorage = this.getScheduleStorage(strategyName);
|
|
1445
|
+
await stateStorage.waitForInit(isInitial);
|
|
1446
|
+
await stateStorage.writeValue(symbol, scheduledSignalRow);
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Registers a custom persistence adapter.
|
|
1451
|
+
*
|
|
1452
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* ```typescript
|
|
1456
|
+
* class RedisPersist extends PersistBase {
|
|
1457
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1458
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1459
|
+
* }
|
|
1460
|
+
* PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
|
|
1461
|
+
* ```
|
|
1462
|
+
*/
|
|
1463
|
+
usePersistScheduleAdapter(Ctor) {
|
|
1464
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
|
|
1465
|
+
this.PersistScheduleFactory = Ctor;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Global singleton instance of PersistScheduleUtils.
|
|
1470
|
+
* Used by ClientStrategy for scheduled signal persistence.
|
|
1471
|
+
*
|
|
1472
|
+
* @example
|
|
1473
|
+
* ```typescript
|
|
1474
|
+
* // Custom adapter
|
|
1475
|
+
* PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
|
|
1476
|
+
*
|
|
1477
|
+
* // Read scheduled signal
|
|
1478
|
+
* const scheduled = await PersistScheduleAdapter.readScheduleData("my-strategy", "BTCUSDT");
|
|
1479
|
+
*
|
|
1480
|
+
* // Write scheduled signal
|
|
1481
|
+
* await PersistScheduleAdapter.writeScheduleData(scheduled, "my-strategy", "BTCUSDT");
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
const PersistScheduleAdapter = new PersistScheduleUtils();
|
|
1485
|
+
/**
|
|
1486
|
+
* Utility class for managing partial profit/loss levels persistence.
|
|
1487
|
+
*
|
|
1488
|
+
* Features:
|
|
1489
|
+
* - Memoized storage instances per symbol
|
|
1490
|
+
* - Custom adapter support
|
|
1491
|
+
* - Atomic read/write operations for partial data
|
|
1492
|
+
* - Crash-safe partial state management
|
|
1493
|
+
*
|
|
1494
|
+
* Used by ClientPartial for live mode persistence of profit/loss levels.
|
|
1495
|
+
*/
|
|
1496
|
+
class PersistPartialUtils {
|
|
1497
|
+
constructor() {
|
|
1498
|
+
this.PersistPartialFactory = PersistBase;
|
|
1499
|
+
this.getPartialStorage = memoize(([symbol]) => `${symbol}`, (symbol) => Reflect.construct(this.PersistPartialFactory, [
|
|
1500
|
+
symbol,
|
|
1501
|
+
`./dump/data/partial/`,
|
|
1502
|
+
]));
|
|
1503
|
+
/**
|
|
1504
|
+
* Reads persisted partial data for a symbol.
|
|
1505
|
+
*
|
|
1506
|
+
* Called by ClientPartial.waitForInit() to restore state.
|
|
1507
|
+
* Returns empty object if no partial data exists.
|
|
1508
|
+
*
|
|
1509
|
+
* @param symbol - Trading pair symbol
|
|
1510
|
+
* @returns Promise resolving to partial data record
|
|
1511
|
+
*/
|
|
1512
|
+
this.readPartialData = async (symbol) => {
|
|
1513
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
|
|
1514
|
+
const isInitial = !this.getPartialStorage.has(symbol);
|
|
1515
|
+
const stateStorage = this.getPartialStorage(symbol);
|
|
1516
|
+
await stateStorage.waitForInit(isInitial);
|
|
1517
|
+
const PARTIAL_STORAGE_KEY = "levels";
|
|
1518
|
+
if (await stateStorage.hasValue(PARTIAL_STORAGE_KEY)) {
|
|
1519
|
+
return await stateStorage.readValue(PARTIAL_STORAGE_KEY);
|
|
1520
|
+
}
|
|
1521
|
+
return {};
|
|
1522
|
+
};
|
|
1523
|
+
/**
|
|
1524
|
+
* Writes partial data to disk with atomic file writes.
|
|
1525
|
+
*
|
|
1526
|
+
* Called by ClientPartial after profit/loss level changes to persist state.
|
|
1527
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1528
|
+
*
|
|
1529
|
+
* @param partialData - Record of signal IDs to partial data
|
|
1530
|
+
* @param symbol - Trading pair symbol
|
|
1531
|
+
* @returns Promise that resolves when write is complete
|
|
1532
|
+
*/
|
|
1533
|
+
this.writePartialData = async (partialData, symbol) => {
|
|
1534
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1535
|
+
const isInitial = !this.getPartialStorage.has(symbol);
|
|
1536
|
+
const stateStorage = this.getPartialStorage(symbol);
|
|
1537
|
+
await stateStorage.waitForInit(isInitial);
|
|
1538
|
+
const PARTIAL_STORAGE_KEY = "levels";
|
|
1539
|
+
await stateStorage.writeValue(PARTIAL_STORAGE_KEY, partialData);
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Registers a custom persistence adapter.
|
|
1544
|
+
*
|
|
1545
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* ```typescript
|
|
1549
|
+
* class RedisPersist extends PersistBase {
|
|
1550
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1551
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1552
|
+
* }
|
|
1553
|
+
* PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
|
|
1554
|
+
* ```
|
|
1555
|
+
*/
|
|
1556
|
+
usePersistPartialAdapter(Ctor) {
|
|
1557
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
|
|
1558
|
+
this.PersistPartialFactory = Ctor;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Global singleton instance of PersistPartialUtils.
|
|
1563
|
+
* Used by ClientPartial for partial profit/loss levels persistence.
|
|
1564
|
+
*
|
|
1565
|
+
* @example
|
|
1566
|
+
* ```typescript
|
|
1567
|
+
* // Custom adapter
|
|
1568
|
+
* PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
|
|
1569
|
+
*
|
|
1570
|
+
* // Read partial data
|
|
1571
|
+
* const partialData = await PersistPartialAdapter.readPartialData("BTCUSDT");
|
|
1572
|
+
*
|
|
1573
|
+
* // Write partial data
|
|
1574
|
+
* await PersistPartialAdapter.writePartialData(partialData, "BTCUSDT");
|
|
1575
|
+
* ```
|
|
1576
|
+
*/
|
|
1577
|
+
const PersistPartialAdapter = new PersistPartialUtils();
|
|
1375
1578
|
|
|
1376
1579
|
/**
|
|
1377
1580
|
* Global signal emitter for all trading events (live + backtest).
|
|
@@ -1412,7 +1615,12 @@ const doneWalkerSubject = new Subject();
|
|
|
1412
1615
|
* Progress emitter for backtest execution progress.
|
|
1413
1616
|
* Emits progress updates during backtest execution.
|
|
1414
1617
|
*/
|
|
1415
|
-
const
|
|
1618
|
+
const progressBacktestEmitter = new Subject();
|
|
1619
|
+
/**
|
|
1620
|
+
* Progress emitter for walker execution progress.
|
|
1621
|
+
* Emits progress updates during walker execution.
|
|
1622
|
+
*/
|
|
1623
|
+
const progressWalkerEmitter = new Subject();
|
|
1416
1624
|
/**
|
|
1417
1625
|
* Performance emitter for execution metrics.
|
|
1418
1626
|
* Emits performance metrics for profiling and bottleneck detection.
|
|
@@ -1428,11 +1636,26 @@ const walkerEmitter = new Subject();
|
|
|
1428
1636
|
* Emits when all strategies have been tested and final results are available.
|
|
1429
1637
|
*/
|
|
1430
1638
|
const walkerCompleteSubject = new Subject();
|
|
1639
|
+
/**
|
|
1640
|
+
* Walker stop emitter for walker cancellation events.
|
|
1641
|
+
* Emits when a walker comparison is stopped/cancelled.
|
|
1642
|
+
*/
|
|
1643
|
+
const walkerStopSubject = new Subject();
|
|
1431
1644
|
/**
|
|
1432
1645
|
* Validation emitter for risk validation errors.
|
|
1433
1646
|
* Emits when risk validation functions throw errors during signal checking.
|
|
1434
1647
|
*/
|
|
1435
1648
|
const validationSubject = new Subject();
|
|
1649
|
+
/**
|
|
1650
|
+
* Partial profit emitter for profit level milestones.
|
|
1651
|
+
* Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
|
|
1652
|
+
*/
|
|
1653
|
+
const partialProfitSubject = new Subject();
|
|
1654
|
+
/**
|
|
1655
|
+
* Partial loss emitter for loss level milestones.
|
|
1656
|
+
* Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
|
|
1657
|
+
*/
|
|
1658
|
+
const partialLossSubject = new Subject();
|
|
1436
1659
|
|
|
1437
1660
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
1438
1661
|
__proto__: null,
|
|
@@ -1440,14 +1663,18 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
1440
1663
|
doneLiveSubject: doneLiveSubject,
|
|
1441
1664
|
doneWalkerSubject: doneWalkerSubject,
|
|
1442
1665
|
errorEmitter: errorEmitter,
|
|
1666
|
+
partialLossSubject: partialLossSubject,
|
|
1667
|
+
partialProfitSubject: partialProfitSubject,
|
|
1443
1668
|
performanceEmitter: performanceEmitter,
|
|
1444
|
-
|
|
1669
|
+
progressBacktestEmitter: progressBacktestEmitter,
|
|
1670
|
+
progressWalkerEmitter: progressWalkerEmitter,
|
|
1445
1671
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
1446
1672
|
signalEmitter: signalEmitter,
|
|
1447
1673
|
signalLiveEmitter: signalLiveEmitter,
|
|
1448
1674
|
validationSubject: validationSubject,
|
|
1449
1675
|
walkerCompleteSubject: walkerCompleteSubject,
|
|
1450
|
-
walkerEmitter: walkerEmitter
|
|
1676
|
+
walkerEmitter: walkerEmitter,
|
|
1677
|
+
walkerStopSubject: walkerStopSubject
|
|
1451
1678
|
});
|
|
1452
1679
|
|
|
1453
1680
|
const INTERVAL_MINUTES$1 = {
|
|
@@ -1460,6 +1687,25 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1460
1687
|
};
|
|
1461
1688
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
1462
1689
|
const errors = [];
|
|
1690
|
+
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
1691
|
+
if (signal.id === undefined || signal.id === null || signal.id === '') {
|
|
1692
|
+
errors.push('id is required and must be a non-empty string');
|
|
1693
|
+
}
|
|
1694
|
+
if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
|
|
1695
|
+
errors.push('exchangeName is required');
|
|
1696
|
+
}
|
|
1697
|
+
if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
|
|
1698
|
+
errors.push('strategyName is required');
|
|
1699
|
+
}
|
|
1700
|
+
if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
|
|
1701
|
+
errors.push('symbol is required and must be a non-empty string');
|
|
1702
|
+
}
|
|
1703
|
+
if (signal._isScheduled === undefined || signal._isScheduled === null) {
|
|
1704
|
+
errors.push('_isScheduled is required');
|
|
1705
|
+
}
|
|
1706
|
+
if (signal.position === undefined || signal.position === null) {
|
|
1707
|
+
errors.push('position is required and must be "long" or "short"');
|
|
1708
|
+
}
|
|
1463
1709
|
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
1464
1710
|
if (!isFinite(currentPrice)) {
|
|
1465
1711
|
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
@@ -1709,26 +1955,42 @@ const GET_AVG_PRICE_FN = (candles) => {
|
|
|
1709
1955
|
? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
|
|
1710
1956
|
: sumPriceVolume / totalVolume;
|
|
1711
1957
|
};
|
|
1712
|
-
const WAIT_FOR_INIT_FN$
|
|
1958
|
+
const WAIT_FOR_INIT_FN$2 = async (self) => {
|
|
1713
1959
|
self.params.logger.debug("ClientStrategy waitForInit");
|
|
1714
1960
|
if (self.params.execution.context.backtest) {
|
|
1715
1961
|
return;
|
|
1716
1962
|
}
|
|
1963
|
+
// Restore pending signal
|
|
1717
1964
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1718
|
-
if (
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1965
|
+
if (pendingSignal) {
|
|
1966
|
+
if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
self._pendingSignal = pendingSignal;
|
|
1973
|
+
// Call onActive callback for restored signal
|
|
1974
|
+
if (self.params.callbacks?.onActive) {
|
|
1975
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1976
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
|
|
1977
|
+
}
|
|
1726
1978
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
if (
|
|
1730
|
-
|
|
1731
|
-
|
|
1979
|
+
// Restore scheduled signal
|
|
1980
|
+
const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1981
|
+
if (scheduledSignal) {
|
|
1982
|
+
if (scheduledSignal.exchangeName !== self.params.method.context.exchangeName) {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (scheduledSignal.strategyName !== self.params.method.context.strategyName) {
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
self._scheduledSignal = scheduledSignal;
|
|
1989
|
+
// Call onSchedule callback for restored scheduled signal
|
|
1990
|
+
if (self.params.callbacks?.onSchedule) {
|
|
1991
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1992
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
|
|
1993
|
+
}
|
|
1732
1994
|
}
|
|
1733
1995
|
};
|
|
1734
1996
|
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
@@ -1745,7 +2007,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
|
|
|
1745
2007
|
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
1746
2008
|
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
1747
2009
|
});
|
|
1748
|
-
self.
|
|
2010
|
+
await self.setScheduledSignal(null);
|
|
1749
2011
|
if (self.params.callbacks?.onCancel) {
|
|
1750
2012
|
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
1751
2013
|
}
|
|
@@ -1800,7 +2062,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
|
|
|
1800
2062
|
averagePrice: currentPrice,
|
|
1801
2063
|
priceStopLoss: scheduled.priceStopLoss,
|
|
1802
2064
|
});
|
|
1803
|
-
self.
|
|
2065
|
+
await self.setScheduledSignal(null);
|
|
1804
2066
|
const result = {
|
|
1805
2067
|
action: "idle",
|
|
1806
2068
|
signal: null,
|
|
@@ -1821,7 +2083,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
1821
2083
|
symbol: self.params.execution.context.symbol,
|
|
1822
2084
|
signalId: scheduled.id,
|
|
1823
2085
|
});
|
|
1824
|
-
self.
|
|
2086
|
+
await self.setScheduledSignal(null);
|
|
1825
2087
|
return null;
|
|
1826
2088
|
}
|
|
1827
2089
|
// В LIVE режиме activationTimestamp - это текущее время при tick()
|
|
@@ -1849,10 +2111,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
1849
2111
|
symbol: self.params.execution.context.symbol,
|
|
1850
2112
|
signalId: scheduled.id,
|
|
1851
2113
|
});
|
|
1852
|
-
self.
|
|
2114
|
+
await self.setScheduledSignal(null);
|
|
1853
2115
|
return null;
|
|
1854
2116
|
}
|
|
1855
|
-
self.
|
|
2117
|
+
await self.setScheduledSignal(null);
|
|
1856
2118
|
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
1857
2119
|
const activatedSignal = {
|
|
1858
2120
|
...scheduled,
|
|
@@ -1990,6 +2252,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
1990
2252
|
if (self.params.callbacks?.onClose) {
|
|
1991
2253
|
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1992
2254
|
}
|
|
2255
|
+
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2256
|
+
await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
|
|
1993
2257
|
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1994
2258
|
strategyName: self.params.method.context.strategyName,
|
|
1995
2259
|
riskName: self.params.riskName,
|
|
@@ -2012,6 +2276,34 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2012
2276
|
return result;
|
|
2013
2277
|
};
|
|
2014
2278
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2279
|
+
// Calculate revenue percentage for partial fill/loss callbacks
|
|
2280
|
+
{
|
|
2281
|
+
let revenuePercent = 0;
|
|
2282
|
+
if (signal.position === "long") {
|
|
2283
|
+
// For long: positive if current > open, negative if current < open
|
|
2284
|
+
revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
|
|
2285
|
+
}
|
|
2286
|
+
else if (signal.position === "short") {
|
|
2287
|
+
// For short: positive if current < open, negative if current > open
|
|
2288
|
+
revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
|
|
2289
|
+
}
|
|
2290
|
+
// Call onPartialProfit if revenue is positive (but not reached TP yet)
|
|
2291
|
+
if (revenuePercent > 0) {
|
|
2292
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2293
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2294
|
+
if (self.params.callbacks?.onPartialProfit) {
|
|
2295
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
// Call onPartialLoss if revenue is negative (but not hit SL yet)
|
|
2299
|
+
if (revenuePercent < 0) {
|
|
2300
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2301
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2302
|
+
if (self.params.callbacks?.onPartialLoss) {
|
|
2303
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2015
2307
|
const result = {
|
|
2016
2308
|
action: "active",
|
|
2017
2309
|
signal: signal,
|
|
@@ -2050,7 +2342,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
2050
2342
|
averagePrice,
|
|
2051
2343
|
priceStopLoss: scheduled.priceStopLoss,
|
|
2052
2344
|
});
|
|
2053
|
-
self.
|
|
2345
|
+
await self.setScheduledSignal(null);
|
|
2054
2346
|
if (self.params.callbacks?.onCancel) {
|
|
2055
2347
|
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
2056
2348
|
}
|
|
@@ -2075,7 +2367,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2075
2367
|
symbol: self.params.execution.context.symbol,
|
|
2076
2368
|
signalId: scheduled.id,
|
|
2077
2369
|
});
|
|
2078
|
-
self.
|
|
2370
|
+
await self.setScheduledSignal(null);
|
|
2079
2371
|
return false;
|
|
2080
2372
|
}
|
|
2081
2373
|
// В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
|
|
@@ -2100,10 +2392,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2100
2392
|
symbol: self.params.execution.context.symbol,
|
|
2101
2393
|
signalId: scheduled.id,
|
|
2102
2394
|
});
|
|
2103
|
-
self.
|
|
2395
|
+
await self.setScheduledSignal(null);
|
|
2104
2396
|
return false;
|
|
2105
2397
|
}
|
|
2106
|
-
self.
|
|
2398
|
+
await self.setScheduledSignal(null);
|
|
2107
2399
|
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
|
|
2108
2400
|
const activatedSignal = {
|
|
2109
2401
|
...scheduled,
|
|
@@ -2139,6 +2431,8 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2139
2431
|
if (self.params.callbacks?.onClose) {
|
|
2140
2432
|
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
2141
2433
|
}
|
|
2434
|
+
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2435
|
+
await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
|
|
2142
2436
|
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
2143
2437
|
strategyName: self.params.method.context.strategyName,
|
|
2144
2438
|
riskName: self.params.riskName,
|
|
@@ -2280,6 +2574,33 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2280
2574
|
}
|
|
2281
2575
|
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
2282
2576
|
}
|
|
2577
|
+
// Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
|
|
2578
|
+
// Calculate revenue percentage
|
|
2579
|
+
{
|
|
2580
|
+
let revenuePercent = 0;
|
|
2581
|
+
if (signal.position === "long") {
|
|
2582
|
+
revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
|
|
2583
|
+
}
|
|
2584
|
+
else if (signal.position === "short") {
|
|
2585
|
+
revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
|
|
2586
|
+
}
|
|
2587
|
+
// Call onPartialProfit if revenue is positive (but not reached TP yet)
|
|
2588
|
+
if (revenuePercent > 0) {
|
|
2589
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2590
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2591
|
+
if (self.params.callbacks?.onPartialProfit) {
|
|
2592
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
// Call onPartialLoss if revenue is negative (but not hit SL yet)
|
|
2596
|
+
if (revenuePercent < 0) {
|
|
2597
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2598
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2599
|
+
if (self.params.callbacks?.onPartialLoss) {
|
|
2600
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2283
2604
|
}
|
|
2284
2605
|
return null;
|
|
2285
2606
|
};
|
|
@@ -2326,7 +2647,7 @@ class ClientStrategy {
|
|
|
2326
2647
|
*
|
|
2327
2648
|
* @returns Promise that resolves when initialization is complete
|
|
2328
2649
|
*/
|
|
2329
|
-
this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$
|
|
2650
|
+
this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$2(this));
|
|
2330
2651
|
}
|
|
2331
2652
|
/**
|
|
2332
2653
|
* Updates pending signal and persists to disk in live mode.
|
|
@@ -2352,6 +2673,25 @@ class ClientStrategy {
|
|
|
2352
2673
|
}
|
|
2353
2674
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2354
2675
|
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Updates scheduled signal and persists to disk in live mode.
|
|
2678
|
+
*
|
|
2679
|
+
* Centralized method for all scheduled signal state changes.
|
|
2680
|
+
* Uses atomic file writes to prevent corruption.
|
|
2681
|
+
*
|
|
2682
|
+
* @param scheduledSignal - New scheduled signal state (null to clear)
|
|
2683
|
+
* @returns Promise that resolves when update is complete
|
|
2684
|
+
*/
|
|
2685
|
+
async setScheduledSignal(scheduledSignal) {
|
|
2686
|
+
this.params.logger.debug("ClientStrategy setScheduledSignal", {
|
|
2687
|
+
scheduledSignal,
|
|
2688
|
+
});
|
|
2689
|
+
this._scheduledSignal = scheduledSignal;
|
|
2690
|
+
if (this.params.execution.context.backtest) {
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2694
|
+
}
|
|
2355
2695
|
/**
|
|
2356
2696
|
* Retrieves the current pending signal.
|
|
2357
2697
|
* If no signal is pending, returns null.
|
|
@@ -2426,7 +2766,7 @@ class ClientStrategy {
|
|
|
2426
2766
|
const signal = await GET_SIGNAL_FN(this);
|
|
2427
2767
|
if (signal) {
|
|
2428
2768
|
if (signal._isScheduled === true) {
|
|
2429
|
-
this.
|
|
2769
|
+
await this.setScheduledSignal(signal);
|
|
2430
2770
|
return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
|
|
2431
2771
|
}
|
|
2432
2772
|
await this.setPendingSignal(signal);
|
|
@@ -2602,7 +2942,7 @@ class ClientStrategy {
|
|
|
2602
2942
|
this._isStopped = true;
|
|
2603
2943
|
// Clear scheduled signal if exists
|
|
2604
2944
|
if (this._scheduledSignal) {
|
|
2605
|
-
this.
|
|
2945
|
+
await this.setScheduledSignal(null);
|
|
2606
2946
|
}
|
|
2607
2947
|
}
|
|
2608
2948
|
}
|
|
@@ -2641,6 +2981,7 @@ class StrategyConnectionService {
|
|
|
2641
2981
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
2642
2982
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
2643
2983
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
2984
|
+
this.partialConnectionService = inject(TYPES.partialConnectionService);
|
|
2644
2985
|
/**
|
|
2645
2986
|
* Retrieves memoized ClientStrategy instance for given strategy name.
|
|
2646
2987
|
*
|
|
@@ -2657,6 +2998,7 @@ class StrategyConnectionService {
|
|
|
2657
2998
|
execution: this.executionContextService,
|
|
2658
2999
|
method: this.methodContextService,
|
|
2659
3000
|
logger: this.loggerService,
|
|
3001
|
+
partial: this.partialConnectionService,
|
|
2660
3002
|
exchange: this.exchangeConnectionService,
|
|
2661
3003
|
risk: riskName ? this.riskConnectionService.getRisk(riskName) : NOOP_RISK,
|
|
2662
3004
|
riskName,
|
|
@@ -2669,11 +3011,14 @@ class StrategyConnectionService {
|
|
|
2669
3011
|
* Retrieves the currently active pending signal for the strategy.
|
|
2670
3012
|
* If no active signal exists, returns null.
|
|
2671
3013
|
* Used internally for monitoring TP/SL and time expiration.
|
|
3014
|
+
*
|
|
3015
|
+
* @param strategyName - Name of strategy to get pending signal for
|
|
3016
|
+
*
|
|
2672
3017
|
* @returns Promise resolving to pending signal or null
|
|
2673
3018
|
*/
|
|
2674
|
-
this.getPendingSignal = async () => {
|
|
3019
|
+
this.getPendingSignal = async (strategyName) => {
|
|
2675
3020
|
this.loggerService.log("strategyConnectionService getPendingSignal");
|
|
2676
|
-
const strategy = await this.getStrategy(
|
|
3021
|
+
const strategy = await this.getStrategy(strategyName);
|
|
2677
3022
|
return await strategy.getPendingSignal();
|
|
2678
3023
|
};
|
|
2679
3024
|
/**
|
|
@@ -3140,7 +3485,7 @@ const DO_VALIDATION_FN = trycatch(async (validation, params) => {
|
|
|
3140
3485
|
* Uses singleshot pattern to ensure it only runs once.
|
|
3141
3486
|
* This function is exported for use in tests or other modules.
|
|
3142
3487
|
*/
|
|
3143
|
-
const WAIT_FOR_INIT_FN = async (self) => {
|
|
3488
|
+
const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
3144
3489
|
self.params.logger.debug("ClientRisk waitForInit");
|
|
3145
3490
|
const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName);
|
|
3146
3491
|
self._activePositions = new Map(persistedPositions);
|
|
@@ -3171,7 +3516,7 @@ class ClientRisk {
|
|
|
3171
3516
|
* Uses singleshot pattern to ensure initialization happens exactly once.
|
|
3172
3517
|
* Skips persistence in backtest mode.
|
|
3173
3518
|
*/
|
|
3174
|
-
this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN(this));
|
|
3519
|
+
this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
|
|
3175
3520
|
/**
|
|
3176
3521
|
* Checks if a signal should be allowed based on risk limits.
|
|
3177
3522
|
*
|
|
@@ -3588,20 +3933,12 @@ class StrategyGlobalService {
|
|
|
3588
3933
|
* @param backtest - Whether running in backtest mode
|
|
3589
3934
|
* @returns Promise resolving to pending signal or null
|
|
3590
3935
|
*/
|
|
3591
|
-
this.getPendingSignal = async (
|
|
3936
|
+
this.getPendingSignal = async (strategyName) => {
|
|
3592
3937
|
this.loggerService.log("strategyGlobalService getPendingSignal", {
|
|
3593
|
-
|
|
3594
|
-
when,
|
|
3595
|
-
backtest,
|
|
3938
|
+
strategyName,
|
|
3596
3939
|
});
|
|
3597
3940
|
await this.validate(this.methodContextService.context.strategyName);
|
|
3598
|
-
return await
|
|
3599
|
-
return await this.strategyConnectionService.getPendingSignal();
|
|
3600
|
-
}, {
|
|
3601
|
-
symbol,
|
|
3602
|
-
when,
|
|
3603
|
-
backtest,
|
|
3604
|
-
});
|
|
3941
|
+
return await this.strategyConnectionService.getPendingSignal(strategyName);
|
|
3605
3942
|
};
|
|
3606
3943
|
/**
|
|
3607
3944
|
* Checks signal status at a specific timestamp.
|
|
@@ -4354,7 +4691,7 @@ class BacktestLogicPrivateService {
|
|
|
4354
4691
|
const when = timeframes[i];
|
|
4355
4692
|
// Emit progress event if context is available
|
|
4356
4693
|
{
|
|
4357
|
-
await
|
|
4694
|
+
await progressBacktestEmitter.next({
|
|
4358
4695
|
exchangeName: this.methodContextService.context.exchangeName,
|
|
4359
4696
|
strategyName: this.methodContextService.context.strategyName,
|
|
4360
4697
|
symbol,
|
|
@@ -4488,7 +4825,7 @@ class BacktestLogicPrivateService {
|
|
|
4488
4825
|
}
|
|
4489
4826
|
// Emit final progress event (100%)
|
|
4490
4827
|
{
|
|
4491
|
-
await
|
|
4828
|
+
await progressBacktestEmitter.next({
|
|
4492
4829
|
exchangeName: this.methodContextService.context.exchangeName,
|
|
4493
4830
|
strategyName: this.methodContextService.context.strategyName,
|
|
4494
4831
|
symbol,
|
|
@@ -4605,6 +4942,7 @@ class LiveLogicPrivateService {
|
|
|
4605
4942
|
}
|
|
4606
4943
|
}
|
|
4607
4944
|
|
|
4945
|
+
const CANCEL_SYMBOL = Symbol("CANCEL_SYMBOL");
|
|
4608
4946
|
/**
|
|
4609
4947
|
* Private service for walker orchestration (strategy comparison).
|
|
4610
4948
|
*
|
|
@@ -4662,6 +5000,10 @@ class WalkerLogicPrivateService {
|
|
|
4662
5000
|
let strategiesTested = 0;
|
|
4663
5001
|
let bestMetric = null;
|
|
4664
5002
|
let bestStrategy = null;
|
|
5003
|
+
const listenStop = walkerStopSubject
|
|
5004
|
+
.filter((walkerName) => walkerName === context.walkerName)
|
|
5005
|
+
.map(() => CANCEL_SYMBOL)
|
|
5006
|
+
.toPromise();
|
|
4665
5007
|
// Run backtest for each strategy
|
|
4666
5008
|
for (const strategyName of strategies) {
|
|
4667
5009
|
// Call onStrategyStart callback if provided
|
|
@@ -4677,7 +5019,16 @@ class WalkerLogicPrivateService {
|
|
|
4677
5019
|
exchangeName: context.exchangeName,
|
|
4678
5020
|
frameName: context.frameName,
|
|
4679
5021
|
});
|
|
4680
|
-
await
|
|
5022
|
+
const result = await Promise.race([
|
|
5023
|
+
await resolveDocuments(iterator),
|
|
5024
|
+
listenStop,
|
|
5025
|
+
]);
|
|
5026
|
+
if (result === CANCEL_SYMBOL) {
|
|
5027
|
+
this.loggerService.info("walkerLogicPrivateService received stop signal, cancelling walker", {
|
|
5028
|
+
context,
|
|
5029
|
+
});
|
|
5030
|
+
break;
|
|
5031
|
+
}
|
|
4681
5032
|
this.loggerService.info("walkerLogicPrivateService backtest complete", {
|
|
4682
5033
|
strategyName,
|
|
4683
5034
|
symbol,
|
|
@@ -4715,6 +5066,16 @@ class WalkerLogicPrivateService {
|
|
|
4715
5066
|
strategiesTested,
|
|
4716
5067
|
totalStrategies: strategies.length,
|
|
4717
5068
|
};
|
|
5069
|
+
// Emit progress event
|
|
5070
|
+
await progressWalkerEmitter.next({
|
|
5071
|
+
walkerName: context.walkerName,
|
|
5072
|
+
exchangeName: context.exchangeName,
|
|
5073
|
+
frameName: context.frameName,
|
|
5074
|
+
symbol,
|
|
5075
|
+
totalStrategies: strategies.length,
|
|
5076
|
+
processedStrategies: strategiesTested,
|
|
5077
|
+
progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
|
|
5078
|
+
});
|
|
4718
5079
|
// Call onStrategyComplete callback if provided
|
|
4719
5080
|
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
4720
5081
|
walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
@@ -5060,7 +5421,7 @@ function isUnsafe$3(value) {
|
|
|
5060
5421
|
}
|
|
5061
5422
|
return false;
|
|
5062
5423
|
}
|
|
5063
|
-
const columns$
|
|
5424
|
+
const columns$4 = [
|
|
5064
5425
|
{
|
|
5065
5426
|
key: "signalId",
|
|
5066
5427
|
label: "Signal ID",
|
|
@@ -5138,7 +5499,7 @@ const columns$3 = [
|
|
|
5138
5499
|
* Storage class for accumulating closed signals per strategy.
|
|
5139
5500
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
5140
5501
|
*/
|
|
5141
|
-
let ReportStorage$
|
|
5502
|
+
let ReportStorage$4 = class ReportStorage {
|
|
5142
5503
|
constructor() {
|
|
5143
5504
|
/** Internal list of all closed signals for this strategy */
|
|
5144
5505
|
this._signalList = [];
|
|
@@ -5228,9 +5589,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
5228
5589
|
if (stats.totalSignals === 0) {
|
|
5229
5590
|
return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
5230
5591
|
}
|
|
5231
|
-
const header = columns$
|
|
5232
|
-
const separator = columns$
|
|
5233
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
5592
|
+
const header = columns$4.map((col) => col.label);
|
|
5593
|
+
const separator = columns$4.map(() => "---");
|
|
5594
|
+
const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
|
|
5234
5595
|
const tableData = [header, separator, ...rows];
|
|
5235
5596
|
const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5236
5597
|
return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
@@ -5239,9 +5600,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
5239
5600
|
* Saves strategy report to disk.
|
|
5240
5601
|
*
|
|
5241
5602
|
* @param strategyName - Strategy name
|
|
5242
|
-
* @param path - Directory path to save report (default: "./
|
|
5603
|
+
* @param path - Directory path to save report (default: "./dump/backtest")
|
|
5243
5604
|
*/
|
|
5244
|
-
async dump(strategyName, path = "./
|
|
5605
|
+
async dump(strategyName, path = "./dump/backtest") {
|
|
5245
5606
|
const markdown = await this.getReport(strategyName);
|
|
5246
5607
|
try {
|
|
5247
5608
|
const dir = join(process.cwd(), path);
|
|
@@ -5291,7 +5652,7 @@ class BacktestMarkdownService {
|
|
|
5291
5652
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
5292
5653
|
* Each strategy gets its own isolated storage instance.
|
|
5293
5654
|
*/
|
|
5294
|
-
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
5655
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$4());
|
|
5295
5656
|
/**
|
|
5296
5657
|
* Processes tick events and accumulates closed signals.
|
|
5297
5658
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -5369,20 +5730,20 @@ class BacktestMarkdownService {
|
|
|
5369
5730
|
* Delegates to ReportStorage.dump().
|
|
5370
5731
|
*
|
|
5371
5732
|
* @param strategyName - Strategy name to save report for
|
|
5372
|
-
* @param path - Directory path to save report (default: "./
|
|
5733
|
+
* @param path - Directory path to save report (default: "./dump/backtest")
|
|
5373
5734
|
*
|
|
5374
5735
|
* @example
|
|
5375
5736
|
* ```typescript
|
|
5376
5737
|
* const service = new BacktestMarkdownService();
|
|
5377
5738
|
*
|
|
5378
|
-
* // Save to default path: ./
|
|
5739
|
+
* // Save to default path: ./dump/backtest/my-strategy.md
|
|
5379
5740
|
* await service.dump("my-strategy");
|
|
5380
5741
|
*
|
|
5381
5742
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5382
5743
|
* await service.dump("my-strategy", "./custom/path");
|
|
5383
5744
|
* ```
|
|
5384
5745
|
*/
|
|
5385
|
-
this.dump = async (strategyName, path = "./
|
|
5746
|
+
this.dump = async (strategyName, path = "./dump/backtest") => {
|
|
5386
5747
|
this.loggerService.log("backtestMarkdownService dump", {
|
|
5387
5748
|
strategyName,
|
|
5388
5749
|
path,
|
|
@@ -5450,7 +5811,7 @@ function isUnsafe$2(value) {
|
|
|
5450
5811
|
}
|
|
5451
5812
|
return false;
|
|
5452
5813
|
}
|
|
5453
|
-
const columns$
|
|
5814
|
+
const columns$3 = [
|
|
5454
5815
|
{
|
|
5455
5816
|
key: "timestamp",
|
|
5456
5817
|
label: "Timestamp",
|
|
@@ -5524,12 +5885,12 @@ const columns$2 = [
|
|
|
5524
5885
|
},
|
|
5525
5886
|
];
|
|
5526
5887
|
/** Maximum number of events to store in live trading reports */
|
|
5527
|
-
const MAX_EVENTS$
|
|
5888
|
+
const MAX_EVENTS$3 = 250;
|
|
5528
5889
|
/**
|
|
5529
5890
|
* Storage class for accumulating all tick events per strategy.
|
|
5530
5891
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
5531
5892
|
*/
|
|
5532
|
-
let ReportStorage$
|
|
5893
|
+
let ReportStorage$3 = class ReportStorage {
|
|
5533
5894
|
constructor() {
|
|
5534
5895
|
/** Internal list of all tick events for this strategy */
|
|
5535
5896
|
this._eventList = [];
|
|
@@ -5557,7 +5918,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5557
5918
|
}
|
|
5558
5919
|
{
|
|
5559
5920
|
this._eventList.push(newEvent);
|
|
5560
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5921
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5561
5922
|
this._eventList.shift();
|
|
5562
5923
|
}
|
|
5563
5924
|
}
|
|
@@ -5581,7 +5942,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5581
5942
|
stopLoss: data.signal.priceStopLoss,
|
|
5582
5943
|
});
|
|
5583
5944
|
// Trim queue if exceeded MAX_EVENTS
|
|
5584
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5945
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5585
5946
|
this._eventList.shift();
|
|
5586
5947
|
}
|
|
5587
5948
|
}
|
|
@@ -5613,7 +5974,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5613
5974
|
else {
|
|
5614
5975
|
this._eventList.push(newEvent);
|
|
5615
5976
|
// Trim queue if exceeded MAX_EVENTS
|
|
5616
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5977
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5617
5978
|
this._eventList.shift();
|
|
5618
5979
|
}
|
|
5619
5980
|
}
|
|
@@ -5651,7 +6012,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5651
6012
|
else {
|
|
5652
6013
|
this._eventList.push(newEvent);
|
|
5653
6014
|
// Trim queue if exceeded MAX_EVENTS
|
|
5654
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6015
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5655
6016
|
this._eventList.shift();
|
|
5656
6017
|
}
|
|
5657
6018
|
}
|
|
@@ -5748,9 +6109,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5748
6109
|
if (stats.totalEvents === 0) {
|
|
5749
6110
|
return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
5750
6111
|
}
|
|
5751
|
-
const header = columns$
|
|
5752
|
-
const separator = columns$
|
|
5753
|
-
const rows = this._eventList.map((event) => columns$
|
|
6112
|
+
const header = columns$3.map((col) => col.label);
|
|
6113
|
+
const separator = columns$3.map(() => "---");
|
|
6114
|
+
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
5754
6115
|
const tableData = [header, separator, ...rows];
|
|
5755
6116
|
const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5756
6117
|
return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
@@ -5759,9 +6120,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5759
6120
|
* Saves strategy report to disk.
|
|
5760
6121
|
*
|
|
5761
6122
|
* @param strategyName - Strategy name
|
|
5762
|
-
* @param path - Directory path to save report (default: "./
|
|
6123
|
+
* @param path - Directory path to save report (default: "./dump/live")
|
|
5763
6124
|
*/
|
|
5764
|
-
async dump(strategyName, path = "./
|
|
6125
|
+
async dump(strategyName, path = "./dump/live") {
|
|
5765
6126
|
const markdown = await this.getReport(strategyName);
|
|
5766
6127
|
try {
|
|
5767
6128
|
const dir = join(process.cwd(), path);
|
|
@@ -5814,7 +6175,7 @@ class LiveMarkdownService {
|
|
|
5814
6175
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
5815
6176
|
* Each strategy gets its own isolated storage instance.
|
|
5816
6177
|
*/
|
|
5817
|
-
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
6178
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
|
|
5818
6179
|
/**
|
|
5819
6180
|
* Processes tick events and accumulates all event types.
|
|
5820
6181
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -5902,20 +6263,20 @@ class LiveMarkdownService {
|
|
|
5902
6263
|
* Delegates to ReportStorage.dump().
|
|
5903
6264
|
*
|
|
5904
6265
|
* @param strategyName - Strategy name to save report for
|
|
5905
|
-
* @param path - Directory path to save report (default: "./
|
|
6266
|
+
* @param path - Directory path to save report (default: "./dump/live")
|
|
5906
6267
|
*
|
|
5907
6268
|
* @example
|
|
5908
6269
|
* ```typescript
|
|
5909
6270
|
* const service = new LiveMarkdownService();
|
|
5910
6271
|
*
|
|
5911
|
-
* // Save to default path: ./
|
|
6272
|
+
* // Save to default path: ./dump/live/my-strategy.md
|
|
5912
6273
|
* await service.dump("my-strategy");
|
|
5913
6274
|
*
|
|
5914
6275
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5915
6276
|
* await service.dump("my-strategy", "./custom/path");
|
|
5916
6277
|
* ```
|
|
5917
6278
|
*/
|
|
5918
|
-
this.dump = async (strategyName, path = "./
|
|
6279
|
+
this.dump = async (strategyName, path = "./dump/live") => {
|
|
5919
6280
|
this.loggerService.log("liveMarkdownService dump", {
|
|
5920
6281
|
strategyName,
|
|
5921
6282
|
path,
|
|
@@ -5965,7 +6326,7 @@ class LiveMarkdownService {
|
|
|
5965
6326
|
}
|
|
5966
6327
|
}
|
|
5967
6328
|
|
|
5968
|
-
const columns$
|
|
6329
|
+
const columns$2 = [
|
|
5969
6330
|
{
|
|
5970
6331
|
key: "timestamp",
|
|
5971
6332
|
label: "Timestamp",
|
|
@@ -6023,12 +6384,12 @@ const columns$1 = [
|
|
|
6023
6384
|
},
|
|
6024
6385
|
];
|
|
6025
6386
|
/** Maximum number of events to store in schedule reports */
|
|
6026
|
-
const MAX_EVENTS$
|
|
6387
|
+
const MAX_EVENTS$2 = 250;
|
|
6027
6388
|
/**
|
|
6028
6389
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
6029
6390
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
6030
6391
|
*/
|
|
6031
|
-
let ReportStorage$
|
|
6392
|
+
let ReportStorage$2 = class ReportStorage {
|
|
6032
6393
|
constructor() {
|
|
6033
6394
|
/** Internal list of all scheduled events for this strategy */
|
|
6034
6395
|
this._eventList = [];
|
|
@@ -6052,7 +6413,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6052
6413
|
stopLoss: data.signal.priceStopLoss,
|
|
6053
6414
|
});
|
|
6054
6415
|
// Trim queue if exceeded MAX_EVENTS
|
|
6055
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6416
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
6056
6417
|
this._eventList.shift();
|
|
6057
6418
|
}
|
|
6058
6419
|
}
|
|
@@ -6088,7 +6449,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6088
6449
|
else {
|
|
6089
6450
|
this._eventList.push(newEvent);
|
|
6090
6451
|
// Trim queue if exceeded MAX_EVENTS
|
|
6091
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6452
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
6092
6453
|
this._eventList.shift();
|
|
6093
6454
|
}
|
|
6094
6455
|
}
|
|
@@ -6140,9 +6501,9 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6140
6501
|
if (stats.totalEvents === 0) {
|
|
6141
6502
|
return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
|
|
6142
6503
|
}
|
|
6143
|
-
const header = columns$
|
|
6144
|
-
const separator = columns$
|
|
6145
|
-
const rows = this._eventList.map((event) => columns$
|
|
6504
|
+
const header = columns$2.map((col) => col.label);
|
|
6505
|
+
const separator = columns$2.map(() => "---");
|
|
6506
|
+
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
6146
6507
|
const tableData = [header, separator, ...rows];
|
|
6147
6508
|
const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
6148
6509
|
return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
|
|
@@ -6151,9 +6512,9 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6151
6512
|
* Saves strategy report to disk.
|
|
6152
6513
|
*
|
|
6153
6514
|
* @param strategyName - Strategy name
|
|
6154
|
-
* @param path - Directory path to save report (default: "./
|
|
6515
|
+
* @param path - Directory path to save report (default: "./dump/schedule")
|
|
6155
6516
|
*/
|
|
6156
|
-
async dump(strategyName, path = "./
|
|
6517
|
+
async dump(strategyName, path = "./dump/schedule") {
|
|
6157
6518
|
const markdown = await this.getReport(strategyName);
|
|
6158
6519
|
try {
|
|
6159
6520
|
const dir = join(process.cwd(), path);
|
|
@@ -6197,7 +6558,7 @@ class ScheduleMarkdownService {
|
|
|
6197
6558
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
6198
6559
|
* Each strategy gets its own isolated storage instance.
|
|
6199
6560
|
*/
|
|
6200
|
-
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
6561
|
+
this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
6201
6562
|
/**
|
|
6202
6563
|
* Processes tick events and accumulates scheduled/cancelled events.
|
|
6203
6564
|
* Should be called from signalLiveEmitter subscription.
|
|
@@ -6272,20 +6633,20 @@ class ScheduleMarkdownService {
|
|
|
6272
6633
|
* Delegates to ReportStorage.dump().
|
|
6273
6634
|
*
|
|
6274
6635
|
* @param strategyName - Strategy name to save report for
|
|
6275
|
-
* @param path - Directory path to save report (default: "./
|
|
6636
|
+
* @param path - Directory path to save report (default: "./dump/schedule")
|
|
6276
6637
|
*
|
|
6277
6638
|
* @example
|
|
6278
6639
|
* ```typescript
|
|
6279
6640
|
* const service = new ScheduleMarkdownService();
|
|
6280
6641
|
*
|
|
6281
|
-
* // Save to default path: ./
|
|
6642
|
+
* // Save to default path: ./dump/schedule/my-strategy.md
|
|
6282
6643
|
* await service.dump("my-strategy");
|
|
6283
6644
|
*
|
|
6284
6645
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
6285
6646
|
* await service.dump("my-strategy", "./custom/path");
|
|
6286
6647
|
* ```
|
|
6287
6648
|
*/
|
|
6288
|
-
this.dump = async (strategyName, path = "./
|
|
6649
|
+
this.dump = async (strategyName, path = "./dump/schedule") => {
|
|
6289
6650
|
this.loggerService.log("scheduleMarkdownService dump", {
|
|
6290
6651
|
strategyName,
|
|
6291
6652
|
path,
|
|
@@ -6345,7 +6706,7 @@ function percentile(sortedArray, p) {
|
|
|
6345
6706
|
return sortedArray[Math.max(0, index)];
|
|
6346
6707
|
}
|
|
6347
6708
|
/** Maximum number of performance events to store per strategy */
|
|
6348
|
-
const MAX_EVENTS = 10000;
|
|
6709
|
+
const MAX_EVENTS$1 = 10000;
|
|
6349
6710
|
/**
|
|
6350
6711
|
* Storage class for accumulating performance metrics per strategy.
|
|
6351
6712
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -6363,7 +6724,7 @@ class PerformanceStorage {
|
|
|
6363
6724
|
addEvent(event) {
|
|
6364
6725
|
this._events.push(event);
|
|
6365
6726
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
6366
|
-
if (this._events.length > MAX_EVENTS) {
|
|
6727
|
+
if (this._events.length > MAX_EVENTS$1) {
|
|
6367
6728
|
this._events.shift();
|
|
6368
6729
|
}
|
|
6369
6730
|
}
|
|
@@ -6502,7 +6863,7 @@ class PerformanceStorage {
|
|
|
6502
6863
|
* @param strategyName - Strategy name
|
|
6503
6864
|
* @param path - Directory path to save report
|
|
6504
6865
|
*/
|
|
6505
|
-
async dump(strategyName, path = "./
|
|
6866
|
+
async dump(strategyName, path = "./dump/performance") {
|
|
6506
6867
|
const markdown = await this.getReport(strategyName);
|
|
6507
6868
|
try {
|
|
6508
6869
|
const dir = join(process.cwd(), path);
|
|
@@ -6615,14 +6976,14 @@ class PerformanceMarkdownService {
|
|
|
6615
6976
|
*
|
|
6616
6977
|
* @example
|
|
6617
6978
|
* ```typescript
|
|
6618
|
-
* // Save to default path: ./
|
|
6979
|
+
* // Save to default path: ./dump/performance/my-strategy.md
|
|
6619
6980
|
* await performanceService.dump("my-strategy");
|
|
6620
6981
|
*
|
|
6621
6982
|
* // Save to custom path
|
|
6622
6983
|
* await performanceService.dump("my-strategy", "./custom/path");
|
|
6623
6984
|
* ```
|
|
6624
6985
|
*/
|
|
6625
|
-
this.dump = async (strategyName, path = "./
|
|
6986
|
+
this.dump = async (strategyName, path = "./dump/performance") => {
|
|
6626
6987
|
this.loggerService.log("performanceMarkdownService dump", {
|
|
6627
6988
|
strategyName,
|
|
6628
6989
|
path,
|
|
@@ -6684,7 +7045,7 @@ function formatMetric(value) {
|
|
|
6684
7045
|
* Storage class for accumulating walker results.
|
|
6685
7046
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
6686
7047
|
*/
|
|
6687
|
-
class ReportStorage {
|
|
7048
|
+
let ReportStorage$1 = class ReportStorage {
|
|
6688
7049
|
constructor(walkerName) {
|
|
6689
7050
|
this.walkerName = walkerName;
|
|
6690
7051
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -6752,9 +7113,9 @@ class ReportStorage {
|
|
|
6752
7113
|
* @param symbol - Trading symbol
|
|
6753
7114
|
* @param metric - Metric being optimized
|
|
6754
7115
|
* @param context - Context with exchangeName and frameName
|
|
6755
|
-
* @param path - Directory path to save report (default: "./
|
|
7116
|
+
* @param path - Directory path to save report (default: "./dump/walker")
|
|
6756
7117
|
*/
|
|
6757
|
-
async dump(symbol, metric, context, path = "./
|
|
7118
|
+
async dump(symbol, metric, context, path = "./dump/walker") {
|
|
6758
7119
|
const markdown = await this.getReport(symbol, metric, context);
|
|
6759
7120
|
try {
|
|
6760
7121
|
const dir = join(process.cwd(), path);
|
|
@@ -6768,7 +7129,7 @@ class ReportStorage {
|
|
|
6768
7129
|
console.error(`Failed to save walker report:`, error);
|
|
6769
7130
|
}
|
|
6770
7131
|
}
|
|
6771
|
-
}
|
|
7132
|
+
};
|
|
6772
7133
|
/**
|
|
6773
7134
|
* Service for generating and saving walker markdown reports.
|
|
6774
7135
|
*
|
|
@@ -6793,7 +7154,7 @@ class WalkerMarkdownService {
|
|
|
6793
7154
|
* Memoized function to get or create ReportStorage for a walker.
|
|
6794
7155
|
* Each walker gets its own isolated storage instance.
|
|
6795
7156
|
*/
|
|
6796
|
-
this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage(walkerName));
|
|
7157
|
+
this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
|
|
6797
7158
|
/**
|
|
6798
7159
|
* Processes walker progress events and accumulates strategy results.
|
|
6799
7160
|
* Should be called from walkerEmitter.
|
|
@@ -6876,20 +7237,20 @@ class WalkerMarkdownService {
|
|
|
6876
7237
|
* @param symbol - Trading symbol
|
|
6877
7238
|
* @param metric - Metric being optimized
|
|
6878
7239
|
* @param context - Context with exchangeName and frameName
|
|
6879
|
-
* @param path - Directory path to save report (default: "./
|
|
7240
|
+
* @param path - Directory path to save report (default: "./dump/walker")
|
|
6880
7241
|
*
|
|
6881
7242
|
* @example
|
|
6882
7243
|
* ```typescript
|
|
6883
7244
|
* const service = new WalkerMarkdownService();
|
|
6884
7245
|
*
|
|
6885
|
-
* // Save to default path: ./
|
|
7246
|
+
* // Save to default path: ./dump/walker/my-walker.md
|
|
6886
7247
|
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
|
|
6887
7248
|
*
|
|
6888
7249
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
6889
7250
|
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" }, "./custom/path");
|
|
6890
7251
|
* ```
|
|
6891
7252
|
*/
|
|
6892
|
-
this.dump = async (walkerName, symbol, metric, context, path = "./
|
|
7253
|
+
this.dump = async (walkerName, symbol, metric, context, path = "./dump/walker") => {
|
|
6893
7254
|
this.loggerService.log("walkerMarkdownService dump", {
|
|
6894
7255
|
walkerName,
|
|
6895
7256
|
symbol,
|
|
@@ -6964,7 +7325,7 @@ function isUnsafe(value) {
|
|
|
6964
7325
|
}
|
|
6965
7326
|
return false;
|
|
6966
7327
|
}
|
|
6967
|
-
const columns = [
|
|
7328
|
+
const columns$1 = [
|
|
6968
7329
|
{
|
|
6969
7330
|
key: "symbol",
|
|
6970
7331
|
label: "Symbol",
|
|
@@ -7260,9 +7621,9 @@ class HeatmapStorage {
|
|
|
7260
7621
|
if (data.symbols.length === 0) {
|
|
7261
7622
|
return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
|
|
7262
7623
|
}
|
|
7263
|
-
const header = columns.map((col) => col.label);
|
|
7264
|
-
const separator = columns.map(() => "---");
|
|
7265
|
-
const rows = data.symbols.map((row) => columns.map((col) => col.format(row)));
|
|
7624
|
+
const header = columns$1.map((col) => col.label);
|
|
7625
|
+
const separator = columns$1.map(() => "---");
|
|
7626
|
+
const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
|
|
7266
7627
|
const tableData = [header, separator, ...rows];
|
|
7267
7628
|
const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
7268
7629
|
return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
|
|
@@ -7271,9 +7632,9 @@ class HeatmapStorage {
|
|
|
7271
7632
|
* Saves heatmap report to disk.
|
|
7272
7633
|
*
|
|
7273
7634
|
* @param strategyName - Strategy name for filename
|
|
7274
|
-
* @param path - Directory path to save report (default: "./
|
|
7635
|
+
* @param path - Directory path to save report (default: "./dump/heatmap")
|
|
7275
7636
|
*/
|
|
7276
|
-
async dump(strategyName, path = "./
|
|
7637
|
+
async dump(strategyName, path = "./dump/heatmap") {
|
|
7277
7638
|
const markdown = await this.getReport(strategyName);
|
|
7278
7639
|
try {
|
|
7279
7640
|
const dir = join(process.cwd(), path);
|
|
@@ -7404,20 +7765,20 @@ class HeatMarkdownService {
|
|
|
7404
7765
|
* Default filename: {strategyName}.md
|
|
7405
7766
|
*
|
|
7406
7767
|
* @param strategyName - Strategy name to save heatmap report for
|
|
7407
|
-
* @param path - Optional directory path to save report (default: "./
|
|
7768
|
+
* @param path - Optional directory path to save report (default: "./dump/heatmap")
|
|
7408
7769
|
*
|
|
7409
7770
|
* @example
|
|
7410
7771
|
* ```typescript
|
|
7411
7772
|
* const service = new HeatMarkdownService();
|
|
7412
7773
|
*
|
|
7413
|
-
* // Save to default path: ./
|
|
7774
|
+
* // Save to default path: ./dump/heatmap/my-strategy.md
|
|
7414
7775
|
* await service.dump("my-strategy");
|
|
7415
7776
|
*
|
|
7416
7777
|
* // Save to custom path: ./reports/my-strategy.md
|
|
7417
7778
|
* await service.dump("my-strategy", "./reports");
|
|
7418
7779
|
* ```
|
|
7419
7780
|
*/
|
|
7420
|
-
this.dump = async (strategyName, path = "./
|
|
7781
|
+
this.dump = async (strategyName, path = "./dump/heatmap") => {
|
|
7421
7782
|
this.loggerService.log(HEATMAP_METHOD_NAME_DUMP, {
|
|
7422
7783
|
strategyName,
|
|
7423
7784
|
path,
|
|
@@ -7849,34 +8210,2297 @@ class RiskValidationService {
|
|
|
7849
8210
|
}
|
|
7850
8211
|
}
|
|
7851
8212
|
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
{
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
{
|
|
7875
|
-
|
|
8213
|
+
/**
|
|
8214
|
+
* Default template service for generating optimizer code snippets.
|
|
8215
|
+
* Implements all IOptimizerTemplate methods with Ollama LLM integration.
|
|
8216
|
+
*
|
|
8217
|
+
* Features:
|
|
8218
|
+
* - Multi-timeframe analysis (1m, 5m, 15m, 1h)
|
|
8219
|
+
* - JSON structured output for signals
|
|
8220
|
+
* - Debug logging to ./dump/strategy
|
|
8221
|
+
* - CCXT exchange integration
|
|
8222
|
+
* - Walker-based strategy comparison
|
|
8223
|
+
*
|
|
8224
|
+
* Can be partially overridden in optimizer schema configuration.
|
|
8225
|
+
*/
|
|
8226
|
+
class OptimizerTemplateService {
|
|
8227
|
+
constructor() {
|
|
8228
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8229
|
+
/**
|
|
8230
|
+
* Generates the top banner with imports and constants.
|
|
8231
|
+
*
|
|
8232
|
+
* @param symbol - Trading pair symbol
|
|
8233
|
+
* @returns Shebang, imports, and WARN_KB constant
|
|
8234
|
+
*/
|
|
8235
|
+
this.getTopBanner = async (symbol) => {
|
|
8236
|
+
this.loggerService.log("optimizerTemplateService getTopBanner", {
|
|
8237
|
+
symbol,
|
|
8238
|
+
});
|
|
8239
|
+
return [
|
|
8240
|
+
"#!/usr/bin/env node",
|
|
8241
|
+
"",
|
|
8242
|
+
`import { Ollama } from "ollama";`,
|
|
8243
|
+
`import ccxt from "ccxt";`,
|
|
8244
|
+
`import {`,
|
|
8245
|
+
` addExchange,`,
|
|
8246
|
+
` addStrategy,`,
|
|
8247
|
+
` addFrame,`,
|
|
8248
|
+
` addWalker,`,
|
|
8249
|
+
` Walker,`,
|
|
8250
|
+
` Backtest,`,
|
|
8251
|
+
` getCandles,`,
|
|
8252
|
+
` listenSignalBacktest,`,
|
|
8253
|
+
` listenWalkerComplete,`,
|
|
8254
|
+
` listenDoneBacktest,`,
|
|
8255
|
+
` listenBacktestProgress,`,
|
|
8256
|
+
` listenWalkerProgress,`,
|
|
8257
|
+
` listenError,`,
|
|
8258
|
+
`} from "backtest-kit";`,
|
|
8259
|
+
`import { promises as fs } from "fs";`,
|
|
8260
|
+
`import { v4 as uuid } from "uuid";`,
|
|
8261
|
+
`import path from "path";`,
|
|
8262
|
+
``,
|
|
8263
|
+
`const WARN_KB = 100;`
|
|
8264
|
+
].join("\n");
|
|
8265
|
+
};
|
|
8266
|
+
/**
|
|
8267
|
+
* Generates default user message for LLM conversation.
|
|
8268
|
+
* Simple prompt to read and acknowledge data.
|
|
8269
|
+
*
|
|
8270
|
+
* @param symbol - Trading pair symbol
|
|
8271
|
+
* @param data - Fetched data array
|
|
8272
|
+
* @param name - Source name
|
|
8273
|
+
* @returns User message with JSON data
|
|
8274
|
+
*/
|
|
8275
|
+
this.getUserMessage = async (symbol, data, name) => {
|
|
8276
|
+
this.loggerService.log("optimizerTemplateService getUserMessage", {
|
|
8277
|
+
symbol,
|
|
8278
|
+
data,
|
|
8279
|
+
name,
|
|
8280
|
+
});
|
|
8281
|
+
return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
|
|
8282
|
+
};
|
|
8283
|
+
/**
|
|
8284
|
+
* Generates default assistant message for LLM conversation.
|
|
8285
|
+
* Simple acknowledgment response.
|
|
8286
|
+
*
|
|
8287
|
+
* @param symbol - Trading pair symbol
|
|
8288
|
+
* @param data - Fetched data array
|
|
8289
|
+
* @param name - Source name
|
|
8290
|
+
* @returns Assistant acknowledgment message
|
|
8291
|
+
*/
|
|
8292
|
+
this.getAssistantMessage = async (symbol, data, name) => {
|
|
8293
|
+
this.loggerService.log("optimizerTemplateService getAssistantMessage", {
|
|
8294
|
+
symbol,
|
|
8295
|
+
data,
|
|
8296
|
+
name,
|
|
8297
|
+
});
|
|
8298
|
+
return "ОК";
|
|
8299
|
+
};
|
|
8300
|
+
/**
|
|
8301
|
+
* Generates Walker configuration code.
|
|
8302
|
+
* Compares multiple strategies on test frame.
|
|
8303
|
+
*
|
|
8304
|
+
* @param walkerName - Unique walker identifier
|
|
8305
|
+
* @param exchangeName - Exchange to use for backtesting
|
|
8306
|
+
* @param frameName - Test frame name
|
|
8307
|
+
* @param strategies - Array of strategy names to compare
|
|
8308
|
+
* @returns Generated addWalker() call
|
|
8309
|
+
*/
|
|
8310
|
+
this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
|
|
8311
|
+
this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
|
|
8312
|
+
walkerName,
|
|
8313
|
+
exchangeName,
|
|
8314
|
+
frameName,
|
|
8315
|
+
strategies,
|
|
8316
|
+
});
|
|
8317
|
+
// Escape special characters to prevent code injection
|
|
8318
|
+
const escapedWalkerName = String(walkerName)
|
|
8319
|
+
.replace(/\\/g, '\\\\')
|
|
8320
|
+
.replace(/"/g, '\\"');
|
|
8321
|
+
const escapedExchangeName = String(exchangeName)
|
|
8322
|
+
.replace(/\\/g, '\\\\')
|
|
8323
|
+
.replace(/"/g, '\\"');
|
|
8324
|
+
const escapedFrameName = String(frameName)
|
|
8325
|
+
.replace(/\\/g, '\\\\')
|
|
8326
|
+
.replace(/"/g, '\\"');
|
|
8327
|
+
const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
|
|
8328
|
+
return [
|
|
8329
|
+
`addWalker({`,
|
|
8330
|
+
` walkerName: "${escapedWalkerName}",`,
|
|
8331
|
+
` exchangeName: "${escapedExchangeName}",`,
|
|
8332
|
+
` frameName: "${escapedFrameName}",`,
|
|
8333
|
+
` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
|
|
8334
|
+
`});`
|
|
8335
|
+
].join("\n");
|
|
8336
|
+
};
|
|
8337
|
+
/**
|
|
8338
|
+
* Generates Strategy configuration with LLM integration.
|
|
8339
|
+
* Includes multi-timeframe analysis and signal generation.
|
|
8340
|
+
*
|
|
8341
|
+
* @param strategyName - Unique strategy identifier
|
|
8342
|
+
* @param interval - Signal throttling interval (e.g., "5m")
|
|
8343
|
+
* @param prompt - Strategy logic from getPrompt()
|
|
8344
|
+
* @returns Generated addStrategy() call with getSignal() function
|
|
8345
|
+
*/
|
|
8346
|
+
this.getStrategyTemplate = async (strategyName, interval, prompt) => {
|
|
8347
|
+
this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
|
|
8348
|
+
strategyName,
|
|
8349
|
+
interval,
|
|
8350
|
+
prompt,
|
|
8351
|
+
});
|
|
8352
|
+
// Escape special characters to prevent code injection
|
|
8353
|
+
const escapedStrategyName = String(strategyName)
|
|
8354
|
+
.replace(/\\/g, '\\\\')
|
|
8355
|
+
.replace(/"/g, '\\"');
|
|
8356
|
+
const escapedInterval = String(interval)
|
|
8357
|
+
.replace(/\\/g, '\\\\')
|
|
8358
|
+
.replace(/"/g, '\\"');
|
|
8359
|
+
const escapedPrompt = String(prompt)
|
|
8360
|
+
.replace(/\\/g, '\\\\')
|
|
8361
|
+
.replace(/`/g, '\\`')
|
|
8362
|
+
.replace(/\$/g, '\\$');
|
|
8363
|
+
return [
|
|
8364
|
+
`addStrategy({`,
|
|
8365
|
+
` strategyName: "${escapedStrategyName}",`,
|
|
8366
|
+
` interval: "${escapedInterval}",`,
|
|
8367
|
+
` getSignal: async (symbol) => {`,
|
|
8368
|
+
` const messages = [];`,
|
|
8369
|
+
``,
|
|
8370
|
+
` // Загружаем данные всех таймфреймов`,
|
|
8371
|
+
` const microTermCandles = await getCandles(symbol, "1m", 30);`,
|
|
8372
|
+
` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
|
|
8373
|
+
` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
|
|
8374
|
+
` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
|
|
8375
|
+
``,
|
|
8376
|
+
` function formatCandles(candles, timeframe) {`,
|
|
8377
|
+
` return candles.map((c) =>`,
|
|
8378
|
+
` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
|
|
8379
|
+
` ).join("\\n");`,
|
|
8380
|
+
` }`,
|
|
8381
|
+
``,
|
|
8382
|
+
` // Сообщение 1: Среднесрочный тренд`,
|
|
8383
|
+
` messages.push(`,
|
|
8384
|
+
` {`,
|
|
8385
|
+
` role: "user",`,
|
|
8386
|
+
` content: [`,
|
|
8387
|
+
` \`\${symbol}\`,`,
|
|
8388
|
+
` "Проанализируй свечи 1h:",`,
|
|
8389
|
+
` "",`,
|
|
8390
|
+
` formatCandles(mediumTermCandles, "1h")`,
|
|
8391
|
+
` ].join("\\n"),`,
|
|
8392
|
+
` },`,
|
|
8393
|
+
` {`,
|
|
8394
|
+
` role: "assistant",`,
|
|
8395
|
+
` content: "Тренд 1h проанализирован",`,
|
|
8396
|
+
` }`,
|
|
8397
|
+
` );`,
|
|
8398
|
+
``,
|
|
8399
|
+
` // Сообщение 2: Краткосрочный тренд`,
|
|
8400
|
+
` messages.push(`,
|
|
8401
|
+
` {`,
|
|
8402
|
+
` role: "user",`,
|
|
8403
|
+
` content: [`,
|
|
8404
|
+
` "Проанализируй свечи 15m:",`,
|
|
8405
|
+
` "",`,
|
|
8406
|
+
` formatCandles(shortTermCandles, "15m")`,
|
|
8407
|
+
` ].join("\\n"),`,
|
|
8408
|
+
` },`,
|
|
8409
|
+
` {`,
|
|
8410
|
+
` role: "assistant",`,
|
|
8411
|
+
` content: "Тренд 15m проанализирован",`,
|
|
8412
|
+
` }`,
|
|
8413
|
+
` );`,
|
|
8414
|
+
``,
|
|
8415
|
+
` // Сообщение 3: Основной таймфрейм`,
|
|
8416
|
+
` messages.push(`,
|
|
8417
|
+
` {`,
|
|
8418
|
+
` role: "user",`,
|
|
8419
|
+
` content: [`,
|
|
8420
|
+
` "Проанализируй свечи 5m:",`,
|
|
8421
|
+
` "",`,
|
|
8422
|
+
` formatCandles(mainTermCandles, "5m")`,
|
|
8423
|
+
` ].join("\\n")`,
|
|
8424
|
+
` },`,
|
|
8425
|
+
` {`,
|
|
8426
|
+
` role: "assistant",`,
|
|
8427
|
+
` content: "Таймфрейм 5m проанализирован",`,
|
|
8428
|
+
` }`,
|
|
8429
|
+
` );`,
|
|
8430
|
+
``,
|
|
8431
|
+
` // Сообщение 4: Микро-структура`,
|
|
8432
|
+
` messages.push(`,
|
|
8433
|
+
` {`,
|
|
8434
|
+
` role: "user",`,
|
|
8435
|
+
` content: [`,
|
|
8436
|
+
` "Проанализируй свечи 1m:",`,
|
|
8437
|
+
` "",`,
|
|
8438
|
+
` formatCandles(microTermCandles, "1m")`,
|
|
8439
|
+
` ].join("\\n")`,
|
|
8440
|
+
` },`,
|
|
8441
|
+
` {`,
|
|
8442
|
+
` role: "assistant",`,
|
|
8443
|
+
` content: "Микроструктура 1m проанализирована",`,
|
|
8444
|
+
` }`,
|
|
8445
|
+
` );`,
|
|
8446
|
+
``,
|
|
8447
|
+
` // Сообщение 5: Запрос сигнала`,
|
|
8448
|
+
` messages.push(`,
|
|
8449
|
+
` {`,
|
|
8450
|
+
` role: "user",`,
|
|
8451
|
+
` content: [`,
|
|
8452
|
+
` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
|
|
8453
|
+
` "",`,
|
|
8454
|
+
` \`${escapedPrompt}\`,`,
|
|
8455
|
+
` "",`,
|
|
8456
|
+
` "Если сигналы противоречивы или тренд слабый то position: wait"`,
|
|
8457
|
+
` ].join("\\n"),`,
|
|
8458
|
+
` }`,
|
|
8459
|
+
` );`,
|
|
8460
|
+
``,
|
|
8461
|
+
` const resultId = uuid();`,
|
|
8462
|
+
``,
|
|
8463
|
+
` const result = await json(messages);`,
|
|
8464
|
+
``,
|
|
8465
|
+
` await dumpJson(resultId, messages, result);`,
|
|
8466
|
+
``,
|
|
8467
|
+
` return result;`,
|
|
8468
|
+
` },`,
|
|
8469
|
+
`});`
|
|
8470
|
+
].join("\n");
|
|
8471
|
+
};
|
|
8472
|
+
/**
|
|
8473
|
+
* Generates Exchange configuration code.
|
|
8474
|
+
* Uses CCXT Binance with standard formatters.
|
|
8475
|
+
*
|
|
8476
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8477
|
+
* @param exchangeName - Unique exchange identifier
|
|
8478
|
+
* @returns Generated addExchange() call with CCXT integration
|
|
8479
|
+
*/
|
|
8480
|
+
this.getExchangeTemplate = async (symbol, exchangeName) => {
|
|
8481
|
+
this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
|
|
8482
|
+
exchangeName,
|
|
8483
|
+
symbol,
|
|
8484
|
+
});
|
|
8485
|
+
// Escape special characters to prevent code injection
|
|
8486
|
+
const escapedExchangeName = String(exchangeName)
|
|
8487
|
+
.replace(/\\/g, '\\\\')
|
|
8488
|
+
.replace(/"/g, '\\"');
|
|
8489
|
+
return [
|
|
8490
|
+
`addExchange({`,
|
|
8491
|
+
` exchangeName: "${escapedExchangeName}",`,
|
|
8492
|
+
` getCandles: async (symbol, interval, since, limit) => {`,
|
|
8493
|
+
` const exchange = new ccxt.binance();`,
|
|
8494
|
+
` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
|
|
8495
|
+
` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
|
|
8496
|
+
` timestamp, open, high, low, close, volume`,
|
|
8497
|
+
` }));`,
|
|
8498
|
+
` },`,
|
|
8499
|
+
` formatPrice: async (symbol, price) => price.toFixed(2),`,
|
|
8500
|
+
` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
|
|
8501
|
+
`});`
|
|
8502
|
+
].join("\n");
|
|
8503
|
+
};
|
|
8504
|
+
/**
|
|
8505
|
+
* Generates Frame (timeframe) configuration code.
|
|
8506
|
+
*
|
|
8507
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8508
|
+
* @param frameName - Unique frame identifier
|
|
8509
|
+
* @param interval - Candle interval (e.g., "1m")
|
|
8510
|
+
* @param startDate - Frame start date
|
|
8511
|
+
* @param endDate - Frame end date
|
|
8512
|
+
* @returns Generated addFrame() call
|
|
8513
|
+
*/
|
|
8514
|
+
this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
|
|
8515
|
+
this.loggerService.log("optimizerTemplateService getFrameTemplate", {
|
|
8516
|
+
symbol,
|
|
8517
|
+
frameName,
|
|
8518
|
+
interval,
|
|
8519
|
+
startDate,
|
|
8520
|
+
endDate,
|
|
8521
|
+
});
|
|
8522
|
+
// Escape special characters to prevent code injection
|
|
8523
|
+
const escapedFrameName = String(frameName)
|
|
8524
|
+
.replace(/\\/g, '\\\\')
|
|
8525
|
+
.replace(/"/g, '\\"');
|
|
8526
|
+
const escapedInterval = String(interval)
|
|
8527
|
+
.replace(/\\/g, '\\\\')
|
|
8528
|
+
.replace(/"/g, '\\"');
|
|
8529
|
+
return [
|
|
8530
|
+
`addFrame({`,
|
|
8531
|
+
` frameName: "${escapedFrameName}",`,
|
|
8532
|
+
` interval: "${escapedInterval}",`,
|
|
8533
|
+
` startDate: new Date("${startDate.toISOString()}"),`,
|
|
8534
|
+
` endDate: new Date("${endDate.toISOString()}"),`,
|
|
8535
|
+
`});`
|
|
8536
|
+
].join("\n");
|
|
8537
|
+
};
|
|
8538
|
+
/**
|
|
8539
|
+
* Generates launcher code to run Walker with event listeners.
|
|
8540
|
+
* Includes progress tracking and completion handlers.
|
|
8541
|
+
*
|
|
8542
|
+
* @param symbol - Trading pair symbol
|
|
8543
|
+
* @param walkerName - Walker name to launch
|
|
8544
|
+
* @returns Generated Walker.background() call with listeners
|
|
8545
|
+
*/
|
|
8546
|
+
this.getLauncherTemplate = async (symbol, walkerName) => {
|
|
8547
|
+
this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
|
|
8548
|
+
symbol,
|
|
8549
|
+
walkerName,
|
|
8550
|
+
});
|
|
8551
|
+
// Escape special characters to prevent code injection
|
|
8552
|
+
const escapedSymbol = String(symbol)
|
|
8553
|
+
.replace(/\\/g, '\\\\')
|
|
8554
|
+
.replace(/"/g, '\\"');
|
|
8555
|
+
const escapedWalkerName = String(walkerName)
|
|
8556
|
+
.replace(/\\/g, '\\\\')
|
|
8557
|
+
.replace(/"/g, '\\"');
|
|
8558
|
+
return [
|
|
8559
|
+
`Walker.background("${escapedSymbol}", {`,
|
|
8560
|
+
` walkerName: "${escapedWalkerName}"`,
|
|
8561
|
+
`});`,
|
|
8562
|
+
``,
|
|
8563
|
+
`listenSignalBacktest((event) => {`,
|
|
8564
|
+
` console.log(event);`,
|
|
8565
|
+
`});`,
|
|
8566
|
+
``,
|
|
8567
|
+
`listenBacktestProgress((event) => {`,
|
|
8568
|
+
` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
|
|
8569
|
+
` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
|
|
8570
|
+
`});`,
|
|
8571
|
+
``,
|
|
8572
|
+
`listenWalkerProgress((event) => {`,
|
|
8573
|
+
` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
|
|
8574
|
+
` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
|
|
8575
|
+
` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
|
|
8576
|
+
`});`,
|
|
8577
|
+
``,
|
|
8578
|
+
`listenWalkerComplete((results) => {`,
|
|
8579
|
+
` console.log("Walker completed:", results.bestStrategy);`,
|
|
8580
|
+
` Walker.dump("${escapedSymbol}", results.walkerName);`,
|
|
8581
|
+
`});`,
|
|
8582
|
+
``,
|
|
8583
|
+
`listenDoneBacktest((event) => {`,
|
|
8584
|
+
` console.log("Backtest completed:", event.symbol);`,
|
|
8585
|
+
` Backtest.dump(event.strategyName);`,
|
|
8586
|
+
`});`,
|
|
8587
|
+
``,
|
|
8588
|
+
`listenError((error) => {`,
|
|
8589
|
+
` console.error("Error occurred:", error);`,
|
|
8590
|
+
`});`
|
|
8591
|
+
].join("\n");
|
|
8592
|
+
};
|
|
8593
|
+
/**
|
|
8594
|
+
* Generates dumpJson() helper function for debug output.
|
|
8595
|
+
* Saves LLM conversations and results to ./dump/strategy/{resultId}/
|
|
8596
|
+
*
|
|
8597
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8598
|
+
* @returns Generated async dumpJson() function
|
|
8599
|
+
*/
|
|
8600
|
+
this.getJsonDumpTemplate = async (symbol) => {
|
|
8601
|
+
this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
|
|
8602
|
+
symbol,
|
|
8603
|
+
});
|
|
8604
|
+
return [
|
|
8605
|
+
`async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
|
|
8606
|
+
` // Extract system messages and system reminders from existing data`,
|
|
8607
|
+
` const systemMessages = history.filter((m) => m.role === "system");`,
|
|
8608
|
+
` const userMessages = history.filter((m) => m.role === "user");`,
|
|
8609
|
+
` const subfolderPath = path.join(outputDir, resultId);`,
|
|
8610
|
+
``,
|
|
8611
|
+
` try {`,
|
|
8612
|
+
` await fs.access(subfolderPath);`,
|
|
8613
|
+
` return;`,
|
|
8614
|
+
` } catch {`,
|
|
8615
|
+
` await fs.mkdir(subfolderPath, { recursive: true });`,
|
|
8616
|
+
` }`,
|
|
8617
|
+
``,
|
|
8618
|
+
` {`,
|
|
8619
|
+
` let summary = "# Outline Result Summary\\n";`,
|
|
8620
|
+
``,
|
|
8621
|
+
` {`,
|
|
8622
|
+
` summary += "\\n";`,
|
|
8623
|
+
` summary += \`**ResultId**: \${resultId}\\n\`;`,
|
|
8624
|
+
` summary += "\\n";`,
|
|
8625
|
+
` }`,
|
|
8626
|
+
``,
|
|
8627
|
+
` if (result) {`,
|
|
8628
|
+
` summary += "## Output Data\\n\\n";`,
|
|
8629
|
+
` summary += "\`\`\`json\\n";`,
|
|
8630
|
+
` summary += JSON.stringify(result, null, 2);`,
|
|
8631
|
+
` summary += "\\n\`\`\`\\n\\n";`,
|
|
8632
|
+
` }`,
|
|
8633
|
+
``,
|
|
8634
|
+
` // Add system messages to summary`,
|
|
8635
|
+
` if (systemMessages.length > 0) {`,
|
|
8636
|
+
` summary += "## System Messages\\n\\n";`,
|
|
8637
|
+
` systemMessages.forEach((msg, idx) => {`,
|
|
8638
|
+
` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
|
|
8639
|
+
` summary += msg.content;`,
|
|
8640
|
+
` summary += "\\n";`,
|
|
8641
|
+
` });`,
|
|
8642
|
+
` }`,
|
|
8643
|
+
``,
|
|
8644
|
+
` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
|
|
8645
|
+
` await fs.writeFile(summaryFile, summary, "utf8");`,
|
|
8646
|
+
` }`,
|
|
8647
|
+
``,
|
|
8648
|
+
` {`,
|
|
8649
|
+
` await Promise.all(`,
|
|
8650
|
+
` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
|
|
8651
|
+
` const messageNum = String(idx + 1).padStart(2, "0");`,
|
|
8652
|
+
` const contentFileName = \`\${messageNum}_user_message.md\`;`,
|
|
8653
|
+
` const contentFilePath = path.join(subfolderPath, contentFileName);`,
|
|
8654
|
+
``,
|
|
8655
|
+
` {`,
|
|
8656
|
+
` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
|
|
8657
|
+
` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
|
|
8658
|
+
` if (messageSizeKb > WARN_KB) {`,
|
|
8659
|
+
` console.warn(`,
|
|
8660
|
+
` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
|
|
8661
|
+
` );`,
|
|
8662
|
+
` }`,
|
|
8663
|
+
` }`,
|
|
8664
|
+
``,
|
|
8665
|
+
` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
|
|
8666
|
+
` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
8667
|
+
` content += message.content;`,
|
|
8668
|
+
` content += "\\n";`,
|
|
8669
|
+
``,
|
|
8670
|
+
` await fs.writeFile(contentFilePath, content, "utf8");`,
|
|
8671
|
+
` })`,
|
|
8672
|
+
` );`,
|
|
8673
|
+
` }`,
|
|
8674
|
+
``,
|
|
8675
|
+
` {`,
|
|
8676
|
+
` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
|
|
8677
|
+
` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
|
|
8678
|
+
` const contentFilePath = path.join(subfolderPath, contentFileName);`,
|
|
8679
|
+
``,
|
|
8680
|
+
` let content = "# Full Outline Result\\n\\n";`,
|
|
8681
|
+
` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
8682
|
+
``,
|
|
8683
|
+
` if (result) {`,
|
|
8684
|
+
` content += "## Output Data\\n\\n";`,
|
|
8685
|
+
` content += "\`\`\`json\\n";`,
|
|
8686
|
+
` content += JSON.stringify(result, null, 2);`,
|
|
8687
|
+
` content += "\\n\`\`\`\\n";`,
|
|
8688
|
+
` }`,
|
|
8689
|
+
``,
|
|
8690
|
+
` await fs.writeFile(contentFilePath, content, "utf8");`,
|
|
8691
|
+
` }`,
|
|
8692
|
+
`}`
|
|
8693
|
+
].join("\n");
|
|
8694
|
+
};
|
|
8695
|
+
/**
|
|
8696
|
+
* Generates text() helper for LLM text generation.
|
|
8697
|
+
* Uses Ollama gpt-oss:20b model for market analysis.
|
|
8698
|
+
*
|
|
8699
|
+
* @param symbol - Trading pair symbol (used in prompt)
|
|
8700
|
+
* @returns Generated async text() function
|
|
8701
|
+
*/
|
|
8702
|
+
this.getTextTemplate = async (symbol) => {
|
|
8703
|
+
this.loggerService.log("optimizerTemplateService getTextTemplate", {
|
|
8704
|
+
symbol,
|
|
8705
|
+
});
|
|
8706
|
+
// Escape special characters in symbol to prevent code injection
|
|
8707
|
+
const escapedSymbol = String(symbol)
|
|
8708
|
+
.replace(/\\/g, '\\\\')
|
|
8709
|
+
.replace(/`/g, '\\`')
|
|
8710
|
+
.replace(/\$/g, '\\$')
|
|
8711
|
+
.toUpperCase();
|
|
8712
|
+
return [
|
|
8713
|
+
`async function text(messages) {`,
|
|
8714
|
+
` const ollama = new Ollama({`,
|
|
8715
|
+
` host: "https://ollama.com",`,
|
|
8716
|
+
` headers: {`,
|
|
8717
|
+
` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
|
|
8718
|
+
` },`,
|
|
8719
|
+
` });`,
|
|
8720
|
+
``,
|
|
8721
|
+
` const response = await ollama.chat({`,
|
|
8722
|
+
` model: "gpt-oss:20b",`,
|
|
8723
|
+
` messages: [`,
|
|
8724
|
+
` {`,
|
|
8725
|
+
` role: "system",`,
|
|
8726
|
+
` content: [`,
|
|
8727
|
+
` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
|
|
8728
|
+
` "только отчёт готовый для копипасты целиком",`,
|
|
8729
|
+
` "",`,
|
|
8730
|
+
` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
|
|
8731
|
+
` ].join("\\n"),`,
|
|
8732
|
+
` },`,
|
|
8733
|
+
` ...messages,`,
|
|
8734
|
+
` {`,
|
|
8735
|
+
` role: "user",`,
|
|
8736
|
+
` content: [`,
|
|
8737
|
+
` "На каких условиях мне купить ${escapedSymbol}?",`,
|
|
8738
|
+
` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
|
|
8739
|
+
` "Какой RR ставить для позиций?",`,
|
|
8740
|
+
` "Предпочтительны LONG или SHORT позиции?",`,
|
|
8741
|
+
` "",`,
|
|
8742
|
+
` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
|
|
8743
|
+
` ].join("\\n")`,
|
|
8744
|
+
` }`,
|
|
8745
|
+
` ]`,
|
|
8746
|
+
` });`,
|
|
8747
|
+
``,
|
|
8748
|
+
` const content = response.message.content.trim();`,
|
|
8749
|
+
` return content`,
|
|
8750
|
+
` .replace(/\\\\/g, '\\\\\\\\')`,
|
|
8751
|
+
` .replace(/\`/g, '\\\\\`')`,
|
|
8752
|
+
` .replace(/\\$/g, '\\\\$')`,
|
|
8753
|
+
` .replace(/"/g, '\\\\"')`,
|
|
8754
|
+
` .replace(/'/g, "\\\\'");`,
|
|
8755
|
+
`}`
|
|
8756
|
+
].join("\n");
|
|
8757
|
+
};
|
|
8758
|
+
/**
|
|
8759
|
+
* Generates json() helper for structured LLM output.
|
|
8760
|
+
* Uses Ollama with JSON schema for trading signals.
|
|
8761
|
+
*
|
|
8762
|
+
* Signal schema:
|
|
8763
|
+
* - position: "wait" | "long" | "short"
|
|
8764
|
+
* - note: strategy explanation
|
|
8765
|
+
* - priceOpen: entry price
|
|
8766
|
+
* - priceTakeProfit: target price
|
|
8767
|
+
* - priceStopLoss: stop price
|
|
8768
|
+
* - minuteEstimatedTime: expected duration (max 360 min)
|
|
8769
|
+
*
|
|
8770
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8771
|
+
* @returns Generated async json() function with signal schema
|
|
8772
|
+
*/
|
|
8773
|
+
this.getJsonTemplate = async (symbol) => {
|
|
8774
|
+
this.loggerService.log("optimizerTemplateService getJsonTemplate", {
|
|
8775
|
+
symbol,
|
|
8776
|
+
});
|
|
8777
|
+
return [
|
|
8778
|
+
`async function json(messages) {`,
|
|
8779
|
+
` const ollama = new Ollama({`,
|
|
8780
|
+
` host: "https://ollama.com",`,
|
|
8781
|
+
` headers: {`,
|
|
8782
|
+
` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
|
|
8783
|
+
` },`,
|
|
8784
|
+
` });`,
|
|
8785
|
+
``,
|
|
8786
|
+
` const response = await ollama.chat({`,
|
|
8787
|
+
` model: "gpt-oss:20b",`,
|
|
8788
|
+
` messages: [`,
|
|
8789
|
+
` {`,
|
|
8790
|
+
` role: "system",`,
|
|
8791
|
+
` content: [`,
|
|
8792
|
+
` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
|
|
8793
|
+
` "",`,
|
|
8794
|
+
` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
|
|
8795
|
+
` "",`,
|
|
8796
|
+
` "1. ТИПЫ ПОЗИЦИЙ:",`,
|
|
8797
|
+
` " - position='wait': нет четкого сигнала, жди лучших условий",`,
|
|
8798
|
+
` " - position='long': бычий сигнал, цена будет расти",`,
|
|
8799
|
+
` " - position='short': медвежий сигнал, цена будет падать",`,
|
|
8800
|
+
` "",`,
|
|
8801
|
+
` "2. ЦЕНА ВХОДА (priceOpen):",`,
|
|
8802
|
+
` " - Может быть текущей рыночной ценой для немедленного входа",`,
|
|
8803
|
+
` " - Может быть отложенной ценой для входа при достижении уровня",`,
|
|
8804
|
+
` " - Укажи оптимальную цену входа согласно технического анализа",`,
|
|
8805
|
+
` "",`,
|
|
8806
|
+
` "3. УРОВНИ ВЫХОДА:",`,
|
|
8807
|
+
` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
|
|
8808
|
+
` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
|
|
8809
|
+
` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
|
|
8810
|
+
` "",`,
|
|
8811
|
+
` "4. ВРЕМЕННЫЕ РАМКИ:",`,
|
|
8812
|
+
` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
|
|
8813
|
+
` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
|
|
8814
|
+
` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
|
|
8815
|
+
` ].join("\\n"),`,
|
|
8816
|
+
` },`,
|
|
8817
|
+
` ...messages,`,
|
|
8818
|
+
` ],`,
|
|
8819
|
+
` format: {`,
|
|
8820
|
+
` type: "object",`,
|
|
8821
|
+
` properties: {`,
|
|
8822
|
+
` position: {`,
|
|
8823
|
+
` type: "string",`,
|
|
8824
|
+
` enum: ["wait", "long", "short"],`,
|
|
8825
|
+
` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
|
|
8826
|
+
` },`,
|
|
8827
|
+
` note: {`,
|
|
8828
|
+
` type: "string",`,
|
|
8829
|
+
` description: "Professional trading recommendation with price levels",`,
|
|
8830
|
+
` },`,
|
|
8831
|
+
` priceOpen: {`,
|
|
8832
|
+
` type: "number",`,
|
|
8833
|
+
` description: "Entry price (current market price or limit order price)",`,
|
|
8834
|
+
` },`,
|
|
8835
|
+
` priceTakeProfit: {`,
|
|
8836
|
+
` type: "number",`,
|
|
8837
|
+
` description: "Take profit target price",`,
|
|
8838
|
+
` },`,
|
|
8839
|
+
` priceStopLoss: {`,
|
|
8840
|
+
` type: "number",`,
|
|
8841
|
+
` description: "Stop loss exit price",`,
|
|
8842
|
+
` },`,
|
|
8843
|
+
` minuteEstimatedTime: {`,
|
|
8844
|
+
` type: "number",`,
|
|
8845
|
+
` description: "Expected time to reach TP in minutes (max 360)",`,
|
|
8846
|
+
` },`,
|
|
8847
|
+
` },`,
|
|
8848
|
+
` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
|
|
8849
|
+
` },`,
|
|
8850
|
+
` });`,
|
|
8851
|
+
``,
|
|
8852
|
+
` const jsonResponse = JSON.parse(response.message.content.trim());`,
|
|
8853
|
+
` return jsonResponse;`,
|
|
8854
|
+
`}`
|
|
8855
|
+
].join("\n");
|
|
8856
|
+
};
|
|
8857
|
+
}
|
|
8858
|
+
}
|
|
8859
|
+
|
|
8860
|
+
/**
|
|
8861
|
+
* Service for managing optimizer schema registration and retrieval.
|
|
8862
|
+
* Provides validation and registry management for optimizer configurations.
|
|
8863
|
+
*
|
|
8864
|
+
* Uses ToolRegistry for immutable schema storage.
|
|
8865
|
+
*/
|
|
8866
|
+
class OptimizerSchemaService {
|
|
8867
|
+
constructor() {
|
|
8868
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8869
|
+
this._registry = new ToolRegistry("optimizerSchema");
|
|
8870
|
+
/**
|
|
8871
|
+
* Registers a new optimizer schema.
|
|
8872
|
+
* Validates required fields before registration.
|
|
8873
|
+
*
|
|
8874
|
+
* @param key - Unique optimizer name
|
|
8875
|
+
* @param value - Optimizer schema configuration
|
|
8876
|
+
* @throws Error if schema validation fails
|
|
8877
|
+
*/
|
|
8878
|
+
this.register = (key, value) => {
|
|
8879
|
+
this.loggerService.log(`optimizerSchemaService register`, { key });
|
|
8880
|
+
this.validateShallow(value);
|
|
8881
|
+
this._registry = this._registry.register(key, value);
|
|
8882
|
+
};
|
|
8883
|
+
/**
|
|
8884
|
+
* Validates optimizer schema structure.
|
|
8885
|
+
* Checks required fields: optimizerName, rangeTrain, source, getPrompt.
|
|
8886
|
+
*
|
|
8887
|
+
* @param optimizerSchema - Schema to validate
|
|
8888
|
+
* @throws Error if validation fails
|
|
8889
|
+
*/
|
|
8890
|
+
this.validateShallow = (optimizerSchema) => {
|
|
8891
|
+
this.loggerService.log(`optimizerTemplateService validateShallow`, {
|
|
8892
|
+
optimizerSchema,
|
|
8893
|
+
});
|
|
8894
|
+
if (typeof optimizerSchema.optimizerName !== "string") {
|
|
8895
|
+
throw new Error(`optimizer template validation failed: missing optimizerName`);
|
|
8896
|
+
}
|
|
8897
|
+
if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
|
|
8898
|
+
throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8899
|
+
}
|
|
8900
|
+
if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
|
|
8901
|
+
throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8902
|
+
}
|
|
8903
|
+
if (typeof optimizerSchema.getPrompt !== "function") {
|
|
8904
|
+
throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8905
|
+
}
|
|
8906
|
+
};
|
|
8907
|
+
/**
|
|
8908
|
+
* Partially overrides an existing optimizer schema.
|
|
8909
|
+
* Merges provided values with existing schema.
|
|
8910
|
+
*
|
|
8911
|
+
* @param key - Optimizer name to override
|
|
8912
|
+
* @param value - Partial schema values to merge
|
|
8913
|
+
* @returns Updated complete schema
|
|
8914
|
+
* @throws Error if optimizer not found
|
|
8915
|
+
*/
|
|
8916
|
+
this.override = (key, value) => {
|
|
8917
|
+
this.loggerService.log(`optimizerSchemaService override`, { key });
|
|
8918
|
+
this._registry = this._registry.override(key, value);
|
|
8919
|
+
return this._registry.get(key);
|
|
8920
|
+
};
|
|
8921
|
+
/**
|
|
8922
|
+
* Retrieves optimizer schema by name.
|
|
8923
|
+
*
|
|
8924
|
+
* @param key - Optimizer name
|
|
8925
|
+
* @returns Complete optimizer schema
|
|
8926
|
+
* @throws Error if optimizer not found
|
|
8927
|
+
*/
|
|
8928
|
+
this.get = (key) => {
|
|
8929
|
+
this.loggerService.log(`optimizerSchemaService get`, { key });
|
|
8930
|
+
return this._registry.get(key);
|
|
8931
|
+
};
|
|
8932
|
+
}
|
|
8933
|
+
}
|
|
8934
|
+
|
|
8935
|
+
/**
|
|
8936
|
+
* Service for validating optimizer existence and managing optimizer registry.
|
|
8937
|
+
* Maintains a Map of registered optimizers for validation purposes.
|
|
8938
|
+
*
|
|
8939
|
+
* Uses memoization for efficient repeated validation checks.
|
|
8940
|
+
*/
|
|
8941
|
+
class OptimizerValidationService {
|
|
8942
|
+
constructor() {
|
|
8943
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8944
|
+
this._optimizerMap = new Map();
|
|
8945
|
+
/**
|
|
8946
|
+
* Adds optimizer to validation registry.
|
|
8947
|
+
* Prevents duplicate optimizer names.
|
|
8948
|
+
*
|
|
8949
|
+
* @param optimizerName - Unique optimizer identifier
|
|
8950
|
+
* @param optimizerSchema - Complete optimizer schema
|
|
8951
|
+
* @throws Error if optimizer with same name already exists
|
|
8952
|
+
*/
|
|
8953
|
+
this.addOptimizer = (optimizerName, optimizerSchema) => {
|
|
8954
|
+
this.loggerService.log("optimizerValidationService addOptimizer", {
|
|
8955
|
+
optimizerName,
|
|
8956
|
+
optimizerSchema,
|
|
8957
|
+
});
|
|
8958
|
+
if (this._optimizerMap.has(optimizerName)) {
|
|
8959
|
+
throw new Error(`optimizer ${optimizerName} already exist`);
|
|
8960
|
+
}
|
|
8961
|
+
this._optimizerMap.set(optimizerName, optimizerSchema);
|
|
8962
|
+
};
|
|
8963
|
+
/**
|
|
8964
|
+
* Validates that optimizer exists in registry.
|
|
8965
|
+
* Memoized for performance on repeated checks.
|
|
8966
|
+
*
|
|
8967
|
+
* @param optimizerName - Optimizer name to validate
|
|
8968
|
+
* @param source - Source method name for error messages
|
|
8969
|
+
* @throws Error if optimizer not found
|
|
8970
|
+
*/
|
|
8971
|
+
this.validate = memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
|
|
8972
|
+
this.loggerService.log("optimizerValidationService validate", {
|
|
8973
|
+
optimizerName,
|
|
8974
|
+
source,
|
|
8975
|
+
});
|
|
8976
|
+
const optimizer = this._optimizerMap.get(optimizerName);
|
|
8977
|
+
if (!optimizer) {
|
|
8978
|
+
throw new Error(`optimizer ${optimizerName} not found source=${source}`);
|
|
8979
|
+
}
|
|
8980
|
+
return true;
|
|
8981
|
+
});
|
|
8982
|
+
/**
|
|
8983
|
+
* Lists all registered optimizer schemas.
|
|
8984
|
+
*
|
|
8985
|
+
* @returns Array of all optimizer schemas
|
|
8986
|
+
*/
|
|
8987
|
+
this.list = async () => {
|
|
8988
|
+
this.loggerService.log("optimizerValidationService list");
|
|
8989
|
+
return Array.from(this._optimizerMap.values());
|
|
8990
|
+
};
|
|
8991
|
+
}
|
|
8992
|
+
}
|
|
8993
|
+
|
|
8994
|
+
const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
|
|
8995
|
+
const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
|
|
8996
|
+
const METHOD_NAME_DUMP = "optimizerGlobalService dump";
|
|
8997
|
+
/**
|
|
8998
|
+
* Global service for optimizer operations with validation.
|
|
8999
|
+
* Entry point for public API, performs validation before delegating to ConnectionService.
|
|
9000
|
+
*
|
|
9001
|
+
* Workflow:
|
|
9002
|
+
* 1. Log operation
|
|
9003
|
+
* 2. Validate optimizer exists
|
|
9004
|
+
* 3. Delegate to OptimizerConnectionService
|
|
9005
|
+
*/
|
|
9006
|
+
class OptimizerGlobalService {
|
|
9007
|
+
constructor() {
|
|
9008
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9009
|
+
this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
|
|
9010
|
+
this.optimizerValidationService = inject(TYPES.optimizerValidationService);
|
|
9011
|
+
/**
|
|
9012
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9013
|
+
* Validates optimizer existence before execution.
|
|
9014
|
+
*
|
|
9015
|
+
* @param symbol - Trading pair symbol
|
|
9016
|
+
* @param optimizerName - Optimizer identifier
|
|
9017
|
+
* @returns Array of generated strategies with conversation context
|
|
9018
|
+
* @throws Error if optimizer not found
|
|
9019
|
+
*/
|
|
9020
|
+
this.getData = async (symbol, optimizerName) => {
|
|
9021
|
+
this.loggerService.log(METHOD_NAME_GET_DATA, {
|
|
9022
|
+
symbol,
|
|
9023
|
+
optimizerName,
|
|
9024
|
+
});
|
|
9025
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
|
|
9026
|
+
return await this.optimizerConnectionService.getData(symbol, optimizerName);
|
|
9027
|
+
};
|
|
9028
|
+
/**
|
|
9029
|
+
* Generates complete executable strategy code.
|
|
9030
|
+
* Validates optimizer existence before execution.
|
|
9031
|
+
*
|
|
9032
|
+
* @param symbol - Trading pair symbol
|
|
9033
|
+
* @param optimizerName - Optimizer identifier
|
|
9034
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9035
|
+
* @throws Error if optimizer not found
|
|
9036
|
+
*/
|
|
9037
|
+
this.getCode = async (symbol, optimizerName) => {
|
|
9038
|
+
this.loggerService.log(METHOD_NAME_GET_CODE, {
|
|
9039
|
+
symbol,
|
|
9040
|
+
optimizerName,
|
|
9041
|
+
});
|
|
9042
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
|
|
9043
|
+
return await this.optimizerConnectionService.getCode(symbol, optimizerName);
|
|
9044
|
+
};
|
|
9045
|
+
/**
|
|
9046
|
+
* Generates and saves strategy code to file.
|
|
9047
|
+
* Validates optimizer existence before execution.
|
|
9048
|
+
*
|
|
9049
|
+
* @param symbol - Trading pair symbol
|
|
9050
|
+
* @param optimizerName - Optimizer identifier
|
|
9051
|
+
* @param path - Output directory path (optional)
|
|
9052
|
+
* @throws Error if optimizer not found
|
|
9053
|
+
*/
|
|
9054
|
+
this.dump = async (symbol, optimizerName, path) => {
|
|
9055
|
+
this.loggerService.log(METHOD_NAME_DUMP, {
|
|
9056
|
+
symbol,
|
|
9057
|
+
optimizerName,
|
|
9058
|
+
path,
|
|
9059
|
+
});
|
|
9060
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
|
|
9061
|
+
return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
|
|
9062
|
+
};
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
|
|
9066
|
+
const ITERATION_LIMIT = 25;
|
|
9067
|
+
const DEFAULT_SOURCE_NAME = "unknown";
|
|
9068
|
+
const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
|
|
9069
|
+
/**
|
|
9070
|
+
* Default user message formatter.
|
|
9071
|
+
* Delegates to template's getUserMessage method.
|
|
9072
|
+
*
|
|
9073
|
+
* @param symbol - Trading pair symbol
|
|
9074
|
+
* @param data - Fetched data array
|
|
9075
|
+
* @param name - Source name
|
|
9076
|
+
* @param self - ClientOptimizer instance
|
|
9077
|
+
* @returns Formatted user message content
|
|
9078
|
+
*/
|
|
9079
|
+
const DEFAULT_USER_FN = async (symbol, data, name, self) => {
|
|
9080
|
+
return await self.params.template.getUserMessage(symbol, data, name);
|
|
9081
|
+
};
|
|
9082
|
+
/**
|
|
9083
|
+
* Default assistant message formatter.
|
|
9084
|
+
* Delegates to template's getAssistantMessage method.
|
|
9085
|
+
*
|
|
9086
|
+
* @param symbol - Trading pair symbol
|
|
9087
|
+
* @param data - Fetched data array
|
|
9088
|
+
* @param name - Source name
|
|
9089
|
+
* @param self - ClientOptimizer instance
|
|
9090
|
+
* @returns Formatted assistant message content
|
|
9091
|
+
*/
|
|
9092
|
+
const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
|
|
9093
|
+
return await self.params.template.getAssistantMessage(symbol, data, name);
|
|
9094
|
+
};
|
|
9095
|
+
/**
|
|
9096
|
+
* Resolves paginated data from source with deduplication.
|
|
9097
|
+
* Uses iterateDocuments to handle pagination automatically.
|
|
9098
|
+
*
|
|
9099
|
+
* @param fetch - Source fetch function
|
|
9100
|
+
* @param filterData - Filter arguments (symbol, dates)
|
|
9101
|
+
* @returns Deduplicated array of all fetched data
|
|
9102
|
+
*/
|
|
9103
|
+
const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
|
|
9104
|
+
const iterator = iterateDocuments({
|
|
9105
|
+
limit: ITERATION_LIMIT,
|
|
9106
|
+
async createRequest({ limit, offset }) {
|
|
9107
|
+
return await fetch({
|
|
9108
|
+
symbol: filterData.symbol,
|
|
9109
|
+
startDate: filterData.startDate,
|
|
9110
|
+
endDate: filterData.endDate,
|
|
9111
|
+
limit,
|
|
9112
|
+
offset,
|
|
9113
|
+
});
|
|
9114
|
+
},
|
|
9115
|
+
});
|
|
9116
|
+
const distinct = distinctDocuments(iterator, (data) => data.id);
|
|
9117
|
+
return await resolveDocuments(distinct);
|
|
9118
|
+
};
|
|
9119
|
+
/**
|
|
9120
|
+
* Collects data from all sources and generates strategy metadata.
|
|
9121
|
+
* Iterates through training ranges, fetches data from each source,
|
|
9122
|
+
* builds LLM conversation history, and generates strategy prompts.
|
|
9123
|
+
*
|
|
9124
|
+
* @param symbol - Trading pair symbol
|
|
9125
|
+
* @param self - ClientOptimizer instance
|
|
9126
|
+
* @returns Array of generated strategies with conversation context
|
|
9127
|
+
*/
|
|
9128
|
+
const GET_STRATEGY_DATA_FN = async (symbol, self) => {
|
|
9129
|
+
const strategyList = [];
|
|
9130
|
+
for (const { startDate, endDate } of self.params.rangeTrain) {
|
|
9131
|
+
const messageList = [];
|
|
9132
|
+
for (const source of self.params.source) {
|
|
9133
|
+
if (typeof source === "function") {
|
|
9134
|
+
const data = await RESOLVE_PAGINATION_FN(source, {
|
|
9135
|
+
symbol,
|
|
9136
|
+
startDate,
|
|
9137
|
+
endDate,
|
|
9138
|
+
});
|
|
9139
|
+
if (self.params.callbacks?.onSourceData) {
|
|
9140
|
+
await self.params.callbacks.onSourceData(symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
|
|
9141
|
+
}
|
|
9142
|
+
const [userContent, assistantContent] = await Promise.all([
|
|
9143
|
+
DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
|
|
9144
|
+
DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
|
|
9145
|
+
]);
|
|
9146
|
+
messageList.push({
|
|
9147
|
+
role: "user",
|
|
9148
|
+
content: userContent,
|
|
9149
|
+
}, {
|
|
9150
|
+
role: "assistant",
|
|
9151
|
+
content: assistantContent,
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
else {
|
|
9155
|
+
const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
|
|
9156
|
+
const data = await RESOLVE_PAGINATION_FN(fetch, {
|
|
9157
|
+
symbol,
|
|
9158
|
+
startDate,
|
|
9159
|
+
endDate,
|
|
9160
|
+
});
|
|
9161
|
+
if (self.params.callbacks?.onSourceData) {
|
|
9162
|
+
await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
|
|
9163
|
+
}
|
|
9164
|
+
const [userContent, assistantContent] = await Promise.all([
|
|
9165
|
+
user(symbol, data, name, self),
|
|
9166
|
+
assistant(symbol, data, name, self),
|
|
9167
|
+
]);
|
|
9168
|
+
messageList.push({
|
|
9169
|
+
role: "user",
|
|
9170
|
+
content: userContent,
|
|
9171
|
+
}, {
|
|
9172
|
+
role: "assistant",
|
|
9173
|
+
content: assistantContent,
|
|
9174
|
+
});
|
|
9175
|
+
}
|
|
9176
|
+
const name = "name" in source
|
|
9177
|
+
? source.name || DEFAULT_SOURCE_NAME
|
|
9178
|
+
: DEFAULT_SOURCE_NAME;
|
|
9179
|
+
strategyList.push({
|
|
9180
|
+
symbol,
|
|
9181
|
+
name,
|
|
9182
|
+
messages: messageList,
|
|
9183
|
+
strategy: await self.params.getPrompt(symbol, messageList),
|
|
9184
|
+
});
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
if (self.params.callbacks?.onData) {
|
|
9188
|
+
await self.params.callbacks.onData(symbol, strategyList);
|
|
9189
|
+
}
|
|
9190
|
+
return strategyList;
|
|
9191
|
+
};
|
|
9192
|
+
/**
|
|
9193
|
+
* Generates complete executable strategy code.
|
|
9194
|
+
* Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
|
|
9195
|
+
*
|
|
9196
|
+
* @param symbol - Trading pair symbol
|
|
9197
|
+
* @param self - ClientOptimizer instance
|
|
9198
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9199
|
+
*/
|
|
9200
|
+
const GET_STRATEGY_CODE_FN = async (symbol, self) => {
|
|
9201
|
+
const strategyData = await self.getData(symbol);
|
|
9202
|
+
const prefix = CREATE_PREFIX_FN();
|
|
9203
|
+
const sections = [];
|
|
9204
|
+
const exchangeName = `${prefix}_exchange`;
|
|
9205
|
+
// 1. Top banner with imports
|
|
9206
|
+
{
|
|
9207
|
+
sections.push(await self.params.template.getTopBanner(symbol));
|
|
9208
|
+
sections.push("");
|
|
9209
|
+
}
|
|
9210
|
+
// 2. JSON dump helper function
|
|
9211
|
+
{
|
|
9212
|
+
sections.push(await self.params.template.getJsonDumpTemplate(symbol));
|
|
9213
|
+
sections.push("");
|
|
9214
|
+
}
|
|
9215
|
+
// 3. Helper functions (text and json)
|
|
9216
|
+
{
|
|
9217
|
+
sections.push(await self.params.template.getTextTemplate(symbol));
|
|
9218
|
+
sections.push("");
|
|
9219
|
+
}
|
|
9220
|
+
{
|
|
9221
|
+
sections.push(await self.params.template.getJsonTemplate(symbol));
|
|
9222
|
+
sections.push("");
|
|
9223
|
+
}
|
|
9224
|
+
// 4. Exchange template (assuming first strategy has exchange info)
|
|
9225
|
+
{
|
|
9226
|
+
sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
|
|
9227
|
+
sections.push("");
|
|
9228
|
+
}
|
|
9229
|
+
// 5. Train frame templates
|
|
9230
|
+
{
|
|
9231
|
+
for (let i = 0; i < self.params.rangeTrain.length; i++) {
|
|
9232
|
+
const range = self.params.rangeTrain[i];
|
|
9233
|
+
const frameName = `${prefix}_train_frame-${i + 1}`;
|
|
9234
|
+
sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
|
|
9235
|
+
range.startDate, range.endDate));
|
|
9236
|
+
sections.push("");
|
|
9237
|
+
}
|
|
9238
|
+
}
|
|
9239
|
+
// 6. Test frame template
|
|
9240
|
+
{
|
|
9241
|
+
const testFrameName = `${prefix}_test_frame`;
|
|
9242
|
+
sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
|
|
9243
|
+
self.params.rangeTest.startDate, self.params.rangeTest.endDate));
|
|
9244
|
+
sections.push("");
|
|
9245
|
+
}
|
|
9246
|
+
// 7. Strategy templates for each generated strategy
|
|
9247
|
+
{
|
|
9248
|
+
for (let i = 0; i < strategyData.length; i++) {
|
|
9249
|
+
const strategy = strategyData[i];
|
|
9250
|
+
const strategyName = `${prefix}_strategy-${i + 1}`;
|
|
9251
|
+
const interval = "5m"; // default interval
|
|
9252
|
+
sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
|
|
9253
|
+
sections.push("");
|
|
9254
|
+
}
|
|
9255
|
+
}
|
|
9256
|
+
// 8. Walker template (uses test frame for validation)
|
|
9257
|
+
{
|
|
9258
|
+
const walkerName = `${prefix}_walker`;
|
|
9259
|
+
const testFrameName = `${prefix}_test_frame`;
|
|
9260
|
+
const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
|
|
9261
|
+
sections.push(await self.params.template.getWalkerTemplate(walkerName, `${prefix}_${exchangeName}`, testFrameName, strategies));
|
|
9262
|
+
sections.push("");
|
|
9263
|
+
}
|
|
9264
|
+
// 9. Launcher template
|
|
9265
|
+
{
|
|
9266
|
+
const walkerName = `${prefix}_walker`;
|
|
9267
|
+
sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
|
|
9268
|
+
sections.push("");
|
|
9269
|
+
}
|
|
9270
|
+
const code = sections.join("\n");
|
|
9271
|
+
if (self.params.callbacks?.onCode) {
|
|
9272
|
+
await self.params.callbacks.onCode(symbol, code);
|
|
9273
|
+
}
|
|
9274
|
+
return code;
|
|
9275
|
+
};
|
|
9276
|
+
/**
|
|
9277
|
+
* Saves generated strategy code to file.
|
|
9278
|
+
* Creates directory if needed, writes .mjs file with generated code.
|
|
9279
|
+
*
|
|
9280
|
+
* @param symbol - Trading pair symbol
|
|
9281
|
+
* @param path - Output directory path
|
|
9282
|
+
* @param self - ClientOptimizer instance
|
|
9283
|
+
*/
|
|
9284
|
+
const GET_STRATEGY_DUMP_FN = async (symbol, path, self) => {
|
|
9285
|
+
const report = await self.getCode(symbol);
|
|
9286
|
+
try {
|
|
9287
|
+
const dir = join(process.cwd(), path);
|
|
9288
|
+
await mkdir(dir, { recursive: true });
|
|
9289
|
+
const filename = `${self.params.optimizerName}_${symbol}.mjs`;
|
|
9290
|
+
const filepath = join(dir, filename);
|
|
9291
|
+
await writeFile(filepath, report, "utf-8");
|
|
9292
|
+
self.params.logger.info(`Optimizer report saved: ${filepath}`);
|
|
9293
|
+
if (self.params.callbacks?.onDump) {
|
|
9294
|
+
await self.params.callbacks.onDump(symbol, filepath);
|
|
9295
|
+
}
|
|
9296
|
+
}
|
|
9297
|
+
catch (error) {
|
|
9298
|
+
self.params.logger.warn(`Failed to save optimizer report:`, error);
|
|
9299
|
+
throw error;
|
|
9300
|
+
}
|
|
9301
|
+
};
|
|
9302
|
+
/**
|
|
9303
|
+
* Client implementation for optimizer operations.
|
|
9304
|
+
*
|
|
9305
|
+
* Features:
|
|
9306
|
+
* - Data collection from multiple sources with pagination
|
|
9307
|
+
* - LLM conversation history building
|
|
9308
|
+
* - Strategy code generation with templates
|
|
9309
|
+
* - File export with callbacks
|
|
9310
|
+
*
|
|
9311
|
+
* Used by OptimizerConnectionService to create optimizer instances.
|
|
9312
|
+
*/
|
|
9313
|
+
class ClientOptimizer {
|
|
9314
|
+
constructor(params) {
|
|
9315
|
+
this.params = params;
|
|
9316
|
+
/**
|
|
9317
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9318
|
+
* Processes each training range and builds LLM conversation history.
|
|
9319
|
+
*
|
|
9320
|
+
* @param symbol - Trading pair symbol
|
|
9321
|
+
* @returns Array of generated strategies with conversation context
|
|
9322
|
+
*/
|
|
9323
|
+
this.getData = async (symbol) => {
|
|
9324
|
+
this.params.logger.debug("ClientOptimizer getData", {
|
|
9325
|
+
symbol,
|
|
9326
|
+
});
|
|
9327
|
+
return await GET_STRATEGY_DATA_FN(symbol, this);
|
|
9328
|
+
};
|
|
9329
|
+
/**
|
|
9330
|
+
* Generates complete executable strategy code.
|
|
9331
|
+
* Includes imports, helpers, strategies, walker, and launcher.
|
|
9332
|
+
*
|
|
9333
|
+
* @param symbol - Trading pair symbol
|
|
9334
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9335
|
+
*/
|
|
9336
|
+
this.getCode = async (symbol) => {
|
|
9337
|
+
this.params.logger.debug("ClientOptimizer getCode", {
|
|
9338
|
+
symbol,
|
|
9339
|
+
});
|
|
9340
|
+
return await GET_STRATEGY_CODE_FN(symbol, this);
|
|
9341
|
+
};
|
|
9342
|
+
/**
|
|
9343
|
+
* Generates and saves strategy code to file.
|
|
9344
|
+
* Creates directory if needed, writes .mjs file.
|
|
9345
|
+
*
|
|
9346
|
+
* @param symbol - Trading pair symbol
|
|
9347
|
+
* @param path - Output directory path (default: "./")
|
|
9348
|
+
*/
|
|
9349
|
+
this.dump = async (symbol, path = "./") => {
|
|
9350
|
+
this.params.logger.debug("ClientOptimizer dump", {
|
|
9351
|
+
symbol,
|
|
9352
|
+
path,
|
|
9353
|
+
});
|
|
9354
|
+
return await GET_STRATEGY_DUMP_FN(symbol, path, this);
|
|
9355
|
+
};
|
|
9356
|
+
}
|
|
9357
|
+
}
|
|
9358
|
+
|
|
9359
|
+
/**
|
|
9360
|
+
* Service for creating and caching optimizer client instances.
|
|
9361
|
+
* Handles dependency injection and template merging.
|
|
9362
|
+
*
|
|
9363
|
+
* Features:
|
|
9364
|
+
* - Memoized optimizer instances (one per optimizerName)
|
|
9365
|
+
* - Template merging (custom + defaults)
|
|
9366
|
+
* - Logger injection
|
|
9367
|
+
* - Delegates to ClientOptimizer for actual operations
|
|
9368
|
+
*/
|
|
9369
|
+
class OptimizerConnectionService {
|
|
9370
|
+
constructor() {
|
|
9371
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9372
|
+
this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
|
|
9373
|
+
this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
|
|
9374
|
+
/**
|
|
9375
|
+
* Creates or retrieves cached optimizer instance.
|
|
9376
|
+
* Memoized by optimizerName for performance.
|
|
9377
|
+
*
|
|
9378
|
+
* Merges custom templates from schema with defaults from OptimizerTemplateService.
|
|
9379
|
+
*
|
|
9380
|
+
* @param optimizerName - Unique optimizer identifier
|
|
9381
|
+
* @returns ClientOptimizer instance with resolved dependencies
|
|
9382
|
+
*/
|
|
9383
|
+
this.getOptimizer = memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
|
|
9384
|
+
const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks } = this.optimizerSchemaService.get(optimizerName);
|
|
9385
|
+
const { getAssistantMessage = this.optimizerTemplateService.getAssistantMessage, getExchangeTemplate = this.optimizerTemplateService.getExchangeTemplate, getFrameTemplate = this.optimizerTemplateService.getFrameTemplate, getJsonDumpTemplate = this.optimizerTemplateService.getJsonDumpTemplate, getJsonTemplate = this.optimizerTemplateService.getJsonTemplate, getLauncherTemplate = this.optimizerTemplateService.getLauncherTemplate, getStrategyTemplate = this.optimizerTemplateService.getStrategyTemplate, getTextTemplate = this.optimizerTemplateService.getTextTemplate, getWalkerTemplate = this.optimizerTemplateService.getWalkerTemplate, getTopBanner = this.optimizerTemplateService.getTopBanner, getUserMessage = this.optimizerTemplateService.getUserMessage, } = rawTemplate;
|
|
9386
|
+
const template = {
|
|
9387
|
+
getAssistantMessage,
|
|
9388
|
+
getExchangeTemplate,
|
|
9389
|
+
getFrameTemplate,
|
|
9390
|
+
getJsonDumpTemplate,
|
|
9391
|
+
getJsonTemplate,
|
|
9392
|
+
getLauncherTemplate,
|
|
9393
|
+
getStrategyTemplate,
|
|
9394
|
+
getTextTemplate,
|
|
9395
|
+
getWalkerTemplate,
|
|
9396
|
+
getTopBanner,
|
|
9397
|
+
getUserMessage,
|
|
9398
|
+
};
|
|
9399
|
+
return new ClientOptimizer({
|
|
9400
|
+
optimizerName,
|
|
9401
|
+
logger: this.loggerService,
|
|
9402
|
+
getPrompt,
|
|
9403
|
+
rangeTest,
|
|
9404
|
+
rangeTrain,
|
|
9405
|
+
source,
|
|
9406
|
+
template,
|
|
9407
|
+
callbacks,
|
|
9408
|
+
});
|
|
9409
|
+
});
|
|
9410
|
+
/**
|
|
9411
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9412
|
+
*
|
|
9413
|
+
* @param symbol - Trading pair symbol
|
|
9414
|
+
* @param optimizerName - Optimizer identifier
|
|
9415
|
+
* @returns Array of generated strategies with conversation context
|
|
9416
|
+
*/
|
|
9417
|
+
this.getData = async (symbol, optimizerName) => {
|
|
9418
|
+
this.loggerService.log("optimizerConnectionService getData", {
|
|
9419
|
+
symbol,
|
|
9420
|
+
optimizerName,
|
|
9421
|
+
});
|
|
9422
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9423
|
+
return await optimizer.getData(symbol);
|
|
9424
|
+
};
|
|
9425
|
+
/**
|
|
9426
|
+
* Generates complete executable strategy code.
|
|
9427
|
+
*
|
|
9428
|
+
* @param symbol - Trading pair symbol
|
|
9429
|
+
* @param optimizerName - Optimizer identifier
|
|
9430
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9431
|
+
*/
|
|
9432
|
+
this.getCode = async (symbol, optimizerName) => {
|
|
9433
|
+
this.loggerService.log("optimizerConnectionService getCode", {
|
|
9434
|
+
symbol,
|
|
9435
|
+
optimizerName,
|
|
9436
|
+
});
|
|
9437
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9438
|
+
return await optimizer.getCode(symbol);
|
|
9439
|
+
};
|
|
9440
|
+
/**
|
|
9441
|
+
* Generates and saves strategy code to file.
|
|
9442
|
+
*
|
|
9443
|
+
* @param symbol - Trading pair symbol
|
|
9444
|
+
* @param optimizerName - Optimizer identifier
|
|
9445
|
+
* @param path - Output directory path (optional)
|
|
9446
|
+
*/
|
|
9447
|
+
this.dump = async (symbol, optimizerName, path) => {
|
|
9448
|
+
this.loggerService.log("optimizerConnectionService getCode", {
|
|
9449
|
+
symbol,
|
|
9450
|
+
optimizerName,
|
|
9451
|
+
});
|
|
9452
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9453
|
+
return await optimizer.dump(symbol, path);
|
|
9454
|
+
};
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
|
|
9458
|
+
/**
|
|
9459
|
+
* Symbol marker indicating that partial state needs initialization.
|
|
9460
|
+
* Used as sentinel value for _states before waitForInit() is called.
|
|
9461
|
+
*/
|
|
9462
|
+
const NEED_FETCH = Symbol("need_fetch");
|
|
9463
|
+
/**
|
|
9464
|
+
* Array of profit level milestones to track (10%, 20%, ..., 100%).
|
|
9465
|
+
* Each level is checked during profit() method to emit events for newly reached levels.
|
|
9466
|
+
*/
|
|
9467
|
+
const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
9468
|
+
/**
|
|
9469
|
+
* Array of loss level milestones to track (-10%, -20%, ..., -100%).
|
|
9470
|
+
* Each level is checked during loss() method to emit events for newly reached levels.
|
|
9471
|
+
*/
|
|
9472
|
+
const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
9473
|
+
/**
|
|
9474
|
+
* Internal profit handler function for ClientPartial.
|
|
9475
|
+
*
|
|
9476
|
+
* Checks which profit levels have been reached and emits events for new levels only.
|
|
9477
|
+
* Uses Set-based deduplication to prevent duplicate events.
|
|
9478
|
+
*
|
|
9479
|
+
* @param symbol - Trading pair symbol
|
|
9480
|
+
* @param data - Signal row data
|
|
9481
|
+
* @param currentPrice - Current market price
|
|
9482
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9483
|
+
* @param backtest - True if backtest mode
|
|
9484
|
+
* @param when - Event timestamp
|
|
9485
|
+
* @param self - ClientPartial instance reference
|
|
9486
|
+
*/
|
|
9487
|
+
const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
|
|
9488
|
+
if (self._states === NEED_FETCH) {
|
|
9489
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9490
|
+
}
|
|
9491
|
+
let state = self._states.get(data.id);
|
|
9492
|
+
if (!state) {
|
|
9493
|
+
state = {
|
|
9494
|
+
profitLevels: new Set(),
|
|
9495
|
+
lossLevels: new Set(),
|
|
9496
|
+
};
|
|
9497
|
+
self._states.set(data.id, state);
|
|
9498
|
+
}
|
|
9499
|
+
let shouldPersist = false;
|
|
9500
|
+
for (const level of PROFIT_LEVELS) {
|
|
9501
|
+
if (revenuePercent >= level && !state.profitLevels.has(level)) {
|
|
9502
|
+
state.profitLevels.add(level);
|
|
9503
|
+
shouldPersist = true;
|
|
9504
|
+
self.params.logger.debug("ClientPartial profit level reached", {
|
|
9505
|
+
symbol,
|
|
9506
|
+
signalId: data.id,
|
|
9507
|
+
level,
|
|
9508
|
+
revenuePercent,
|
|
9509
|
+
backtest,
|
|
9510
|
+
});
|
|
9511
|
+
await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
if (shouldPersist) {
|
|
9515
|
+
await self._persistState(symbol);
|
|
9516
|
+
}
|
|
9517
|
+
};
|
|
9518
|
+
/**
|
|
9519
|
+
* Internal loss handler function for ClientPartial.
|
|
9520
|
+
*
|
|
9521
|
+
* Checks which loss levels have been reached and emits events for new levels only.
|
|
9522
|
+
* Uses Set-based deduplication to prevent duplicate events.
|
|
9523
|
+
* Converts negative lossPercent to absolute value for level comparison.
|
|
9524
|
+
*
|
|
9525
|
+
* @param symbol - Trading pair symbol
|
|
9526
|
+
* @param data - Signal row data
|
|
9527
|
+
* @param currentPrice - Current market price
|
|
9528
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9529
|
+
* @param backtest - True if backtest mode
|
|
9530
|
+
* @param when - Event timestamp
|
|
9531
|
+
* @param self - ClientPartial instance reference
|
|
9532
|
+
*/
|
|
9533
|
+
const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
|
|
9534
|
+
if (self._states === NEED_FETCH) {
|
|
9535
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9536
|
+
}
|
|
9537
|
+
let state = self._states.get(data.id);
|
|
9538
|
+
if (!state) {
|
|
9539
|
+
state = {
|
|
9540
|
+
profitLevels: new Set(),
|
|
9541
|
+
lossLevels: new Set(),
|
|
9542
|
+
};
|
|
9543
|
+
self._states.set(data.id, state);
|
|
9544
|
+
}
|
|
9545
|
+
const absLoss = Math.abs(lossPercent);
|
|
9546
|
+
let shouldPersist = false;
|
|
9547
|
+
for (const level of LOSS_LEVELS) {
|
|
9548
|
+
if (absLoss >= level && !state.lossLevels.has(level)) {
|
|
9549
|
+
state.lossLevels.add(level);
|
|
9550
|
+
shouldPersist = true;
|
|
9551
|
+
self.params.logger.debug("ClientPartial loss level reached", {
|
|
9552
|
+
symbol,
|
|
9553
|
+
signalId: data.id,
|
|
9554
|
+
level,
|
|
9555
|
+
lossPercent,
|
|
9556
|
+
backtest,
|
|
9557
|
+
});
|
|
9558
|
+
await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
if (shouldPersist) {
|
|
9562
|
+
await self._persistState(symbol);
|
|
9563
|
+
}
|
|
9564
|
+
};
|
|
9565
|
+
/**
|
|
9566
|
+
* Internal initialization function for ClientPartial.
|
|
9567
|
+
*
|
|
9568
|
+
* Loads persisted partial state from disk and restores in-memory Maps.
|
|
9569
|
+
* Converts serialized arrays back to Sets for O(1) lookups.
|
|
9570
|
+
*
|
|
9571
|
+
* @param symbol - Trading pair symbol
|
|
9572
|
+
* @param self - ClientPartial instance reference
|
|
9573
|
+
*/
|
|
9574
|
+
const WAIT_FOR_INIT_FN = async (symbol, self) => {
|
|
9575
|
+
self.params.logger.debug("ClientPartial waitForInit", { symbol });
|
|
9576
|
+
if (self._states === NEED_FETCH) {
|
|
9577
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9578
|
+
}
|
|
9579
|
+
const partialData = await PersistPartialAdapter.readPartialData(symbol);
|
|
9580
|
+
for (const [signalId, data] of Object.entries(partialData)) {
|
|
9581
|
+
const state = {
|
|
9582
|
+
profitLevels: new Set(data.profitLevels),
|
|
9583
|
+
lossLevels: new Set(data.lossLevels),
|
|
9584
|
+
};
|
|
9585
|
+
self._states.set(signalId, state);
|
|
9586
|
+
}
|
|
9587
|
+
self.params.logger.info("ClientPartial restored state", {
|
|
9588
|
+
symbol,
|
|
9589
|
+
signalCount: Object.keys(partialData).length,
|
|
9590
|
+
});
|
|
9591
|
+
};
|
|
9592
|
+
/**
|
|
9593
|
+
* Client implementation for partial profit/loss level tracking.
|
|
9594
|
+
*
|
|
9595
|
+
* Features:
|
|
9596
|
+
* - Tracks profit and loss level milestones (10%, 20%, 30%, etc) per signal
|
|
9597
|
+
* - Deduplicates events using Set-based state per signal ID
|
|
9598
|
+
* - Persists state to disk for crash recovery in live mode
|
|
9599
|
+
* - Emits events via onProfit/onLoss callbacks for each newly reached level
|
|
9600
|
+
*
|
|
9601
|
+
* Architecture:
|
|
9602
|
+
* - Created per signal ID by PartialConnectionService (memoized)
|
|
9603
|
+
* - State stored in Map<signalId, IPartialState> with Set<PartialLevel>
|
|
9604
|
+
* - Persistence handled by PersistPartialAdapter (atomic file writes)
|
|
9605
|
+
*
|
|
9606
|
+
* Lifecycle:
|
|
9607
|
+
* 1. Construction: Initialize empty Map
|
|
9608
|
+
* 2. waitForInit(): Load persisted state from disk
|
|
9609
|
+
* 3. profit()/loss(): Check levels, emit events, persist changes
|
|
9610
|
+
* 4. clear(): Remove signal state, persist, clean up memoized instance
|
|
9611
|
+
*
|
|
9612
|
+
* @example
|
|
9613
|
+
* ```typescript
|
|
9614
|
+
* import { ClientPartial } from "./client/ClientPartial";
|
|
9615
|
+
*
|
|
9616
|
+
* const partial = new ClientPartial({
|
|
9617
|
+
* logger: loggerService,
|
|
9618
|
+
* onProfit: async (symbol, data, price, level, backtest, timestamp) => {
|
|
9619
|
+
* console.log(`Signal ${data.id} reached ${level}% profit at ${price}`);
|
|
9620
|
+
* // Emit to partialProfitSubject
|
|
9621
|
+
* },
|
|
9622
|
+
* onLoss: async (symbol, data, price, level, backtest, timestamp) => {
|
|
9623
|
+
* console.log(`Signal ${data.id} reached -${level}% loss at ${price}`);
|
|
9624
|
+
* // Emit to partialLossSubject
|
|
9625
|
+
* }
|
|
9626
|
+
* });
|
|
9627
|
+
*
|
|
9628
|
+
* // Initialize from persisted state
|
|
9629
|
+
* await partial.waitForInit("BTCUSDT");
|
|
9630
|
+
*
|
|
9631
|
+
* // During signal monitoring in ClientStrategy
|
|
9632
|
+
* const signal = { id: "abc123", priceOpen: 50000, position: "long", ... };
|
|
9633
|
+
*
|
|
9634
|
+
* // Price rises to $55000 (10% profit)
|
|
9635
|
+
* await partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
9636
|
+
* // Emits onProfit callback for 10% level
|
|
9637
|
+
*
|
|
9638
|
+
* // Price rises to $61000 (22% profit)
|
|
9639
|
+
* await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
|
|
9640
|
+
* // Emits onProfit for 20% level only (10% already emitted)
|
|
9641
|
+
*
|
|
9642
|
+
* // Signal closes
|
|
9643
|
+
* await partial.clear("BTCUSDT", signal, 61000);
|
|
9644
|
+
* // State removed, changes persisted
|
|
9645
|
+
* ```
|
|
9646
|
+
*/
|
|
9647
|
+
class ClientPartial {
|
|
9648
|
+
/**
|
|
9649
|
+
* Creates new ClientPartial instance.
|
|
9650
|
+
*
|
|
9651
|
+
* @param params - Partial parameters (logger, onProfit, onLoss callbacks)
|
|
9652
|
+
*/
|
|
9653
|
+
constructor(params) {
|
|
9654
|
+
this.params = params;
|
|
9655
|
+
/**
|
|
9656
|
+
* Map of signal IDs to partial profit/loss state.
|
|
9657
|
+
* Uses NEED_FETCH sentinel before initialization.
|
|
9658
|
+
*
|
|
9659
|
+
* Each state contains:
|
|
9660
|
+
* - profitLevels: Set of reached profit levels (10, 20, 30, etc)
|
|
9661
|
+
* - lossLevels: Set of reached loss levels (10, 20, 30, etc)
|
|
9662
|
+
*/
|
|
9663
|
+
this._states = NEED_FETCH;
|
|
9664
|
+
/**
|
|
9665
|
+
* Initializes partial state by loading from disk.
|
|
9666
|
+
*
|
|
9667
|
+
* Uses singleshot pattern to ensure initialization happens exactly once per symbol.
|
|
9668
|
+
* Reads persisted state from PersistPartialAdapter and restores to _states Map.
|
|
9669
|
+
*
|
|
9670
|
+
* Must be called before profit()/loss()/clear() methods.
|
|
9671
|
+
*
|
|
9672
|
+
* @param symbol - Trading pair symbol
|
|
9673
|
+
* @returns Promise that resolves when initialization is complete
|
|
9674
|
+
*
|
|
9675
|
+
* @example
|
|
9676
|
+
* ```typescript
|
|
9677
|
+
* const partial = new ClientPartial(params);
|
|
9678
|
+
* await partial.waitForInit("BTCUSDT"); // Load persisted state
|
|
9679
|
+
* // Now profit()/loss() can be called
|
|
9680
|
+
* ```
|
|
9681
|
+
*/
|
|
9682
|
+
this.waitForInit = singleshot(async (symbol) => await WAIT_FOR_INIT_FN(symbol, this));
|
|
9683
|
+
this._states = new Map();
|
|
9684
|
+
}
|
|
9685
|
+
/**
|
|
9686
|
+
* Persists current partial state to disk.
|
|
9687
|
+
*
|
|
9688
|
+
* Converts in-memory Maps and Sets to JSON-serializable format:
|
|
9689
|
+
* - Map<signalId, IPartialState> → Record<signalId, IPartialData>
|
|
9690
|
+
* - Set<PartialLevel> → PartialLevel[]
|
|
9691
|
+
*
|
|
9692
|
+
* Called automatically after profit/loss level changes or clear().
|
|
9693
|
+
* Uses atomic file writes via PersistPartialAdapter.
|
|
9694
|
+
*
|
|
9695
|
+
* @param symbol - Trading pair symbol
|
|
9696
|
+
* @returns Promise that resolves when persistence is complete
|
|
9697
|
+
*/
|
|
9698
|
+
async _persistState(symbol) {
|
|
9699
|
+
this.params.logger.debug("ClientPartial persistState", { symbol });
|
|
9700
|
+
if (this._states === NEED_FETCH) {
|
|
9701
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9702
|
+
}
|
|
9703
|
+
const partialData = {};
|
|
9704
|
+
for (const [signalId, state] of this._states.entries()) {
|
|
9705
|
+
partialData[signalId] = {
|
|
9706
|
+
profitLevels: Array.from(state.profitLevels),
|
|
9707
|
+
lossLevels: Array.from(state.lossLevels),
|
|
9708
|
+
};
|
|
9709
|
+
}
|
|
9710
|
+
await PersistPartialAdapter.writePartialData(partialData, symbol);
|
|
9711
|
+
}
|
|
9712
|
+
/**
|
|
9713
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
9714
|
+
*
|
|
9715
|
+
* Called by ClientStrategy during signal monitoring when revenuePercent > 0.
|
|
9716
|
+
* Iterates through PROFIT_LEVELS (10%, 20%, 30%, etc) and checks which
|
|
9717
|
+
* levels have been reached but not yet emitted (Set-based deduplication).
|
|
9718
|
+
*
|
|
9719
|
+
* For each new level:
|
|
9720
|
+
* 1. Adds level to state.profitLevels Set
|
|
9721
|
+
* 2. Logs debug message
|
|
9722
|
+
* 3. Calls params.onProfit callback (emits to partialProfitSubject)
|
|
9723
|
+
*
|
|
9724
|
+
* After all levels processed, persists state to disk if any new levels were found.
|
|
9725
|
+
*
|
|
9726
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9727
|
+
* @param data - Signal row data
|
|
9728
|
+
* @param currentPrice - Current market price
|
|
9729
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9730
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9731
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9732
|
+
* @returns Promise that resolves when profit processing is complete
|
|
9733
|
+
*
|
|
9734
|
+
* @example
|
|
9735
|
+
* ```typescript
|
|
9736
|
+
* // Signal at $50000, price rises to $61000 (22% profit)
|
|
9737
|
+
* await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
|
|
9738
|
+
* // Emits events for 10% and 20% levels (if not already emitted)
|
|
9739
|
+
* // State persisted to disk
|
|
9740
|
+
* ```
|
|
9741
|
+
*/
|
|
9742
|
+
async profit(symbol, data, currentPrice, revenuePercent, backtest, when) {
|
|
9743
|
+
this.params.logger.debug("ClientPartial profit", {
|
|
9744
|
+
symbol,
|
|
9745
|
+
signalId: data.id,
|
|
9746
|
+
currentPrice,
|
|
9747
|
+
revenuePercent,
|
|
9748
|
+
backtest,
|
|
9749
|
+
when,
|
|
9750
|
+
});
|
|
9751
|
+
return await HANDLE_PROFIT_FN(symbol, data, currentPrice, revenuePercent, backtest, when, this);
|
|
9752
|
+
}
|
|
9753
|
+
/**
|
|
9754
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
9755
|
+
*
|
|
9756
|
+
* Called by ClientStrategy during signal monitoring when revenuePercent < 0.
|
|
9757
|
+
* Converts negative lossPercent to absolute value and iterates through
|
|
9758
|
+
* LOSS_LEVELS (10%, 20%, 30%, etc) to check which levels have been reached
|
|
9759
|
+
* but not yet emitted (Set-based deduplication).
|
|
9760
|
+
*
|
|
9761
|
+
* For each new level:
|
|
9762
|
+
* 1. Adds level to state.lossLevels Set
|
|
9763
|
+
* 2. Logs debug message
|
|
9764
|
+
* 3. Calls params.onLoss callback (emits to partialLossSubject)
|
|
9765
|
+
*
|
|
9766
|
+
* After all levels processed, persists state to disk if any new levels were found.
|
|
9767
|
+
*
|
|
9768
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9769
|
+
* @param data - Signal row data
|
|
9770
|
+
* @param currentPrice - Current market price
|
|
9771
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9772
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9773
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9774
|
+
* @returns Promise that resolves when loss processing is complete
|
|
9775
|
+
*
|
|
9776
|
+
* @example
|
|
9777
|
+
* ```typescript
|
|
9778
|
+
* // Signal at $50000, price drops to $39000 (-22% loss)
|
|
9779
|
+
* await partial.loss("BTCUSDT", signal, 39000, -22.0, false, new Date());
|
|
9780
|
+
* // Emits events for 10% and 20% loss levels (if not already emitted)
|
|
9781
|
+
* // State persisted to disk
|
|
9782
|
+
* ```
|
|
9783
|
+
*/
|
|
9784
|
+
async loss(symbol, data, currentPrice, lossPercent, backtest, when) {
|
|
9785
|
+
this.params.logger.debug("ClientPartial loss", {
|
|
9786
|
+
symbol,
|
|
9787
|
+
signalId: data.id,
|
|
9788
|
+
currentPrice,
|
|
9789
|
+
lossPercent,
|
|
9790
|
+
backtest,
|
|
9791
|
+
when,
|
|
9792
|
+
});
|
|
9793
|
+
return await HANDLE_LOSS_FN(symbol, data, currentPrice, lossPercent, backtest, when, this);
|
|
9794
|
+
}
|
|
9795
|
+
/**
|
|
9796
|
+
* Clears partial profit/loss state for a signal when it closes.
|
|
9797
|
+
*
|
|
9798
|
+
* Called by ClientStrategy when signal completes (TP/SL/time_expired).
|
|
9799
|
+
* Removes signal's state from _states Map and persists changes to disk.
|
|
9800
|
+
*
|
|
9801
|
+
* After clear() completes:
|
|
9802
|
+
* - Signal state removed from memory (_states.delete)
|
|
9803
|
+
* - Changes persisted to disk (PersistPartialAdapter.writePartialData)
|
|
9804
|
+
* - Memoized ClientPartial instance cleared in PartialConnectionService
|
|
9805
|
+
*
|
|
9806
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9807
|
+
* @param data - Signal row data
|
|
9808
|
+
* @param priceClose - Final closing price
|
|
9809
|
+
* @returns Promise that resolves when clear is complete
|
|
9810
|
+
* @throws Error if ClientPartial not initialized (waitForInit not called)
|
|
9811
|
+
*
|
|
9812
|
+
* @example
|
|
9813
|
+
* ```typescript
|
|
9814
|
+
* // Signal closes at take profit
|
|
9815
|
+
* await partial.clear("BTCUSDT", signal, 52000);
|
|
9816
|
+
* // State removed: _states.delete(signal.id)
|
|
9817
|
+
* // Persisted: ./dump/data/partial/BTCUSDT/levels.json updated
|
|
9818
|
+
* // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
|
|
9819
|
+
* ```
|
|
9820
|
+
*/
|
|
9821
|
+
async clear(symbol, data, priceClose) {
|
|
9822
|
+
this.params.logger.log("ClientPartial clear", {
|
|
9823
|
+
symbol,
|
|
9824
|
+
data,
|
|
9825
|
+
priceClose,
|
|
9826
|
+
});
|
|
9827
|
+
if (this._states === NEED_FETCH) {
|
|
9828
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9829
|
+
}
|
|
9830
|
+
this._states.delete(data.id);
|
|
9831
|
+
await this._persistState(symbol);
|
|
9832
|
+
}
|
|
9833
|
+
}
|
|
9834
|
+
|
|
9835
|
+
/**
|
|
9836
|
+
* Callback function for emitting profit events to partialProfitSubject.
|
|
9837
|
+
*
|
|
9838
|
+
* Called by ClientPartial when a new profit level is reached.
|
|
9839
|
+
* Emits PartialProfitContract event to all subscribers.
|
|
9840
|
+
*
|
|
9841
|
+
* @param symbol - Trading pair symbol
|
|
9842
|
+
* @param data - Signal row data
|
|
9843
|
+
* @param currentPrice - Current market price
|
|
9844
|
+
* @param level - Profit level reached
|
|
9845
|
+
* @param backtest - True if backtest mode
|
|
9846
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
9847
|
+
*/
|
|
9848
|
+
const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
9849
|
+
symbol,
|
|
9850
|
+
data,
|
|
9851
|
+
currentPrice,
|
|
9852
|
+
level,
|
|
9853
|
+
backtest,
|
|
9854
|
+
timestamp,
|
|
9855
|
+
});
|
|
9856
|
+
/**
|
|
9857
|
+
* Callback function for emitting loss events to partialLossSubject.
|
|
9858
|
+
*
|
|
9859
|
+
* Called by ClientPartial when a new loss level is reached.
|
|
9860
|
+
* Emits PartialLossContract event to all subscribers.
|
|
9861
|
+
*
|
|
9862
|
+
* @param symbol - Trading pair symbol
|
|
9863
|
+
* @param data - Signal row data
|
|
9864
|
+
* @param currentPrice - Current market price
|
|
9865
|
+
* @param level - Loss level reached
|
|
9866
|
+
* @param backtest - True if backtest mode
|
|
9867
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
9868
|
+
*/
|
|
9869
|
+
const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
9870
|
+
symbol,
|
|
9871
|
+
data,
|
|
9872
|
+
currentPrice,
|
|
9873
|
+
level,
|
|
9874
|
+
backtest,
|
|
9875
|
+
timestamp,
|
|
9876
|
+
});
|
|
9877
|
+
/**
|
|
9878
|
+
* Connection service for partial profit/loss tracking.
|
|
9879
|
+
*
|
|
9880
|
+
* Provides memoized ClientPartial instances per signal ID.
|
|
9881
|
+
* Acts as factory and lifetime manager for ClientPartial objects.
|
|
9882
|
+
*
|
|
9883
|
+
* Features:
|
|
9884
|
+
* - Creates one ClientPartial instance per signal ID (memoized)
|
|
9885
|
+
* - Configures instances with logger and event emitter callbacks
|
|
9886
|
+
* - Delegates profit/loss/clear operations to appropriate ClientPartial
|
|
9887
|
+
* - Cleans up memoized instances when signals are cleared
|
|
9888
|
+
*
|
|
9889
|
+
* Architecture:
|
|
9890
|
+
* - Injected into ClientStrategy via PartialGlobalService
|
|
9891
|
+
* - Uses memoize from functools-kit for instance caching
|
|
9892
|
+
* - Emits events to partialProfitSubject/partialLossSubject
|
|
9893
|
+
*
|
|
9894
|
+
* @example
|
|
9895
|
+
* ```typescript
|
|
9896
|
+
* // Service injected via DI
|
|
9897
|
+
* const service = inject<PartialConnectionService>(TYPES.partialConnectionService);
|
|
9898
|
+
*
|
|
9899
|
+
* // Called by ClientStrategy during signal monitoring
|
|
9900
|
+
* await service.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
9901
|
+
* // Creates or reuses ClientPartial for signal.id
|
|
9902
|
+
* // Delegates to ClientPartial.profit()
|
|
9903
|
+
*
|
|
9904
|
+
* // When signal closes
|
|
9905
|
+
* await service.clear("BTCUSDT", signal, 52000);
|
|
9906
|
+
* // Clears signal state and removes memoized instance
|
|
9907
|
+
* ```
|
|
9908
|
+
*/
|
|
9909
|
+
class PartialConnectionService {
|
|
9910
|
+
constructor() {
|
|
9911
|
+
/**
|
|
9912
|
+
* Logger service injected from DI container.
|
|
9913
|
+
*/
|
|
9914
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9915
|
+
/**
|
|
9916
|
+
* Memoized factory function for ClientPartial instances.
|
|
9917
|
+
*
|
|
9918
|
+
* Creates one ClientPartial per signal ID with configured callbacks.
|
|
9919
|
+
* Instances are cached until clear() is called.
|
|
9920
|
+
*
|
|
9921
|
+
* Key format: signalId
|
|
9922
|
+
* Value: ClientPartial instance with logger and event emitters
|
|
9923
|
+
*/
|
|
9924
|
+
this.getPartial = memoize(([signalId]) => `${signalId}`, () => {
|
|
9925
|
+
return new ClientPartial({
|
|
9926
|
+
logger: this.loggerService,
|
|
9927
|
+
onProfit: COMMIT_PROFIT_FN,
|
|
9928
|
+
onLoss: COMMIT_LOSS_FN,
|
|
9929
|
+
});
|
|
9930
|
+
});
|
|
9931
|
+
/**
|
|
9932
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
9933
|
+
*
|
|
9934
|
+
* Retrieves or creates ClientPartial for signal ID, initializes it if needed,
|
|
9935
|
+
* then delegates to ClientPartial.profit() method.
|
|
9936
|
+
*
|
|
9937
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9938
|
+
* @param data - Signal row data
|
|
9939
|
+
* @param currentPrice - Current market price
|
|
9940
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9941
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9942
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9943
|
+
* @returns Promise that resolves when profit processing is complete
|
|
9944
|
+
*/
|
|
9945
|
+
this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
|
|
9946
|
+
this.loggerService.log("partialConnectionService profit", {
|
|
9947
|
+
symbol,
|
|
9948
|
+
data,
|
|
9949
|
+
currentPrice,
|
|
9950
|
+
revenuePercent,
|
|
9951
|
+
backtest,
|
|
9952
|
+
when,
|
|
9953
|
+
});
|
|
9954
|
+
const partial = this.getPartial(data.id);
|
|
9955
|
+
await partial.waitForInit(symbol);
|
|
9956
|
+
return await partial.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
|
|
9957
|
+
};
|
|
9958
|
+
/**
|
|
9959
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
9960
|
+
*
|
|
9961
|
+
* Retrieves or creates ClientPartial for signal ID, initializes it if needed,
|
|
9962
|
+
* then delegates to ClientPartial.loss() method.
|
|
9963
|
+
*
|
|
9964
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9965
|
+
* @param data - Signal row data
|
|
9966
|
+
* @param currentPrice - Current market price
|
|
9967
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9968
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9969
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9970
|
+
* @returns Promise that resolves when loss processing is complete
|
|
9971
|
+
*/
|
|
9972
|
+
this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
|
|
9973
|
+
this.loggerService.log("partialConnectionService loss", {
|
|
9974
|
+
symbol,
|
|
9975
|
+
data,
|
|
9976
|
+
currentPrice,
|
|
9977
|
+
lossPercent,
|
|
9978
|
+
backtest,
|
|
9979
|
+
when,
|
|
9980
|
+
});
|
|
9981
|
+
const partial = this.getPartial(data.id);
|
|
9982
|
+
await partial.waitForInit(symbol);
|
|
9983
|
+
return await partial.loss(symbol, data, currentPrice, lossPercent, backtest, when);
|
|
9984
|
+
};
|
|
9985
|
+
/**
|
|
9986
|
+
* Clears partial profit/loss state when signal closes.
|
|
9987
|
+
*
|
|
9988
|
+
* Retrieves ClientPartial for signal ID, initializes if needed,
|
|
9989
|
+
* delegates clear operation, then removes memoized instance.
|
|
9990
|
+
*
|
|
9991
|
+
* Sequence:
|
|
9992
|
+
* 1. Get ClientPartial from memoize cache
|
|
9993
|
+
* 2. Ensure initialization (waitForInit)
|
|
9994
|
+
* 3. Call ClientPartial.clear() - removes state, persists to disk
|
|
9995
|
+
* 4. Clear memoized instance - prevents memory leaks
|
|
9996
|
+
*
|
|
9997
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9998
|
+
* @param data - Signal row data
|
|
9999
|
+
* @param priceClose - Final closing price
|
|
10000
|
+
* @returns Promise that resolves when clear is complete
|
|
10001
|
+
*/
|
|
10002
|
+
this.clear = async (symbol, data, priceClose) => {
|
|
10003
|
+
this.loggerService.log("partialConnectionService profit", {
|
|
10004
|
+
symbol,
|
|
10005
|
+
data,
|
|
10006
|
+
priceClose,
|
|
10007
|
+
});
|
|
10008
|
+
const partial = this.getPartial(data.id);
|
|
10009
|
+
await partial.waitForInit(symbol);
|
|
10010
|
+
await partial.clear(symbol, data, priceClose);
|
|
10011
|
+
this.getPartial.clear(data.id);
|
|
10012
|
+
};
|
|
10013
|
+
}
|
|
10014
|
+
}
|
|
10015
|
+
|
|
10016
|
+
const columns = [
|
|
10017
|
+
{
|
|
10018
|
+
key: "action",
|
|
10019
|
+
label: "Action",
|
|
10020
|
+
format: (data) => data.action.toUpperCase(),
|
|
10021
|
+
},
|
|
10022
|
+
{
|
|
10023
|
+
key: "symbol",
|
|
10024
|
+
label: "Symbol",
|
|
10025
|
+
format: (data) => data.symbol,
|
|
10026
|
+
},
|
|
10027
|
+
{
|
|
10028
|
+
key: "signalId",
|
|
10029
|
+
label: "Signal ID",
|
|
10030
|
+
format: (data) => data.signalId,
|
|
10031
|
+
},
|
|
10032
|
+
{
|
|
10033
|
+
key: "position",
|
|
10034
|
+
label: "Position",
|
|
10035
|
+
format: (data) => data.position.toUpperCase(),
|
|
10036
|
+
},
|
|
10037
|
+
{
|
|
10038
|
+
key: "level",
|
|
10039
|
+
label: "Level %",
|
|
10040
|
+
format: (data) => data.action === "profit" ? `+${data.level}%` : `-${data.level}%`,
|
|
10041
|
+
},
|
|
10042
|
+
{
|
|
10043
|
+
key: "currentPrice",
|
|
10044
|
+
label: "Current Price",
|
|
10045
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
10046
|
+
},
|
|
10047
|
+
{
|
|
10048
|
+
key: "timestamp",
|
|
10049
|
+
label: "Timestamp",
|
|
10050
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
10051
|
+
},
|
|
10052
|
+
{
|
|
10053
|
+
key: "mode",
|
|
10054
|
+
label: "Mode",
|
|
10055
|
+
format: (data) => (data.backtest ? "Backtest" : "Live"),
|
|
10056
|
+
},
|
|
10057
|
+
];
|
|
10058
|
+
/** Maximum number of events to store in partial reports */
|
|
10059
|
+
const MAX_EVENTS = 250;
|
|
10060
|
+
/**
|
|
10061
|
+
* Storage class for accumulating partial profit/loss events per symbol.
|
|
10062
|
+
* Maintains a chronological list of profit and loss level events.
|
|
10063
|
+
*/
|
|
10064
|
+
class ReportStorage {
|
|
10065
|
+
constructor() {
|
|
10066
|
+
/** Internal list of all partial events for this symbol */
|
|
10067
|
+
this._eventList = [];
|
|
10068
|
+
}
|
|
10069
|
+
/**
|
|
10070
|
+
* Adds a profit event to the storage.
|
|
10071
|
+
*
|
|
10072
|
+
* @param symbol - Trading pair symbol
|
|
10073
|
+
* @param data - Signal row data
|
|
10074
|
+
* @param currentPrice - Current market price
|
|
10075
|
+
* @param level - Profit level reached
|
|
10076
|
+
* @param backtest - True if backtest mode
|
|
10077
|
+
*/
|
|
10078
|
+
addProfitEvent(symbol, data, currentPrice, level, backtest, timestamp) {
|
|
10079
|
+
this._eventList.push({
|
|
10080
|
+
timestamp,
|
|
10081
|
+
action: "profit",
|
|
10082
|
+
symbol,
|
|
10083
|
+
signalId: data.id,
|
|
10084
|
+
position: data.position,
|
|
10085
|
+
currentPrice,
|
|
10086
|
+
level,
|
|
10087
|
+
backtest,
|
|
10088
|
+
});
|
|
10089
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
10090
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
10091
|
+
this._eventList.shift();
|
|
10092
|
+
}
|
|
10093
|
+
}
|
|
10094
|
+
/**
|
|
10095
|
+
* Adds a loss event to the storage.
|
|
10096
|
+
*
|
|
10097
|
+
* @param symbol - Trading pair symbol
|
|
10098
|
+
* @param data - Signal row data
|
|
10099
|
+
* @param currentPrice - Current market price
|
|
10100
|
+
* @param level - Loss level reached
|
|
10101
|
+
* @param backtest - True if backtest mode
|
|
10102
|
+
*/
|
|
10103
|
+
addLossEvent(symbol, data, currentPrice, level, backtest, timestamp) {
|
|
10104
|
+
this._eventList.push({
|
|
10105
|
+
timestamp,
|
|
10106
|
+
action: "loss",
|
|
10107
|
+
symbol,
|
|
10108
|
+
signalId: data.id,
|
|
10109
|
+
position: data.position,
|
|
10110
|
+
currentPrice,
|
|
10111
|
+
level,
|
|
10112
|
+
backtest,
|
|
10113
|
+
});
|
|
10114
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
10115
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
10116
|
+
this._eventList.shift();
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
10119
|
+
/**
|
|
10120
|
+
* Calculates statistical data from partial profit/loss events (Controller).
|
|
10121
|
+
*
|
|
10122
|
+
* @returns Statistical data (empty object if no events)
|
|
10123
|
+
*/
|
|
10124
|
+
async getData() {
|
|
10125
|
+
if (this._eventList.length === 0) {
|
|
10126
|
+
return {
|
|
10127
|
+
eventList: [],
|
|
10128
|
+
totalEvents: 0,
|
|
10129
|
+
totalProfit: 0,
|
|
10130
|
+
totalLoss: 0,
|
|
10131
|
+
};
|
|
10132
|
+
}
|
|
10133
|
+
const profitEvents = this._eventList.filter((e) => e.action === "profit");
|
|
10134
|
+
const lossEvents = this._eventList.filter((e) => e.action === "loss");
|
|
10135
|
+
return {
|
|
10136
|
+
eventList: this._eventList,
|
|
10137
|
+
totalEvents: this._eventList.length,
|
|
10138
|
+
totalProfit: profitEvents.length,
|
|
10139
|
+
totalLoss: lossEvents.length,
|
|
10140
|
+
};
|
|
10141
|
+
}
|
|
10142
|
+
/**
|
|
10143
|
+
* Generates markdown report with all partial events for a symbol (View).
|
|
10144
|
+
*
|
|
10145
|
+
* @param symbol - Trading pair symbol
|
|
10146
|
+
* @returns Markdown formatted report with all events
|
|
10147
|
+
*/
|
|
10148
|
+
async getReport(symbol) {
|
|
10149
|
+
const stats = await this.getData();
|
|
10150
|
+
if (stats.totalEvents === 0) {
|
|
10151
|
+
return str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", "No partial profit/loss events recorded yet.");
|
|
10152
|
+
}
|
|
10153
|
+
const header = columns.map((col) => col.label);
|
|
10154
|
+
const separator = columns.map(() => "---");
|
|
10155
|
+
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
10156
|
+
const tableData = [header, separator, ...rows];
|
|
10157
|
+
const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
10158
|
+
return str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
|
|
10159
|
+
}
|
|
10160
|
+
/**
|
|
10161
|
+
* Saves symbol report to disk.
|
|
10162
|
+
*
|
|
10163
|
+
* @param symbol - Trading pair symbol
|
|
10164
|
+
* @param path - Directory path to save report (default: "./dump/partial")
|
|
10165
|
+
*/
|
|
10166
|
+
async dump(symbol, path = "./dump/partial") {
|
|
10167
|
+
const markdown = await this.getReport(symbol);
|
|
10168
|
+
try {
|
|
10169
|
+
const dir = join(process.cwd(), path);
|
|
10170
|
+
await mkdir(dir, { recursive: true });
|
|
10171
|
+
const filename = `${symbol}.md`;
|
|
10172
|
+
const filepath = join(dir, filename);
|
|
10173
|
+
await writeFile(filepath, markdown, "utf-8");
|
|
10174
|
+
console.log(`Partial profit/loss report saved: ${filepath}`);
|
|
10175
|
+
}
|
|
10176
|
+
catch (error) {
|
|
10177
|
+
console.error(`Failed to save markdown report:`, error);
|
|
10178
|
+
}
|
|
10179
|
+
}
|
|
10180
|
+
}
|
|
10181
|
+
/**
|
|
10182
|
+
* Service for generating and saving partial profit/loss markdown reports.
|
|
10183
|
+
*
|
|
10184
|
+
* Features:
|
|
10185
|
+
* - Listens to partial profit and loss events via partialProfitSubject/partialLossSubject
|
|
10186
|
+
* - Accumulates all events (profit, loss) per symbol
|
|
10187
|
+
* - Generates markdown tables with detailed event information
|
|
10188
|
+
* - Provides statistics (total profit/loss events)
|
|
10189
|
+
* - Saves reports to disk in dump/partial/{symbol}.md
|
|
10190
|
+
*
|
|
10191
|
+
* @example
|
|
10192
|
+
* ```typescript
|
|
10193
|
+
* const service = new PartialMarkdownService();
|
|
10194
|
+
*
|
|
10195
|
+
* // Service automatically subscribes to subjects on init
|
|
10196
|
+
* // No manual callback setup needed
|
|
10197
|
+
*
|
|
10198
|
+
* // Later: generate and save report
|
|
10199
|
+
* await service.dump("BTCUSDT");
|
|
10200
|
+
* ```
|
|
10201
|
+
*/
|
|
10202
|
+
class PartialMarkdownService {
|
|
10203
|
+
constructor() {
|
|
10204
|
+
/** Logger service for debug output */
|
|
10205
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
10206
|
+
/**
|
|
10207
|
+
* Memoized function to get or create ReportStorage for a symbol.
|
|
10208
|
+
* Each symbol gets its own isolated storage instance.
|
|
10209
|
+
*/
|
|
10210
|
+
this.getStorage = memoize(([symbol]) => `${symbol}`, () => new ReportStorage());
|
|
10211
|
+
/**
|
|
10212
|
+
* Processes profit events and accumulates them.
|
|
10213
|
+
* Should be called from partialProfitSubject subscription.
|
|
10214
|
+
*
|
|
10215
|
+
* @param data - Profit event data
|
|
10216
|
+
*
|
|
10217
|
+
* @example
|
|
10218
|
+
* ```typescript
|
|
10219
|
+
* const service = new PartialMarkdownService();
|
|
10220
|
+
* // Service automatically subscribes in init()
|
|
10221
|
+
* ```
|
|
10222
|
+
*/
|
|
10223
|
+
this.tickProfit = async (data) => {
|
|
10224
|
+
this.loggerService.log("partialMarkdownService tickProfit", {
|
|
10225
|
+
data,
|
|
10226
|
+
});
|
|
10227
|
+
const storage = this.getStorage(data.symbol);
|
|
10228
|
+
storage.addProfitEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
|
|
10229
|
+
};
|
|
10230
|
+
/**
|
|
10231
|
+
* Processes loss events and accumulates them.
|
|
10232
|
+
* Should be called from partialLossSubject subscription.
|
|
10233
|
+
*
|
|
10234
|
+
* @param data - Loss event data
|
|
10235
|
+
*
|
|
10236
|
+
* @example
|
|
10237
|
+
* ```typescript
|
|
10238
|
+
* const service = new PartialMarkdownService();
|
|
10239
|
+
* // Service automatically subscribes in init()
|
|
10240
|
+
* ```
|
|
10241
|
+
*/
|
|
10242
|
+
this.tickLoss = async (data) => {
|
|
10243
|
+
this.loggerService.log("partialMarkdownService tickLoss", {
|
|
10244
|
+
data,
|
|
10245
|
+
});
|
|
10246
|
+
const storage = this.getStorage(data.symbol);
|
|
10247
|
+
storage.addLossEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
|
|
10248
|
+
};
|
|
10249
|
+
/**
|
|
10250
|
+
* Gets statistical data from all partial profit/loss events for a symbol.
|
|
10251
|
+
* Delegates to ReportStorage.getData().
|
|
10252
|
+
*
|
|
10253
|
+
* @param symbol - Trading pair symbol to get data for
|
|
10254
|
+
* @returns Statistical data object with all metrics
|
|
10255
|
+
*
|
|
10256
|
+
* @example
|
|
10257
|
+
* ```typescript
|
|
10258
|
+
* const service = new PartialMarkdownService();
|
|
10259
|
+
* const stats = await service.getData("BTCUSDT");
|
|
10260
|
+
* console.log(stats.totalProfit, stats.totalLoss);
|
|
10261
|
+
* ```
|
|
10262
|
+
*/
|
|
10263
|
+
this.getData = async (symbol) => {
|
|
10264
|
+
this.loggerService.log("partialMarkdownService getData", {
|
|
10265
|
+
symbol,
|
|
10266
|
+
});
|
|
10267
|
+
const storage = this.getStorage(symbol);
|
|
10268
|
+
return storage.getData();
|
|
10269
|
+
};
|
|
10270
|
+
/**
|
|
10271
|
+
* Generates markdown report with all partial events for a symbol.
|
|
10272
|
+
* Delegates to ReportStorage.getReport().
|
|
10273
|
+
*
|
|
10274
|
+
* @param symbol - Trading pair symbol to generate report for
|
|
10275
|
+
* @returns Markdown formatted report string with table of all events
|
|
10276
|
+
*
|
|
10277
|
+
* @example
|
|
10278
|
+
* ```typescript
|
|
10279
|
+
* const service = new PartialMarkdownService();
|
|
10280
|
+
* const markdown = await service.getReport("BTCUSDT");
|
|
10281
|
+
* console.log(markdown);
|
|
10282
|
+
* ```
|
|
10283
|
+
*/
|
|
10284
|
+
this.getReport = async (symbol) => {
|
|
10285
|
+
this.loggerService.log("partialMarkdownService getReport", {
|
|
10286
|
+
symbol,
|
|
10287
|
+
});
|
|
10288
|
+
const storage = this.getStorage(symbol);
|
|
10289
|
+
return storage.getReport(symbol);
|
|
10290
|
+
};
|
|
10291
|
+
/**
|
|
10292
|
+
* Saves symbol report to disk.
|
|
10293
|
+
* Creates directory if it doesn't exist.
|
|
10294
|
+
* Delegates to ReportStorage.dump().
|
|
10295
|
+
*
|
|
10296
|
+
* @param symbol - Trading pair symbol to save report for
|
|
10297
|
+
* @param path - Directory path to save report (default: "./dump/partial")
|
|
10298
|
+
*
|
|
10299
|
+
* @example
|
|
10300
|
+
* ```typescript
|
|
10301
|
+
* const service = new PartialMarkdownService();
|
|
10302
|
+
*
|
|
10303
|
+
* // Save to default path: ./dump/partial/BTCUSDT.md
|
|
10304
|
+
* await service.dump("BTCUSDT");
|
|
10305
|
+
*
|
|
10306
|
+
* // Save to custom path: ./custom/path/BTCUSDT.md
|
|
10307
|
+
* await service.dump("BTCUSDT", "./custom/path");
|
|
10308
|
+
* ```
|
|
10309
|
+
*/
|
|
10310
|
+
this.dump = async (symbol, path = "./dump/partial") => {
|
|
10311
|
+
this.loggerService.log("partialMarkdownService dump", {
|
|
10312
|
+
symbol,
|
|
10313
|
+
path,
|
|
10314
|
+
});
|
|
10315
|
+
const storage = this.getStorage(symbol);
|
|
10316
|
+
await storage.dump(symbol, path);
|
|
10317
|
+
};
|
|
10318
|
+
/**
|
|
10319
|
+
* Clears accumulated event data from storage.
|
|
10320
|
+
* If symbol is provided, clears only that symbol's data.
|
|
10321
|
+
* If symbol is omitted, clears all symbols' data.
|
|
10322
|
+
*
|
|
10323
|
+
* @param symbol - Optional symbol to clear specific symbol data
|
|
10324
|
+
*
|
|
10325
|
+
* @example
|
|
10326
|
+
* ```typescript
|
|
10327
|
+
* const service = new PartialMarkdownService();
|
|
10328
|
+
*
|
|
10329
|
+
* // Clear specific symbol data
|
|
10330
|
+
* await service.clear("BTCUSDT");
|
|
10331
|
+
*
|
|
10332
|
+
* // Clear all symbols' data
|
|
10333
|
+
* await service.clear();
|
|
10334
|
+
* ```
|
|
10335
|
+
*/
|
|
10336
|
+
this.clear = async (symbol) => {
|
|
10337
|
+
this.loggerService.log("partialMarkdownService clear", {
|
|
10338
|
+
symbol,
|
|
10339
|
+
});
|
|
10340
|
+
this.getStorage.clear(symbol);
|
|
10341
|
+
};
|
|
10342
|
+
/**
|
|
10343
|
+
* Initializes the service by subscribing to partial profit/loss events.
|
|
10344
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
10345
|
+
* Automatically called on first use.
|
|
10346
|
+
*
|
|
10347
|
+
* @example
|
|
10348
|
+
* ```typescript
|
|
10349
|
+
* const service = new PartialMarkdownService();
|
|
10350
|
+
* await service.init(); // Subscribe to profit/loss events
|
|
10351
|
+
* ```
|
|
10352
|
+
*/
|
|
10353
|
+
this.init = singleshot(async () => {
|
|
10354
|
+
this.loggerService.log("partialMarkdownService init");
|
|
10355
|
+
partialProfitSubject.subscribe(this.tickProfit);
|
|
10356
|
+
partialLossSubject.subscribe(this.tickLoss);
|
|
10357
|
+
});
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
|
|
10361
|
+
/**
|
|
10362
|
+
* Global service for partial profit/loss tracking.
|
|
10363
|
+
*
|
|
10364
|
+
* Thin delegation layer that forwards operations to PartialConnectionService.
|
|
10365
|
+
* Provides centralized logging for all partial operations at the global level.
|
|
10366
|
+
*
|
|
10367
|
+
* Architecture:
|
|
10368
|
+
* - Injected into ClientStrategy constructor via IStrategyParams
|
|
10369
|
+
* - Delegates all operations to PartialConnectionService
|
|
10370
|
+
* - Logs operations at "partialGlobalService" level before delegation
|
|
10371
|
+
*
|
|
10372
|
+
* Purpose:
|
|
10373
|
+
* - Single injection point for ClientStrategy (dependency injection pattern)
|
|
10374
|
+
* - Centralized logging for monitoring partial operations
|
|
10375
|
+
* - Layer of abstraction between strategy and connection layer
|
|
10376
|
+
*
|
|
10377
|
+
* @example
|
|
10378
|
+
* ```typescript
|
|
10379
|
+
* // Service injected into ClientStrategy via DI
|
|
10380
|
+
* const strategy = new ClientStrategy({
|
|
10381
|
+
* partial: partialGlobalService,
|
|
10382
|
+
* ...
|
|
10383
|
+
* });
|
|
10384
|
+
*
|
|
10385
|
+
* // Called during signal monitoring
|
|
10386
|
+
* await strategy.params.partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
10387
|
+
* // Logs at global level → delegates to PartialConnectionService
|
|
10388
|
+
* ```
|
|
10389
|
+
*/
|
|
10390
|
+
class PartialGlobalService {
|
|
10391
|
+
constructor() {
|
|
10392
|
+
/**
|
|
10393
|
+
* Logger service injected from DI container.
|
|
10394
|
+
* Used for logging operations at global service level.
|
|
10395
|
+
*/
|
|
10396
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
10397
|
+
/**
|
|
10398
|
+
* Connection service injected from DI container.
|
|
10399
|
+
* Handles actual ClientPartial instance creation and management.
|
|
10400
|
+
*/
|
|
10401
|
+
this.partialConnectionService = inject(TYPES.partialConnectionService);
|
|
10402
|
+
/**
|
|
10403
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
10404
|
+
*
|
|
10405
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10406
|
+
*
|
|
10407
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10408
|
+
* @param data - Signal row data
|
|
10409
|
+
* @param currentPrice - Current market price
|
|
10410
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
10411
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
10412
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
10413
|
+
* @returns Promise that resolves when profit processing is complete
|
|
10414
|
+
*/
|
|
10415
|
+
this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
|
|
10416
|
+
this.loggerService.log("partialGlobalService profit", {
|
|
10417
|
+
symbol,
|
|
10418
|
+
data,
|
|
10419
|
+
currentPrice,
|
|
10420
|
+
revenuePercent,
|
|
10421
|
+
backtest,
|
|
10422
|
+
when,
|
|
10423
|
+
});
|
|
10424
|
+
return await this.partialConnectionService.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
|
|
10425
|
+
};
|
|
10426
|
+
/**
|
|
10427
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
10428
|
+
*
|
|
10429
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10430
|
+
*
|
|
10431
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10432
|
+
* @param data - Signal row data
|
|
10433
|
+
* @param currentPrice - Current market price
|
|
10434
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
10435
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
10436
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
10437
|
+
* @returns Promise that resolves when loss processing is complete
|
|
10438
|
+
*/
|
|
10439
|
+
this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
|
|
10440
|
+
this.loggerService.log("partialGlobalService loss", {
|
|
10441
|
+
symbol,
|
|
10442
|
+
data,
|
|
10443
|
+
currentPrice,
|
|
10444
|
+
lossPercent,
|
|
10445
|
+
backtest,
|
|
10446
|
+
when,
|
|
10447
|
+
});
|
|
10448
|
+
return await this.partialConnectionService.loss(symbol, data, currentPrice, lossPercent, backtest, when);
|
|
10449
|
+
};
|
|
10450
|
+
/**
|
|
10451
|
+
* Clears partial profit/loss state when signal closes.
|
|
10452
|
+
*
|
|
10453
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10454
|
+
*
|
|
10455
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10456
|
+
* @param data - Signal row data
|
|
10457
|
+
* @param priceClose - Final closing price
|
|
10458
|
+
* @returns Promise that resolves when clear is complete
|
|
10459
|
+
*/
|
|
10460
|
+
this.clear = async (symbol, data, priceClose) => {
|
|
10461
|
+
this.loggerService.log("partialGlobalService profit", {
|
|
10462
|
+
symbol,
|
|
10463
|
+
data,
|
|
10464
|
+
priceClose,
|
|
10465
|
+
});
|
|
10466
|
+
return await this.partialConnectionService.clear(symbol, data, priceClose);
|
|
10467
|
+
};
|
|
10468
|
+
}
|
|
10469
|
+
}
|
|
10470
|
+
|
|
10471
|
+
{
|
|
10472
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
10473
|
+
}
|
|
10474
|
+
{
|
|
10475
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
10476
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
10477
|
+
}
|
|
10478
|
+
{
|
|
10479
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
10480
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
10481
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
10482
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
10483
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
10484
|
+
provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
|
|
10485
|
+
provide(TYPES.partialConnectionService, () => new PartialConnectionService());
|
|
10486
|
+
}
|
|
10487
|
+
{
|
|
10488
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
10489
|
+
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
10490
|
+
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
10491
|
+
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
|
|
10492
|
+
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
|
|
10493
|
+
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
|
|
10494
|
+
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
|
|
10495
|
+
}
|
|
10496
|
+
{
|
|
10497
|
+
provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
|
|
7876
10498
|
provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
|
|
7877
10499
|
provide(TYPES.frameGlobalService, () => new FrameGlobalService());
|
|
7878
10500
|
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
7879
10501
|
provide(TYPES.riskGlobalService, () => new RiskGlobalService());
|
|
10502
|
+
provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
|
|
10503
|
+
provide(TYPES.partialGlobalService, () => new PartialGlobalService());
|
|
7880
10504
|
}
|
|
7881
10505
|
{
|
|
7882
10506
|
provide(TYPES.liveCommandService, () => new LiveCommandService());
|
|
@@ -7900,6 +10524,7 @@ class RiskValidationService {
|
|
|
7900
10524
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
7901
10525
|
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
7902
10526
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
10527
|
+
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
7903
10528
|
}
|
|
7904
10529
|
{
|
|
7905
10530
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -7908,6 +10533,10 @@ class RiskValidationService {
|
|
|
7908
10533
|
provide(TYPES.walkerValidationService, () => new WalkerValidationService());
|
|
7909
10534
|
provide(TYPES.sizingValidationService, () => new SizingValidationService());
|
|
7910
10535
|
provide(TYPES.riskValidationService, () => new RiskValidationService());
|
|
10536
|
+
provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
|
|
10537
|
+
}
|
|
10538
|
+
{
|
|
10539
|
+
provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
|
|
7911
10540
|
}
|
|
7912
10541
|
|
|
7913
10542
|
const baseServices = {
|
|
@@ -7923,6 +10552,8 @@ const connectionServices = {
|
|
|
7923
10552
|
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
7924
10553
|
sizingConnectionService: inject(TYPES.sizingConnectionService),
|
|
7925
10554
|
riskConnectionService: inject(TYPES.riskConnectionService),
|
|
10555
|
+
optimizerConnectionService: inject(TYPES.optimizerConnectionService),
|
|
10556
|
+
partialConnectionService: inject(TYPES.partialConnectionService),
|
|
7926
10557
|
};
|
|
7927
10558
|
const schemaServices = {
|
|
7928
10559
|
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
@@ -7931,6 +10562,7 @@ const schemaServices = {
|
|
|
7931
10562
|
walkerSchemaService: inject(TYPES.walkerSchemaService),
|
|
7932
10563
|
sizingSchemaService: inject(TYPES.sizingSchemaService),
|
|
7933
10564
|
riskSchemaService: inject(TYPES.riskSchemaService),
|
|
10565
|
+
optimizerSchemaService: inject(TYPES.optimizerSchemaService),
|
|
7934
10566
|
};
|
|
7935
10567
|
const globalServices = {
|
|
7936
10568
|
exchangeGlobalService: inject(TYPES.exchangeGlobalService),
|
|
@@ -7938,6 +10570,8 @@ const globalServices = {
|
|
|
7938
10570
|
frameGlobalService: inject(TYPES.frameGlobalService),
|
|
7939
10571
|
sizingGlobalService: inject(TYPES.sizingGlobalService),
|
|
7940
10572
|
riskGlobalService: inject(TYPES.riskGlobalService),
|
|
10573
|
+
optimizerGlobalService: inject(TYPES.optimizerGlobalService),
|
|
10574
|
+
partialGlobalService: inject(TYPES.partialGlobalService),
|
|
7941
10575
|
};
|
|
7942
10576
|
const commandServices = {
|
|
7943
10577
|
liveCommandService: inject(TYPES.liveCommandService),
|
|
@@ -7961,6 +10595,7 @@ const markdownServices = {
|
|
|
7961
10595
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
7962
10596
|
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
7963
10597
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
10598
|
+
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
7964
10599
|
};
|
|
7965
10600
|
const validationServices = {
|
|
7966
10601
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -7969,6 +10604,10 @@ const validationServices = {
|
|
|
7969
10604
|
walkerValidationService: inject(TYPES.walkerValidationService),
|
|
7970
10605
|
sizingValidationService: inject(TYPES.sizingValidationService),
|
|
7971
10606
|
riskValidationService: inject(TYPES.riskValidationService),
|
|
10607
|
+
optimizerValidationService: inject(TYPES.optimizerValidationService),
|
|
10608
|
+
};
|
|
10609
|
+
const templateServices = {
|
|
10610
|
+
optimizerTemplateService: inject(TYPES.optimizerTemplateService),
|
|
7972
10611
|
};
|
|
7973
10612
|
const backtest = {
|
|
7974
10613
|
...baseServices,
|
|
@@ -7981,6 +10620,7 @@ const backtest = {
|
|
|
7981
10620
|
...logicPublicServices,
|
|
7982
10621
|
...markdownServices,
|
|
7983
10622
|
...validationServices,
|
|
10623
|
+
...templateServices,
|
|
7984
10624
|
};
|
|
7985
10625
|
init();
|
|
7986
10626
|
var backtest$1 = backtest;
|
|
@@ -8026,6 +10666,7 @@ const ADD_FRAME_METHOD_NAME = "add.addFrame";
|
|
|
8026
10666
|
const ADD_WALKER_METHOD_NAME = "add.addWalker";
|
|
8027
10667
|
const ADD_SIZING_METHOD_NAME = "add.addSizing";
|
|
8028
10668
|
const ADD_RISK_METHOD_NAME = "add.addRisk";
|
|
10669
|
+
const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizer";
|
|
8029
10670
|
/**
|
|
8030
10671
|
* Registers a trading strategy in the framework.
|
|
8031
10672
|
*
|
|
@@ -8317,6 +10958,100 @@ function addRisk(riskSchema) {
|
|
|
8317
10958
|
backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
|
|
8318
10959
|
backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
|
|
8319
10960
|
}
|
|
10961
|
+
/**
|
|
10962
|
+
* Registers an optimizer configuration in the framework.
|
|
10963
|
+
*
|
|
10964
|
+
* The optimizer generates trading strategies by:
|
|
10965
|
+
* - Collecting data from multiple sources across training periods
|
|
10966
|
+
* - Building LLM conversation history with fetched data
|
|
10967
|
+
* - Generating strategy prompts using getPrompt()
|
|
10968
|
+
* - Creating executable backtest code with templates
|
|
10969
|
+
*
|
|
10970
|
+
* The optimizer produces a complete .mjs file containing:
|
|
10971
|
+
* - Exchange, Frame, Strategy, and Walker configurations
|
|
10972
|
+
* - Multi-timeframe analysis logic
|
|
10973
|
+
* - LLM integration for signal generation
|
|
10974
|
+
* - Event listeners for progress tracking
|
|
10975
|
+
*
|
|
10976
|
+
* @param optimizerSchema - Optimizer configuration object
|
|
10977
|
+
* @param optimizerSchema.optimizerName - Unique optimizer identifier
|
|
10978
|
+
* @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
|
|
10979
|
+
* @param optimizerSchema.rangeTest - Testing time range for strategy validation
|
|
10980
|
+
* @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
|
|
10981
|
+
* @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
|
|
10982
|
+
* @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
|
|
10983
|
+
* @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
|
|
10984
|
+
*
|
|
10985
|
+
* @example
|
|
10986
|
+
* ```typescript
|
|
10987
|
+
* // Basic optimizer with single data source
|
|
10988
|
+
* addOptimizer({
|
|
10989
|
+
* optimizerName: "llm-strategy-generator",
|
|
10990
|
+
* rangeTrain: [
|
|
10991
|
+
* {
|
|
10992
|
+
* note: "Bull market period",
|
|
10993
|
+
* startDate: new Date("2024-01-01"),
|
|
10994
|
+
* endDate: new Date("2024-01-31"),
|
|
10995
|
+
* },
|
|
10996
|
+
* {
|
|
10997
|
+
* note: "Bear market period",
|
|
10998
|
+
* startDate: new Date("2024-02-01"),
|
|
10999
|
+
* endDate: new Date("2024-02-28"),
|
|
11000
|
+
* },
|
|
11001
|
+
* ],
|
|
11002
|
+
* rangeTest: {
|
|
11003
|
+
* note: "Validation period",
|
|
11004
|
+
* startDate: new Date("2024-03-01"),
|
|
11005
|
+
* endDate: new Date("2024-03-31"),
|
|
11006
|
+
* },
|
|
11007
|
+
* source: [
|
|
11008
|
+
* {
|
|
11009
|
+
* name: "historical-backtests",
|
|
11010
|
+
* fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
|
|
11011
|
+
* // Fetch historical backtest results from database
|
|
11012
|
+
* return await db.backtests.find({
|
|
11013
|
+
* symbol,
|
|
11014
|
+
* date: { $gte: startDate, $lte: endDate },
|
|
11015
|
+
* })
|
|
11016
|
+
* .skip(offset)
|
|
11017
|
+
* .limit(limit);
|
|
11018
|
+
* },
|
|
11019
|
+
* user: async (symbol, data, name) => {
|
|
11020
|
+
* return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
|
|
11021
|
+
* },
|
|
11022
|
+
* assistant: async (symbol, data, name) => {
|
|
11023
|
+
* return "Historical data analyzed successfully";
|
|
11024
|
+
* },
|
|
11025
|
+
* },
|
|
11026
|
+
* ],
|
|
11027
|
+
* getPrompt: async (symbol, messages) => {
|
|
11028
|
+
* // Generate strategy prompt from conversation
|
|
11029
|
+
* return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
|
|
11030
|
+
* },
|
|
11031
|
+
* callbacks: {
|
|
11032
|
+
* onData: (symbol, strategyData) => {
|
|
11033
|
+
* console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
|
|
11034
|
+
* },
|
|
11035
|
+
* onCode: (symbol, code) => {
|
|
11036
|
+
* console.log(`Generated ${code.length} characters of code for ${symbol}`);
|
|
11037
|
+
* },
|
|
11038
|
+
* onDump: (symbol, filepath) => {
|
|
11039
|
+
* console.log(`Saved strategy to ${filepath}`);
|
|
11040
|
+
* },
|
|
11041
|
+
* onSourceData: (symbol, sourceName, data, startDate, endDate) => {
|
|
11042
|
+
* console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
|
|
11043
|
+
* },
|
|
11044
|
+
* },
|
|
11045
|
+
* });
|
|
11046
|
+
* ```
|
|
11047
|
+
*/
|
|
11048
|
+
function addOptimizer(optimizerSchema) {
|
|
11049
|
+
backtest$1.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
|
|
11050
|
+
optimizerSchema,
|
|
11051
|
+
});
|
|
11052
|
+
backtest$1.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
|
|
11053
|
+
backtest$1.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
|
|
11054
|
+
}
|
|
8320
11055
|
|
|
8321
11056
|
const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
|
|
8322
11057
|
const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
|
|
@@ -8324,6 +11059,7 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
|
|
|
8324
11059
|
const LIST_WALKERS_METHOD_NAME = "list.listWalkers";
|
|
8325
11060
|
const LIST_SIZINGS_METHOD_NAME = "list.listSizings";
|
|
8326
11061
|
const LIST_RISKS_METHOD_NAME = "list.listRisks";
|
|
11062
|
+
const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizers";
|
|
8327
11063
|
/**
|
|
8328
11064
|
* Returns a list of all registered exchange schemas.
|
|
8329
11065
|
*
|
|
@@ -8521,6 +11257,46 @@ async function listRisks() {
|
|
|
8521
11257
|
backtest$1.loggerService.log(LIST_RISKS_METHOD_NAME);
|
|
8522
11258
|
return await backtest$1.riskValidationService.list();
|
|
8523
11259
|
}
|
|
11260
|
+
/**
|
|
11261
|
+
* Returns a list of all registered optimizer schemas.
|
|
11262
|
+
*
|
|
11263
|
+
* Retrieves all optimizers that have been registered via addOptimizer().
|
|
11264
|
+
* Useful for debugging, documentation, or building dynamic UIs.
|
|
11265
|
+
*
|
|
11266
|
+
* @returns Array of optimizer schemas with their configurations
|
|
11267
|
+
*
|
|
11268
|
+
* @example
|
|
11269
|
+
* ```typescript
|
|
11270
|
+
* import { listOptimizers, addOptimizer } from "backtest-kit";
|
|
11271
|
+
*
|
|
11272
|
+
* addOptimizer({
|
|
11273
|
+
* optimizerName: "llm-strategy-generator",
|
|
11274
|
+
* note: "Generates trading strategies using LLM",
|
|
11275
|
+
* rangeTrain: [
|
|
11276
|
+
* {
|
|
11277
|
+
* note: "Training period 1",
|
|
11278
|
+
* startDate: new Date("2024-01-01"),
|
|
11279
|
+
* endDate: new Date("2024-01-31"),
|
|
11280
|
+
* },
|
|
11281
|
+
* ],
|
|
11282
|
+
* rangeTest: {
|
|
11283
|
+
* note: "Testing period",
|
|
11284
|
+
* startDate: new Date("2024-02-01"),
|
|
11285
|
+
* endDate: new Date("2024-02-28"),
|
|
11286
|
+
* },
|
|
11287
|
+
* source: [],
|
|
11288
|
+
* getPrompt: async (symbol, messages) => "Generate strategy",
|
|
11289
|
+
* });
|
|
11290
|
+
*
|
|
11291
|
+
* const optimizers = listOptimizers();
|
|
11292
|
+
* console.log(optimizers);
|
|
11293
|
+
* // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
|
|
11294
|
+
* ```
|
|
11295
|
+
*/
|
|
11296
|
+
async function listOptimizers() {
|
|
11297
|
+
backtest$1.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
|
|
11298
|
+
return await backtest$1.optimizerValidationService.list();
|
|
11299
|
+
}
|
|
8524
11300
|
|
|
8525
11301
|
const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
|
|
8526
11302
|
const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
|
|
@@ -8535,12 +11311,17 @@ const LISTEN_DONE_BACKTEST_METHOD_NAME = "event.listenDoneBacktest";
|
|
|
8535
11311
|
const LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME = "event.listenDoneBacktestOnce";
|
|
8536
11312
|
const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
|
|
8537
11313
|
const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
|
|
8538
|
-
const LISTEN_PROGRESS_METHOD_NAME = "event.
|
|
11314
|
+
const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
|
|
11315
|
+
const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
|
|
8539
11316
|
const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
|
|
8540
11317
|
const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
|
|
8541
11318
|
const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
|
|
8542
11319
|
const LISTEN_WALKER_COMPLETE_METHOD_NAME = "event.listenWalkerComplete";
|
|
8543
11320
|
const LISTEN_VALIDATION_METHOD_NAME = "event.listenValidation";
|
|
11321
|
+
const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
|
|
11322
|
+
const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
|
|
11323
|
+
const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
|
|
11324
|
+
const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
|
|
8544
11325
|
/**
|
|
8545
11326
|
* Subscribes to all signal events with queued async processing.
|
|
8546
11327
|
*
|
|
@@ -8921,21 +11702,55 @@ function listenDoneWalkerOnce(filterFn, fn) {
|
|
|
8921
11702
|
* Events are processed sequentially in order received, even if callback is async.
|
|
8922
11703
|
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
8923
11704
|
*
|
|
8924
|
-
* @param fn - Callback function to handle progress events
|
|
11705
|
+
* @param fn - Callback function to handle progress events
|
|
11706
|
+
* @returns Unsubscribe function to stop listening to events
|
|
11707
|
+
*
|
|
11708
|
+
* @example
|
|
11709
|
+
* ```typescript
|
|
11710
|
+
* import { listenBacktestProgress, Backtest } from "backtest-kit";
|
|
11711
|
+
*
|
|
11712
|
+
* const unsubscribe = listenBacktestProgress((event) => {
|
|
11713
|
+
* console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
|
|
11714
|
+
* console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
|
|
11715
|
+
* console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
|
|
11716
|
+
* });
|
|
11717
|
+
*
|
|
11718
|
+
* Backtest.background("BTCUSDT", {
|
|
11719
|
+
* strategyName: "my-strategy",
|
|
11720
|
+
* exchangeName: "binance",
|
|
11721
|
+
* frameName: "1d-backtest"
|
|
11722
|
+
* });
|
|
11723
|
+
*
|
|
11724
|
+
* // Later: stop listening
|
|
11725
|
+
* unsubscribe();
|
|
11726
|
+
* ```
|
|
11727
|
+
*/
|
|
11728
|
+
function listenBacktestProgress(fn) {
|
|
11729
|
+
backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
|
|
11730
|
+
return progressBacktestEmitter.subscribe(queued(async (event) => fn(event)));
|
|
11731
|
+
}
|
|
11732
|
+
/**
|
|
11733
|
+
* Subscribes to walker progress events with queued async processing.
|
|
11734
|
+
*
|
|
11735
|
+
* Emits during Walker.run() execution after each strategy completes.
|
|
11736
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
11737
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
11738
|
+
*
|
|
11739
|
+
* @param fn - Callback function to handle walker progress events
|
|
8925
11740
|
* @returns Unsubscribe function to stop listening to events
|
|
8926
11741
|
*
|
|
8927
11742
|
* @example
|
|
8928
11743
|
* ```typescript
|
|
8929
|
-
* import {
|
|
11744
|
+
* import { listenWalkerProgress, Walker } from "backtest-kit";
|
|
8930
11745
|
*
|
|
8931
|
-
* const unsubscribe =
|
|
11746
|
+
* const unsubscribe = listenWalkerProgress((event) => {
|
|
8932
11747
|
* console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
|
|
8933
|
-
* console.log(`${event.
|
|
8934
|
-
* console.log(`
|
|
11748
|
+
* console.log(`${event.processedStrategies} / ${event.totalStrategies} strategies`);
|
|
11749
|
+
* console.log(`Walker: ${event.walkerName}, Symbol: ${event.symbol}`);
|
|
8935
11750
|
* });
|
|
8936
11751
|
*
|
|
8937
|
-
*
|
|
8938
|
-
*
|
|
11752
|
+
* Walker.run("BTCUSDT", {
|
|
11753
|
+
* walkerName: "my-walker",
|
|
8939
11754
|
* exchangeName: "binance",
|
|
8940
11755
|
* frameName: "1d-backtest"
|
|
8941
11756
|
* });
|
|
@@ -8944,9 +11759,9 @@ function listenDoneWalkerOnce(filterFn, fn) {
|
|
|
8944
11759
|
* unsubscribe();
|
|
8945
11760
|
* ```
|
|
8946
11761
|
*/
|
|
8947
|
-
function
|
|
8948
|
-
backtest$1.loggerService.log(
|
|
8949
|
-
return
|
|
11762
|
+
function listenWalkerProgress(fn) {
|
|
11763
|
+
backtest$1.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
|
|
11764
|
+
return progressWalkerEmitter.subscribe(queued(async (event) => fn(event)));
|
|
8950
11765
|
}
|
|
8951
11766
|
/**
|
|
8952
11767
|
* Subscribes to performance metric events with queued async processing.
|
|
@@ -9124,6 +11939,130 @@ function listenValidation(fn) {
|
|
|
9124
11939
|
backtest$1.loggerService.log(LISTEN_VALIDATION_METHOD_NAME);
|
|
9125
11940
|
return validationSubject.subscribe(queued(async (error) => fn(error)));
|
|
9126
11941
|
}
|
|
11942
|
+
/**
|
|
11943
|
+
* Subscribes to partial profit level events with queued async processing.
|
|
11944
|
+
*
|
|
11945
|
+
* Emits when a signal reaches a profit level milestone (10%, 20%, 30%, etc).
|
|
11946
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
11947
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
11948
|
+
*
|
|
11949
|
+
* @param fn - Callback function to handle partial profit events
|
|
11950
|
+
* @returns Unsubscribe function to stop listening to events
|
|
11951
|
+
*
|
|
11952
|
+
* @example
|
|
11953
|
+
* ```typescript
|
|
11954
|
+
* import { listenPartialProfit } from "./function/event";
|
|
11955
|
+
*
|
|
11956
|
+
* const unsubscribe = listenPartialProfit((event) => {
|
|
11957
|
+
* console.log(`Signal ${event.data.id} reached ${event.level}% profit`);
|
|
11958
|
+
* console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
|
|
11959
|
+
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
11960
|
+
* });
|
|
11961
|
+
*
|
|
11962
|
+
* // Later: stop listening
|
|
11963
|
+
* unsubscribe();
|
|
11964
|
+
* ```
|
|
11965
|
+
*/
|
|
11966
|
+
function listenPartialProfit(fn) {
|
|
11967
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_METHOD_NAME);
|
|
11968
|
+
return partialProfitSubject.subscribe(queued(async (event) => fn(event)));
|
|
11969
|
+
}
|
|
11970
|
+
/**
|
|
11971
|
+
* Subscribes to filtered partial profit level events with one-time execution.
|
|
11972
|
+
*
|
|
11973
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
11974
|
+
* and automatically unsubscribes. Useful for waiting for specific profit conditions.
|
|
11975
|
+
*
|
|
11976
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
11977
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
11978
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
11979
|
+
*
|
|
11980
|
+
* @example
|
|
11981
|
+
* ```typescript
|
|
11982
|
+
* import { listenPartialProfitOnce } from "./function/event";
|
|
11983
|
+
*
|
|
11984
|
+
* // Wait for first 50% profit level on any signal
|
|
11985
|
+
* listenPartialProfitOnce(
|
|
11986
|
+
* (event) => event.level === 50,
|
|
11987
|
+
* (event) => console.log("50% profit reached:", event.data.id)
|
|
11988
|
+
* );
|
|
11989
|
+
*
|
|
11990
|
+
* // Wait for 30% profit on BTCUSDT
|
|
11991
|
+
* const cancel = listenPartialProfitOnce(
|
|
11992
|
+
* (event) => event.symbol === "BTCUSDT" && event.level === 30,
|
|
11993
|
+
* (event) => console.log("BTCUSDT hit 30% profit")
|
|
11994
|
+
* );
|
|
11995
|
+
*
|
|
11996
|
+
* // Cancel if needed before event fires
|
|
11997
|
+
* cancel();
|
|
11998
|
+
* ```
|
|
11999
|
+
*/
|
|
12000
|
+
function listenPartialProfitOnce(filterFn, fn) {
|
|
12001
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME);
|
|
12002
|
+
return partialProfitSubject.filter(filterFn).once(fn);
|
|
12003
|
+
}
|
|
12004
|
+
/**
|
|
12005
|
+
* Subscribes to partial loss level events with queued async processing.
|
|
12006
|
+
*
|
|
12007
|
+
* Emits when a signal reaches a loss level milestone (10%, 20%, 30%, etc).
|
|
12008
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
12009
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
12010
|
+
*
|
|
12011
|
+
* @param fn - Callback function to handle partial loss events
|
|
12012
|
+
* @returns Unsubscribe function to stop listening to events
|
|
12013
|
+
*
|
|
12014
|
+
* @example
|
|
12015
|
+
* ```typescript
|
|
12016
|
+
* import { listenPartialLoss } from "./function/event";
|
|
12017
|
+
*
|
|
12018
|
+
* const unsubscribe = listenPartialLoss((event) => {
|
|
12019
|
+
* console.log(`Signal ${event.data.id} reached ${event.level}% loss`);
|
|
12020
|
+
* console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
|
|
12021
|
+
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
12022
|
+
* });
|
|
12023
|
+
*
|
|
12024
|
+
* // Later: stop listening
|
|
12025
|
+
* unsubscribe();
|
|
12026
|
+
* ```
|
|
12027
|
+
*/
|
|
12028
|
+
function listenPartialLoss(fn) {
|
|
12029
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_METHOD_NAME);
|
|
12030
|
+
return partialLossSubject.subscribe(queued(async (event) => fn(event)));
|
|
12031
|
+
}
|
|
12032
|
+
/**
|
|
12033
|
+
* Subscribes to filtered partial loss level events with one-time execution.
|
|
12034
|
+
*
|
|
12035
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
12036
|
+
* and automatically unsubscribes. Useful for waiting for specific loss conditions.
|
|
12037
|
+
*
|
|
12038
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
12039
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
12040
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
12041
|
+
*
|
|
12042
|
+
* @example
|
|
12043
|
+
* ```typescript
|
|
12044
|
+
* import { listenPartialLossOnce } from "./function/event";
|
|
12045
|
+
*
|
|
12046
|
+
* // Wait for first 20% loss level on any signal
|
|
12047
|
+
* listenPartialLossOnce(
|
|
12048
|
+
* (event) => event.level === 20,
|
|
12049
|
+
* (event) => console.log("20% loss reached:", event.data.id)
|
|
12050
|
+
* );
|
|
12051
|
+
*
|
|
12052
|
+
* // Wait for 10% loss on ETHUSDT in live mode
|
|
12053
|
+
* const cancel = listenPartialLossOnce(
|
|
12054
|
+
* (event) => event.symbol === "ETHUSDT" && event.level === 10 && !event.backtest,
|
|
12055
|
+
* (event) => console.log("ETHUSDT hit 10% loss in live mode")
|
|
12056
|
+
* );
|
|
12057
|
+
*
|
|
12058
|
+
* // Cancel if needed before event fires
|
|
12059
|
+
* cancel();
|
|
12060
|
+
* ```
|
|
12061
|
+
*/
|
|
12062
|
+
function listenPartialLossOnce(filterFn, fn) {
|
|
12063
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
|
|
12064
|
+
return partialLossSubject.filter(filterFn).once(fn);
|
|
12065
|
+
}
|
|
9127
12066
|
|
|
9128
12067
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
9129
12068
|
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
@@ -9341,22 +12280,42 @@ class BacktestUtils {
|
|
|
9341
12280
|
context,
|
|
9342
12281
|
});
|
|
9343
12282
|
let isStopped = false;
|
|
12283
|
+
let isDone = false;
|
|
9344
12284
|
const task = async () => {
|
|
9345
12285
|
for await (const _ of this.run(symbol, context)) {
|
|
9346
12286
|
if (isStopped) {
|
|
9347
12287
|
break;
|
|
9348
12288
|
}
|
|
9349
12289
|
}
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
12290
|
+
if (!isDone) {
|
|
12291
|
+
await doneBacktestSubject.next({
|
|
12292
|
+
exchangeName: context.exchangeName,
|
|
12293
|
+
strategyName: context.strategyName,
|
|
12294
|
+
backtest: true,
|
|
12295
|
+
symbol,
|
|
12296
|
+
});
|
|
12297
|
+
}
|
|
12298
|
+
isDone = true;
|
|
9356
12299
|
};
|
|
9357
12300
|
task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
|
|
9358
12301
|
return () => {
|
|
9359
12302
|
backtest$1.strategyGlobalService.stop(context.strategyName);
|
|
12303
|
+
backtest$1.strategyGlobalService
|
|
12304
|
+
.getPendingSignal(context.strategyName)
|
|
12305
|
+
.then(async (pendingSignal) => {
|
|
12306
|
+
if (pendingSignal) {
|
|
12307
|
+
return;
|
|
12308
|
+
}
|
|
12309
|
+
if (!isDone) {
|
|
12310
|
+
await doneBacktestSubject.next({
|
|
12311
|
+
exchangeName: context.exchangeName,
|
|
12312
|
+
strategyName: context.strategyName,
|
|
12313
|
+
backtest: true,
|
|
12314
|
+
symbol,
|
|
12315
|
+
});
|
|
12316
|
+
}
|
|
12317
|
+
isDone = true;
|
|
12318
|
+
});
|
|
9360
12319
|
isStopped = true;
|
|
9361
12320
|
};
|
|
9362
12321
|
};
|
|
@@ -9400,11 +12359,11 @@ class BacktestUtils {
|
|
|
9400
12359
|
* Saves strategy report to disk.
|
|
9401
12360
|
*
|
|
9402
12361
|
* @param strategyName - Strategy name to save report for
|
|
9403
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12362
|
+
* @param path - Optional directory path to save report (default: "./dump/backtest")
|
|
9404
12363
|
*
|
|
9405
12364
|
* @example
|
|
9406
12365
|
* ```typescript
|
|
9407
|
-
* // Save to default path: ./
|
|
12366
|
+
* // Save to default path: ./dump/backtest/my-strategy.md
|
|
9408
12367
|
* await Backtest.dump("my-strategy");
|
|
9409
12368
|
*
|
|
9410
12369
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9551,8 +12510,11 @@ class LiveUtils {
|
|
|
9551
12510
|
return () => {
|
|
9552
12511
|
backtest$1.strategyGlobalService.stop(context.strategyName);
|
|
9553
12512
|
backtest$1.strategyGlobalService
|
|
9554
|
-
.getPendingSignal(
|
|
9555
|
-
.then(async () => {
|
|
12513
|
+
.getPendingSignal(context.strategyName)
|
|
12514
|
+
.then(async (pendingSignal) => {
|
|
12515
|
+
if (pendingSignal) {
|
|
12516
|
+
return;
|
|
12517
|
+
}
|
|
9556
12518
|
if (!isDone) {
|
|
9557
12519
|
await doneLiveSubject.next({
|
|
9558
12520
|
exchangeName: context.exchangeName,
|
|
@@ -9606,11 +12568,11 @@ class LiveUtils {
|
|
|
9606
12568
|
* Saves strategy report to disk.
|
|
9607
12569
|
*
|
|
9608
12570
|
* @param strategyName - Strategy name to save report for
|
|
9609
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12571
|
+
* @param path - Optional directory path to save report (default: "./dump/live")
|
|
9610
12572
|
*
|
|
9611
12573
|
* @example
|
|
9612
12574
|
* ```typescript
|
|
9613
|
-
* // Save to default path: ./
|
|
12575
|
+
* // Save to default path: ./dump/live/my-strategy.md
|
|
9614
12576
|
* await Live.dump("my-strategy");
|
|
9615
12577
|
*
|
|
9616
12578
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9646,7 +12608,6 @@ const Live = new LiveUtils();
|
|
|
9646
12608
|
const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
|
|
9647
12609
|
const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
|
|
9648
12610
|
const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
|
|
9649
|
-
const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
|
|
9650
12611
|
/**
|
|
9651
12612
|
* Utility class for scheduled signals reporting operations.
|
|
9652
12613
|
*
|
|
@@ -9714,11 +12675,11 @@ class ScheduleUtils {
|
|
|
9714
12675
|
* Saves strategy report to disk.
|
|
9715
12676
|
*
|
|
9716
12677
|
* @param strategyName - Strategy name to save report for
|
|
9717
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12678
|
+
* @param path - Optional directory path to save report (default: "./dump/schedule")
|
|
9718
12679
|
*
|
|
9719
12680
|
* @example
|
|
9720
12681
|
* ```typescript
|
|
9721
|
-
* // Save to default path: ./
|
|
12682
|
+
* // Save to default path: ./dump/schedule/my-strategy.md
|
|
9722
12683
|
* await Schedule.dump("my-strategy");
|
|
9723
12684
|
*
|
|
9724
12685
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9732,28 +12693,6 @@ class ScheduleUtils {
|
|
|
9732
12693
|
});
|
|
9733
12694
|
await backtest$1.scheduleMarkdownService.dump(strategyName, path);
|
|
9734
12695
|
};
|
|
9735
|
-
/**
|
|
9736
|
-
* Clears accumulated scheduled signal data from storage.
|
|
9737
|
-
* If strategyName is provided, clears only that strategy's data.
|
|
9738
|
-
* If strategyName is omitted, clears all strategies' data.
|
|
9739
|
-
*
|
|
9740
|
-
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
9741
|
-
*
|
|
9742
|
-
* @example
|
|
9743
|
-
* ```typescript
|
|
9744
|
-
* // Clear specific strategy data
|
|
9745
|
-
* await Schedule.clear("my-strategy");
|
|
9746
|
-
*
|
|
9747
|
-
* // Clear all strategies' data
|
|
9748
|
-
* await Schedule.clear();
|
|
9749
|
-
* ```
|
|
9750
|
-
*/
|
|
9751
|
-
this.clear = async (strategyName) => {
|
|
9752
|
-
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
|
|
9753
|
-
strategyName,
|
|
9754
|
-
});
|
|
9755
|
-
await backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9756
|
-
};
|
|
9757
12696
|
}
|
|
9758
12697
|
}
|
|
9759
12698
|
/**
|
|
@@ -9860,21 +12799,21 @@ class Performance {
|
|
|
9860
12799
|
* Saves performance report to disk.
|
|
9861
12800
|
*
|
|
9862
12801
|
* Creates directory if it doesn't exist.
|
|
9863
|
-
* Default path: ./
|
|
12802
|
+
* Default path: ./dump/performance/{strategyName}.md
|
|
9864
12803
|
*
|
|
9865
12804
|
* @param strategyName - Strategy name to save report for
|
|
9866
12805
|
* @param path - Optional custom directory path
|
|
9867
12806
|
*
|
|
9868
12807
|
* @example
|
|
9869
12808
|
* ```typescript
|
|
9870
|
-
* // Save to default path: ./
|
|
12809
|
+
* // Save to default path: ./dump/performance/my-strategy.md
|
|
9871
12810
|
* await Performance.dump("my-strategy");
|
|
9872
12811
|
*
|
|
9873
12812
|
* // Save to custom path: ./reports/perf/my-strategy.md
|
|
9874
12813
|
* await Performance.dump("my-strategy", "./reports/perf");
|
|
9875
12814
|
* ```
|
|
9876
12815
|
*/
|
|
9877
|
-
static async dump(strategyName, path = "./
|
|
12816
|
+
static async dump(strategyName, path = "./dump/performance") {
|
|
9878
12817
|
return backtest$1.performanceMarkdownService.dump(strategyName, path);
|
|
9879
12818
|
}
|
|
9880
12819
|
/**
|
|
@@ -9988,25 +12927,39 @@ class WalkerUtils {
|
|
|
9988
12927
|
});
|
|
9989
12928
|
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
9990
12929
|
let isStopped = false;
|
|
12930
|
+
let isDone = false;
|
|
9991
12931
|
const task = async () => {
|
|
9992
12932
|
for await (const _ of this.run(symbol, context)) {
|
|
9993
12933
|
if (isStopped) {
|
|
9994
12934
|
break;
|
|
9995
12935
|
}
|
|
9996
12936
|
}
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
12937
|
+
if (!isDone) {
|
|
12938
|
+
await doneWalkerSubject.next({
|
|
12939
|
+
exchangeName: walkerSchema.exchangeName,
|
|
12940
|
+
strategyName: context.walkerName,
|
|
12941
|
+
backtest: true,
|
|
12942
|
+
symbol,
|
|
12943
|
+
});
|
|
12944
|
+
}
|
|
12945
|
+
isDone = true;
|
|
10003
12946
|
};
|
|
10004
12947
|
task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
|
|
10005
12948
|
return () => {
|
|
10006
|
-
isStopped = true;
|
|
10007
12949
|
for (const strategyName of walkerSchema.strategies) {
|
|
10008
12950
|
backtest$1.strategyGlobalService.stop(strategyName);
|
|
10009
12951
|
}
|
|
12952
|
+
walkerStopSubject.next(context.walkerName);
|
|
12953
|
+
if (!isDone) {
|
|
12954
|
+
doneWalkerSubject.next({
|
|
12955
|
+
exchangeName: walkerSchema.exchangeName,
|
|
12956
|
+
strategyName: context.walkerName,
|
|
12957
|
+
backtest: true,
|
|
12958
|
+
symbol,
|
|
12959
|
+
});
|
|
12960
|
+
}
|
|
12961
|
+
isDone = true;
|
|
12962
|
+
isStopped = true;
|
|
10010
12963
|
};
|
|
10011
12964
|
};
|
|
10012
12965
|
/**
|
|
@@ -10062,11 +13015,11 @@ class WalkerUtils {
|
|
|
10062
13015
|
*
|
|
10063
13016
|
* @param symbol - Trading symbol
|
|
10064
13017
|
* @param walkerName - Walker name to save report for
|
|
10065
|
-
* @param path - Optional directory path to save report (default: "./
|
|
13018
|
+
* @param path - Optional directory path to save report (default: "./dump/walker")
|
|
10066
13019
|
*
|
|
10067
13020
|
* @example
|
|
10068
13021
|
* ```typescript
|
|
10069
|
-
* // Save to default path: ./
|
|
13022
|
+
* // Save to default path: ./dump/walker/my-walker.md
|
|
10070
13023
|
* await Walker.dump("BTCUSDT", "my-walker");
|
|
10071
13024
|
*
|
|
10072
13025
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
@@ -10195,11 +13148,11 @@ class HeatUtils {
|
|
|
10195
13148
|
* Default filename: {strategyName}.md
|
|
10196
13149
|
*
|
|
10197
13150
|
* @param strategyName - Strategy name to save heatmap report for
|
|
10198
|
-
* @param path - Optional directory path to save report (default: "./
|
|
13151
|
+
* @param path - Optional directory path to save report (default: "./dump/heatmap")
|
|
10199
13152
|
*
|
|
10200
13153
|
* @example
|
|
10201
13154
|
* ```typescript
|
|
10202
|
-
* // Save to default path: ./
|
|
13155
|
+
* // Save to default path: ./dump/heatmap/my-strategy.md
|
|
10203
13156
|
* await Heat.dump("my-strategy");
|
|
10204
13157
|
*
|
|
10205
13158
|
* // Save to custom path: ./reports/my-strategy.md
|
|
@@ -10362,4 +13315,258 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
|
|
|
10362
13315
|
};
|
|
10363
13316
|
const PositionSize = PositionSizeUtils;
|
|
10364
13317
|
|
|
10365
|
-
|
|
13318
|
+
const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
|
|
13319
|
+
const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
|
|
13320
|
+
const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
|
|
13321
|
+
/**
|
|
13322
|
+
* Public API utilities for optimizer operations.
|
|
13323
|
+
* Provides high-level methods for strategy generation and code export.
|
|
13324
|
+
*
|
|
13325
|
+
* Usage:
|
|
13326
|
+
* ```typescript
|
|
13327
|
+
* import { Optimizer } from "backtest-kit";
|
|
13328
|
+
*
|
|
13329
|
+
* // Get strategy data
|
|
13330
|
+
* const strategies = await Optimizer.getData("BTCUSDT", {
|
|
13331
|
+
* optimizerName: "my-optimizer"
|
|
13332
|
+
* });
|
|
13333
|
+
*
|
|
13334
|
+
* // Generate code
|
|
13335
|
+
* const code = await Optimizer.getCode("BTCUSDT", {
|
|
13336
|
+
* optimizerName: "my-optimizer"
|
|
13337
|
+
* });
|
|
13338
|
+
*
|
|
13339
|
+
* // Save to file
|
|
13340
|
+
* await Optimizer.dump("BTCUSDT", {
|
|
13341
|
+
* optimizerName: "my-optimizer"
|
|
13342
|
+
* }, "./output");
|
|
13343
|
+
* ```
|
|
13344
|
+
*/
|
|
13345
|
+
class OptimizerUtils {
|
|
13346
|
+
constructor() {
|
|
13347
|
+
/**
|
|
13348
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
13349
|
+
* Processes each training range and builds LLM conversation history.
|
|
13350
|
+
*
|
|
13351
|
+
* @param symbol - Trading pair symbol
|
|
13352
|
+
* @param context - Context with optimizerName
|
|
13353
|
+
* @returns Array of generated strategies with conversation context
|
|
13354
|
+
* @throws Error if optimizer not found
|
|
13355
|
+
*/
|
|
13356
|
+
this.getData = async (symbol, context) => {
|
|
13357
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
|
|
13358
|
+
symbol,
|
|
13359
|
+
context,
|
|
13360
|
+
});
|
|
13361
|
+
return await backtest$1.optimizerGlobalService.getData(symbol, context.optimizerName);
|
|
13362
|
+
};
|
|
13363
|
+
/**
|
|
13364
|
+
* Generates complete executable strategy code.
|
|
13365
|
+
* Includes imports, helpers, strategies, walker, and launcher.
|
|
13366
|
+
*
|
|
13367
|
+
* @param symbol - Trading pair symbol
|
|
13368
|
+
* @param context - Context with optimizerName
|
|
13369
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
13370
|
+
* @throws Error if optimizer not found
|
|
13371
|
+
*/
|
|
13372
|
+
this.getCode = async (symbol, context) => {
|
|
13373
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
|
|
13374
|
+
symbol,
|
|
13375
|
+
context,
|
|
13376
|
+
});
|
|
13377
|
+
return await backtest$1.optimizerGlobalService.getCode(symbol, context.optimizerName);
|
|
13378
|
+
};
|
|
13379
|
+
/**
|
|
13380
|
+
* Generates and saves strategy code to file.
|
|
13381
|
+
* Creates directory if needed, writes .mjs file.
|
|
13382
|
+
*
|
|
13383
|
+
* Format: `{optimizerName}_{symbol}.mjs`
|
|
13384
|
+
*
|
|
13385
|
+
* @param symbol - Trading pair symbol
|
|
13386
|
+
* @param context - Context with optimizerName
|
|
13387
|
+
* @param path - Output directory path (default: "./")
|
|
13388
|
+
* @throws Error if optimizer not found or file write fails
|
|
13389
|
+
*/
|
|
13390
|
+
this.dump = async (symbol, context, path) => {
|
|
13391
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
|
|
13392
|
+
symbol,
|
|
13393
|
+
context,
|
|
13394
|
+
path,
|
|
13395
|
+
});
|
|
13396
|
+
await backtest$1.optimizerGlobalService.dump(symbol, context.optimizerName, path);
|
|
13397
|
+
};
|
|
13398
|
+
}
|
|
13399
|
+
}
|
|
13400
|
+
/**
|
|
13401
|
+
* Singleton instance of OptimizerUtils.
|
|
13402
|
+
* Public API for optimizer operations.
|
|
13403
|
+
*
|
|
13404
|
+
* @example
|
|
13405
|
+
* ```typescript
|
|
13406
|
+
* import { Optimizer } from "backtest-kit";
|
|
13407
|
+
*
|
|
13408
|
+
* await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
|
|
13409
|
+
* ```
|
|
13410
|
+
*/
|
|
13411
|
+
const Optimizer = new OptimizerUtils();
|
|
13412
|
+
|
|
13413
|
+
const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
|
|
13414
|
+
const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
|
|
13415
|
+
const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
|
|
13416
|
+
/**
|
|
13417
|
+
* Utility class for accessing partial profit/loss reports and statistics.
|
|
13418
|
+
*
|
|
13419
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
13420
|
+
* accumulated by PartialMarkdownService from partial profit/loss events.
|
|
13421
|
+
*
|
|
13422
|
+
* Features:
|
|
13423
|
+
* - Statistical data extraction (total profit/loss events count)
|
|
13424
|
+
* - Markdown report generation with event tables
|
|
13425
|
+
* - File export to disk
|
|
13426
|
+
*
|
|
13427
|
+
* Data source:
|
|
13428
|
+
* - PartialMarkdownService listens to partialProfitSubject/partialLossSubject
|
|
13429
|
+
* - Accumulates events in ReportStorage (max 250 events per symbol)
|
|
13430
|
+
* - Events include: timestamp, action, symbol, signalId, position, level, price, mode
|
|
13431
|
+
*
|
|
13432
|
+
* @example
|
|
13433
|
+
* ```typescript
|
|
13434
|
+
* import { Partial } from "./classes/Partial";
|
|
13435
|
+
*
|
|
13436
|
+
* // Get statistical data for BTCUSDT
|
|
13437
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13438
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
13439
|
+
* console.log(`Profit events: ${stats.totalProfit}`);
|
|
13440
|
+
* console.log(`Loss events: ${stats.totalLoss}`);
|
|
13441
|
+
*
|
|
13442
|
+
* // Generate markdown report
|
|
13443
|
+
* const markdown = await Partial.getReport("BTCUSDT");
|
|
13444
|
+
* console.log(markdown); // Formatted table with all events
|
|
13445
|
+
*
|
|
13446
|
+
* // Export report to file
|
|
13447
|
+
* await Partial.dump("BTCUSDT"); // Saves to ./dump/partial/BTCUSDT.md
|
|
13448
|
+
* await Partial.dump("BTCUSDT", "./custom/path"); // Custom directory
|
|
13449
|
+
* ```
|
|
13450
|
+
*/
|
|
13451
|
+
class PartialUtils {
|
|
13452
|
+
constructor() {
|
|
13453
|
+
/**
|
|
13454
|
+
* Retrieves statistical data from accumulated partial profit/loss events.
|
|
13455
|
+
*
|
|
13456
|
+
* Delegates to PartialMarkdownService.getData() which reads from ReportStorage.
|
|
13457
|
+
* Returns aggregated metrics calculated from all profit and loss events.
|
|
13458
|
+
*
|
|
13459
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13460
|
+
* @returns Promise resolving to PartialStatistics object with counts and event list
|
|
13461
|
+
*
|
|
13462
|
+
* @example
|
|
13463
|
+
* ```typescript
|
|
13464
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13465
|
+
*
|
|
13466
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
13467
|
+
* console.log(`Profit events: ${stats.totalProfit} (${(stats.totalProfit / stats.totalEvents * 100).toFixed(1)}%)`);
|
|
13468
|
+
* console.log(`Loss events: ${stats.totalLoss} (${(stats.totalLoss / stats.totalEvents * 100).toFixed(1)}%)`);
|
|
13469
|
+
*
|
|
13470
|
+
* // Iterate through all events
|
|
13471
|
+
* for (const event of stats.eventList) {
|
|
13472
|
+
* console.log(`${event.action.toUpperCase()}: Signal ${event.signalId} reached ${event.level}%`);
|
|
13473
|
+
* }
|
|
13474
|
+
* ```
|
|
13475
|
+
*/
|
|
13476
|
+
this.getData = async (symbol) => {
|
|
13477
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol });
|
|
13478
|
+
return await backtest$1.partialMarkdownService.getData(symbol);
|
|
13479
|
+
};
|
|
13480
|
+
/**
|
|
13481
|
+
* Generates markdown report with all partial profit/loss events for a symbol.
|
|
13482
|
+
*
|
|
13483
|
+
* Creates formatted table containing:
|
|
13484
|
+
* - Action (PROFIT/LOSS)
|
|
13485
|
+
* - Symbol
|
|
13486
|
+
* - Signal ID
|
|
13487
|
+
* - Position (LONG/SHORT)
|
|
13488
|
+
* - Level % (+10%, -20%, etc)
|
|
13489
|
+
* - Current Price
|
|
13490
|
+
* - Timestamp (ISO 8601)
|
|
13491
|
+
* - Mode (Backtest/Live)
|
|
13492
|
+
*
|
|
13493
|
+
* Also includes summary statistics at the end.
|
|
13494
|
+
*
|
|
13495
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13496
|
+
* @returns Promise resolving to markdown formatted report string
|
|
13497
|
+
*
|
|
13498
|
+
* @example
|
|
13499
|
+
* ```typescript
|
|
13500
|
+
* const markdown = await Partial.getReport("BTCUSDT");
|
|
13501
|
+
* console.log(markdown);
|
|
13502
|
+
*
|
|
13503
|
+
* // Output:
|
|
13504
|
+
* // # Partial Profit/Loss Report: BTCUSDT
|
|
13505
|
+
* //
|
|
13506
|
+
* // | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
|
|
13507
|
+
* // | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
13508
|
+
* // | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
|
|
13509
|
+
* // | LOSS | BTCUSDT | abc123 | LONG | -10% | 49000.00000000 USD | 2024-01-15T11:00:00.000Z | Backtest |
|
|
13510
|
+
* //
|
|
13511
|
+
* // **Total events:** 2
|
|
13512
|
+
* // **Profit events:** 1
|
|
13513
|
+
* // **Loss events:** 1
|
|
13514
|
+
* ```
|
|
13515
|
+
*/
|
|
13516
|
+
this.getReport = async (symbol) => {
|
|
13517
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol });
|
|
13518
|
+
return await backtest$1.partialMarkdownService.getReport(symbol);
|
|
13519
|
+
};
|
|
13520
|
+
/**
|
|
13521
|
+
* Generates and saves markdown report to file.
|
|
13522
|
+
*
|
|
13523
|
+
* Creates directory if it doesn't exist.
|
|
13524
|
+
* Filename format: {symbol}.md (e.g., "BTCUSDT.md")
|
|
13525
|
+
*
|
|
13526
|
+
* Delegates to PartialMarkdownService.dump() which:
|
|
13527
|
+
* 1. Generates markdown report via getReport()
|
|
13528
|
+
* 2. Creates output directory (recursive mkdir)
|
|
13529
|
+
* 3. Writes file with UTF-8 encoding
|
|
13530
|
+
* 4. Logs success/failure to console
|
|
13531
|
+
*
|
|
13532
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13533
|
+
* @param path - Output directory path (default: "./dump/partial")
|
|
13534
|
+
* @returns Promise that resolves when file is written
|
|
13535
|
+
*
|
|
13536
|
+
* @example
|
|
13537
|
+
* ```typescript
|
|
13538
|
+
* // Save to default path: ./dump/partial/BTCUSDT.md
|
|
13539
|
+
* await Partial.dump("BTCUSDT");
|
|
13540
|
+
*
|
|
13541
|
+
* // Save to custom path: ./reports/partial/BTCUSDT.md
|
|
13542
|
+
* await Partial.dump("BTCUSDT", "./reports/partial");
|
|
13543
|
+
*
|
|
13544
|
+
* // After multiple symbols backtested, export all reports
|
|
13545
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
|
|
13546
|
+
* await Partial.dump(symbol, "./backtest-results");
|
|
13547
|
+
* }
|
|
13548
|
+
* ```
|
|
13549
|
+
*/
|
|
13550
|
+
this.dump = async (symbol, path) => {
|
|
13551
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, path });
|
|
13552
|
+
await backtest$1.partialMarkdownService.dump(symbol, path);
|
|
13553
|
+
};
|
|
13554
|
+
}
|
|
13555
|
+
}
|
|
13556
|
+
/**
|
|
13557
|
+
* Global singleton instance of PartialUtils.
|
|
13558
|
+
* Provides static-like access to partial profit/loss reporting methods.
|
|
13559
|
+
*
|
|
13560
|
+
* @example
|
|
13561
|
+
* ```typescript
|
|
13562
|
+
* import { Partial } from "backtest-kit";
|
|
13563
|
+
*
|
|
13564
|
+
* // Usage same as PartialUtils methods
|
|
13565
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13566
|
+
* const report = await Partial.getReport("BTCUSDT");
|
|
13567
|
+
* await Partial.dump("BTCUSDT");
|
|
13568
|
+
* ```
|
|
13569
|
+
*/
|
|
13570
|
+
const Partial = new PartialUtils();
|
|
13571
|
+
|
|
13572
|
+
export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };
|