backtest-kit 1.3.2 → 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 +3349 -183
- package/build/index.mjs +3339 -184
- package/package.json +3 -2
- package/types.d.ts +2260 -122
package/build/index.cjs
CHANGED
|
@@ -118,6 +118,8 @@ const connectionServices$1 = {
|
|
|
118
118
|
frameConnectionService: Symbol('frameConnectionService'),
|
|
119
119
|
sizingConnectionService: Symbol('sizingConnectionService'),
|
|
120
120
|
riskConnectionService: Symbol('riskConnectionService'),
|
|
121
|
+
optimizerConnectionService: Symbol('optimizerConnectionService'),
|
|
122
|
+
partialConnectionService: Symbol('partialConnectionService'),
|
|
121
123
|
};
|
|
122
124
|
const schemaServices$1 = {
|
|
123
125
|
exchangeSchemaService: Symbol('exchangeSchemaService'),
|
|
@@ -126,6 +128,7 @@ const schemaServices$1 = {
|
|
|
126
128
|
walkerSchemaService: Symbol('walkerSchemaService'),
|
|
127
129
|
sizingSchemaService: Symbol('sizingSchemaService'),
|
|
128
130
|
riskSchemaService: Symbol('riskSchemaService'),
|
|
131
|
+
optimizerSchemaService: Symbol('optimizerSchemaService'),
|
|
129
132
|
};
|
|
130
133
|
const globalServices$1 = {
|
|
131
134
|
exchangeGlobalService: Symbol('exchangeGlobalService'),
|
|
@@ -133,6 +136,8 @@ const globalServices$1 = {
|
|
|
133
136
|
frameGlobalService: Symbol('frameGlobalService'),
|
|
134
137
|
sizingGlobalService: Symbol('sizingGlobalService'),
|
|
135
138
|
riskGlobalService: Symbol('riskGlobalService'),
|
|
139
|
+
optimizerGlobalService: Symbol('optimizerGlobalService'),
|
|
140
|
+
partialGlobalService: Symbol('partialGlobalService'),
|
|
136
141
|
};
|
|
137
142
|
const commandServices$1 = {
|
|
138
143
|
liveCommandService: Symbol('liveCommandService'),
|
|
@@ -156,6 +161,7 @@ const markdownServices$1 = {
|
|
|
156
161
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
157
162
|
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
158
163
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
164
|
+
partialMarkdownService: Symbol('partialMarkdownService'),
|
|
159
165
|
};
|
|
160
166
|
const validationServices$1 = {
|
|
161
167
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -164,6 +170,10 @@ const validationServices$1 = {
|
|
|
164
170
|
walkerValidationService: Symbol('walkerValidationService'),
|
|
165
171
|
sizingValidationService: Symbol('sizingValidationService'),
|
|
166
172
|
riskValidationService: Symbol('riskValidationService'),
|
|
173
|
+
optimizerValidationService: Symbol('optimizerValidationService'),
|
|
174
|
+
};
|
|
175
|
+
const templateServices$1 = {
|
|
176
|
+
optimizerTemplateService: Symbol('optimizerTemplateService'),
|
|
167
177
|
};
|
|
168
178
|
const TYPES = {
|
|
169
179
|
...baseServices$1,
|
|
@@ -176,6 +186,7 @@ const TYPES = {
|
|
|
176
186
|
...logicPublicServices$1,
|
|
177
187
|
...markdownServices$1,
|
|
178
188
|
...validationServices$1,
|
|
189
|
+
...templateServices$1,
|
|
179
190
|
};
|
|
180
191
|
|
|
181
192
|
/**
|
|
@@ -877,6 +888,12 @@ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
|
877
888
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
878
889
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
879
890
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
891
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
|
|
892
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
|
|
893
|
+
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
|
|
894
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
|
|
895
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
|
|
896
|
+
const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
|
|
880
897
|
const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
|
|
881
898
|
const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
|
|
882
899
|
const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
|
|
@@ -942,7 +959,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
|
|
|
942
959
|
* Creates new persistence instance.
|
|
943
960
|
*
|
|
944
961
|
* @param entityName - Unique entity type identifier
|
|
945
|
-
* @param baseDir - Base directory for all entities (default: ./
|
|
962
|
+
* @param baseDir - Base directory for all entities (default: ./dump/data)
|
|
946
963
|
*/
|
|
947
964
|
constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
|
|
948
965
|
this.entityName = entityName;
|
|
@@ -1201,7 +1218,7 @@ class PersistSignalUtils {
|
|
|
1201
1218
|
this.PersistSignalFactory = PersistBase;
|
|
1202
1219
|
this.getSignalStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
|
|
1203
1220
|
strategyName,
|
|
1204
|
-
`./
|
|
1221
|
+
`./dump/data/signal/`,
|
|
1205
1222
|
]));
|
|
1206
1223
|
/**
|
|
1207
1224
|
* Reads persisted signal data for a strategy and symbol.
|
|
@@ -1297,7 +1314,7 @@ class PersistRiskUtils {
|
|
|
1297
1314
|
this.PersistRiskFactory = PersistBase;
|
|
1298
1315
|
this.getRiskStorage = functoolsKit.memoize(([riskName]) => `${riskName}`, (riskName) => Reflect.construct(this.PersistRiskFactory, [
|
|
1299
1316
|
riskName,
|
|
1300
|
-
`./
|
|
1317
|
+
`./dump/data/risk/`,
|
|
1301
1318
|
]));
|
|
1302
1319
|
/**
|
|
1303
1320
|
* Reads persisted active positions for a risk profile.
|
|
@@ -1374,6 +1391,192 @@ class PersistRiskUtils {
|
|
|
1374
1391
|
* ```
|
|
1375
1392
|
*/
|
|
1376
1393
|
const PersistRiskAdapter = new PersistRiskUtils();
|
|
1394
|
+
/**
|
|
1395
|
+
* Utility class for managing scheduled signal persistence.
|
|
1396
|
+
*
|
|
1397
|
+
* Features:
|
|
1398
|
+
* - Memoized storage instances per strategy
|
|
1399
|
+
* - Custom adapter support
|
|
1400
|
+
* - Atomic read/write operations for scheduled signals
|
|
1401
|
+
* - Crash-safe scheduled signal state management
|
|
1402
|
+
*
|
|
1403
|
+
* Used by ClientStrategy for live mode persistence of scheduled signals (_scheduledSignal).
|
|
1404
|
+
*/
|
|
1405
|
+
class PersistScheduleUtils {
|
|
1406
|
+
constructor() {
|
|
1407
|
+
this.PersistScheduleFactory = PersistBase;
|
|
1408
|
+
this.getScheduleStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistScheduleFactory, [
|
|
1409
|
+
strategyName,
|
|
1410
|
+
`./dump/data/schedule/`,
|
|
1411
|
+
]));
|
|
1412
|
+
/**
|
|
1413
|
+
* Reads persisted scheduled signal data for a strategy and symbol.
|
|
1414
|
+
*
|
|
1415
|
+
* Called by ClientStrategy.waitForInit() to restore scheduled signal state.
|
|
1416
|
+
* Returns null if no scheduled signal exists.
|
|
1417
|
+
*
|
|
1418
|
+
* @param strategyName - Strategy identifier
|
|
1419
|
+
* @param symbol - Trading pair symbol
|
|
1420
|
+
* @returns Promise resolving to scheduled signal or null
|
|
1421
|
+
*/
|
|
1422
|
+
this.readScheduleData = async (strategyName, symbol) => {
|
|
1423
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
|
|
1424
|
+
const isInitial = !this.getScheduleStorage.has(strategyName);
|
|
1425
|
+
const stateStorage = this.getScheduleStorage(strategyName);
|
|
1426
|
+
await stateStorage.waitForInit(isInitial);
|
|
1427
|
+
if (await stateStorage.hasValue(symbol)) {
|
|
1428
|
+
return await stateStorage.readValue(symbol);
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
};
|
|
1432
|
+
/**
|
|
1433
|
+
* Writes scheduled signal data to disk with atomic file writes.
|
|
1434
|
+
*
|
|
1435
|
+
* Called by ClientStrategy.setScheduledSignal() to persist state.
|
|
1436
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1437
|
+
*
|
|
1438
|
+
* @param scheduledSignalRow - Scheduled signal data (null to clear)
|
|
1439
|
+
* @param strategyName - Strategy identifier
|
|
1440
|
+
* @param symbol - Trading pair symbol
|
|
1441
|
+
* @returns Promise that resolves when write is complete
|
|
1442
|
+
*/
|
|
1443
|
+
this.writeScheduleData = async (scheduledSignalRow, strategyName, symbol) => {
|
|
1444
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1445
|
+
const isInitial = !this.getScheduleStorage.has(strategyName);
|
|
1446
|
+
const stateStorage = this.getScheduleStorage(strategyName);
|
|
1447
|
+
await stateStorage.waitForInit(isInitial);
|
|
1448
|
+
await stateStorage.writeValue(symbol, scheduledSignalRow);
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Registers a custom persistence adapter.
|
|
1453
|
+
*
|
|
1454
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* ```typescript
|
|
1458
|
+
* class RedisPersist extends PersistBase {
|
|
1459
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1460
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1461
|
+
* }
|
|
1462
|
+
* PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
|
|
1463
|
+
* ```
|
|
1464
|
+
*/
|
|
1465
|
+
usePersistScheduleAdapter(Ctor) {
|
|
1466
|
+
backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
|
|
1467
|
+
this.PersistScheduleFactory = Ctor;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Global singleton instance of PersistScheduleUtils.
|
|
1472
|
+
* Used by ClientStrategy for scheduled signal persistence.
|
|
1473
|
+
*
|
|
1474
|
+
* @example
|
|
1475
|
+
* ```typescript
|
|
1476
|
+
* // Custom adapter
|
|
1477
|
+
* PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
|
|
1478
|
+
*
|
|
1479
|
+
* // Read scheduled signal
|
|
1480
|
+
* const scheduled = await PersistScheduleAdapter.readScheduleData("my-strategy", "BTCUSDT");
|
|
1481
|
+
*
|
|
1482
|
+
* // Write scheduled signal
|
|
1483
|
+
* await PersistScheduleAdapter.writeScheduleData(scheduled, "my-strategy", "BTCUSDT");
|
|
1484
|
+
* ```
|
|
1485
|
+
*/
|
|
1486
|
+
const PersistScheduleAdapter = new PersistScheduleUtils();
|
|
1487
|
+
/**
|
|
1488
|
+
* Utility class for managing partial profit/loss levels persistence.
|
|
1489
|
+
*
|
|
1490
|
+
* Features:
|
|
1491
|
+
* - Memoized storage instances per symbol
|
|
1492
|
+
* - Custom adapter support
|
|
1493
|
+
* - Atomic read/write operations for partial data
|
|
1494
|
+
* - Crash-safe partial state management
|
|
1495
|
+
*
|
|
1496
|
+
* Used by ClientPartial for live mode persistence of profit/loss levels.
|
|
1497
|
+
*/
|
|
1498
|
+
class PersistPartialUtils {
|
|
1499
|
+
constructor() {
|
|
1500
|
+
this.PersistPartialFactory = PersistBase;
|
|
1501
|
+
this.getPartialStorage = functoolsKit.memoize(([symbol]) => `${symbol}`, (symbol) => Reflect.construct(this.PersistPartialFactory, [
|
|
1502
|
+
symbol,
|
|
1503
|
+
`./dump/data/partial/`,
|
|
1504
|
+
]));
|
|
1505
|
+
/**
|
|
1506
|
+
* Reads persisted partial data for a symbol.
|
|
1507
|
+
*
|
|
1508
|
+
* Called by ClientPartial.waitForInit() to restore state.
|
|
1509
|
+
* Returns empty object if no partial data exists.
|
|
1510
|
+
*
|
|
1511
|
+
* @param symbol - Trading pair symbol
|
|
1512
|
+
* @returns Promise resolving to partial data record
|
|
1513
|
+
*/
|
|
1514
|
+
this.readPartialData = async (symbol) => {
|
|
1515
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
|
|
1516
|
+
const isInitial = !this.getPartialStorage.has(symbol);
|
|
1517
|
+
const stateStorage = this.getPartialStorage(symbol);
|
|
1518
|
+
await stateStorage.waitForInit(isInitial);
|
|
1519
|
+
const PARTIAL_STORAGE_KEY = "levels";
|
|
1520
|
+
if (await stateStorage.hasValue(PARTIAL_STORAGE_KEY)) {
|
|
1521
|
+
return await stateStorage.readValue(PARTIAL_STORAGE_KEY);
|
|
1522
|
+
}
|
|
1523
|
+
return {};
|
|
1524
|
+
};
|
|
1525
|
+
/**
|
|
1526
|
+
* Writes partial data to disk with atomic file writes.
|
|
1527
|
+
*
|
|
1528
|
+
* Called by ClientPartial after profit/loss level changes to persist state.
|
|
1529
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1530
|
+
*
|
|
1531
|
+
* @param partialData - Record of signal IDs to partial data
|
|
1532
|
+
* @param symbol - Trading pair symbol
|
|
1533
|
+
* @returns Promise that resolves when write is complete
|
|
1534
|
+
*/
|
|
1535
|
+
this.writePartialData = async (partialData, symbol) => {
|
|
1536
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1537
|
+
const isInitial = !this.getPartialStorage.has(symbol);
|
|
1538
|
+
const stateStorage = this.getPartialStorage(symbol);
|
|
1539
|
+
await stateStorage.waitForInit(isInitial);
|
|
1540
|
+
const PARTIAL_STORAGE_KEY = "levels";
|
|
1541
|
+
await stateStorage.writeValue(PARTIAL_STORAGE_KEY, partialData);
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Registers a custom persistence adapter.
|
|
1546
|
+
*
|
|
1547
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1548
|
+
*
|
|
1549
|
+
* @example
|
|
1550
|
+
* ```typescript
|
|
1551
|
+
* class RedisPersist extends PersistBase {
|
|
1552
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1553
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1554
|
+
* }
|
|
1555
|
+
* PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
|
|
1556
|
+
* ```
|
|
1557
|
+
*/
|
|
1558
|
+
usePersistPartialAdapter(Ctor) {
|
|
1559
|
+
backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
|
|
1560
|
+
this.PersistPartialFactory = Ctor;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Global singleton instance of PersistPartialUtils.
|
|
1565
|
+
* Used by ClientPartial for partial profit/loss levels persistence.
|
|
1566
|
+
*
|
|
1567
|
+
* @example
|
|
1568
|
+
* ```typescript
|
|
1569
|
+
* // Custom adapter
|
|
1570
|
+
* PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
|
|
1571
|
+
*
|
|
1572
|
+
* // Read partial data
|
|
1573
|
+
* const partialData = await PersistPartialAdapter.readPartialData("BTCUSDT");
|
|
1574
|
+
*
|
|
1575
|
+
* // Write partial data
|
|
1576
|
+
* await PersistPartialAdapter.writePartialData(partialData, "BTCUSDT");
|
|
1577
|
+
* ```
|
|
1578
|
+
*/
|
|
1579
|
+
const PersistPartialAdapter = new PersistPartialUtils();
|
|
1377
1580
|
|
|
1378
1581
|
/**
|
|
1379
1582
|
* Global signal emitter for all trading events (live + backtest).
|
|
@@ -1414,7 +1617,12 @@ const doneWalkerSubject = new functoolsKit.Subject();
|
|
|
1414
1617
|
* Progress emitter for backtest execution progress.
|
|
1415
1618
|
* Emits progress updates during backtest execution.
|
|
1416
1619
|
*/
|
|
1417
|
-
const
|
|
1620
|
+
const progressBacktestEmitter = new functoolsKit.Subject();
|
|
1621
|
+
/**
|
|
1622
|
+
* Progress emitter for walker execution progress.
|
|
1623
|
+
* Emits progress updates during walker execution.
|
|
1624
|
+
*/
|
|
1625
|
+
const progressWalkerEmitter = new functoolsKit.Subject();
|
|
1418
1626
|
/**
|
|
1419
1627
|
* Performance emitter for execution metrics.
|
|
1420
1628
|
* Emits performance metrics for profiling and bottleneck detection.
|
|
@@ -1440,6 +1648,16 @@ const walkerStopSubject = new functoolsKit.Subject();
|
|
|
1440
1648
|
* Emits when risk validation functions throw errors during signal checking.
|
|
1441
1649
|
*/
|
|
1442
1650
|
const validationSubject = new functoolsKit.Subject();
|
|
1651
|
+
/**
|
|
1652
|
+
* Partial profit emitter for profit level milestones.
|
|
1653
|
+
* Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
|
|
1654
|
+
*/
|
|
1655
|
+
const partialProfitSubject = new functoolsKit.Subject();
|
|
1656
|
+
/**
|
|
1657
|
+
* Partial loss emitter for loss level milestones.
|
|
1658
|
+
* Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
|
|
1659
|
+
*/
|
|
1660
|
+
const partialLossSubject = new functoolsKit.Subject();
|
|
1443
1661
|
|
|
1444
1662
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
1445
1663
|
__proto__: null,
|
|
@@ -1447,8 +1665,11 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
1447
1665
|
doneLiveSubject: doneLiveSubject,
|
|
1448
1666
|
doneWalkerSubject: doneWalkerSubject,
|
|
1449
1667
|
errorEmitter: errorEmitter,
|
|
1668
|
+
partialLossSubject: partialLossSubject,
|
|
1669
|
+
partialProfitSubject: partialProfitSubject,
|
|
1450
1670
|
performanceEmitter: performanceEmitter,
|
|
1451
|
-
|
|
1671
|
+
progressBacktestEmitter: progressBacktestEmitter,
|
|
1672
|
+
progressWalkerEmitter: progressWalkerEmitter,
|
|
1452
1673
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
1453
1674
|
signalEmitter: signalEmitter,
|
|
1454
1675
|
signalLiveEmitter: signalLiveEmitter,
|
|
@@ -1468,6 +1689,25 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1468
1689
|
};
|
|
1469
1690
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
1470
1691
|
const errors = [];
|
|
1692
|
+
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
1693
|
+
if (signal.id === undefined || signal.id === null || signal.id === '') {
|
|
1694
|
+
errors.push('id is required and must be a non-empty string');
|
|
1695
|
+
}
|
|
1696
|
+
if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
|
|
1697
|
+
errors.push('exchangeName is required');
|
|
1698
|
+
}
|
|
1699
|
+
if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
|
|
1700
|
+
errors.push('strategyName is required');
|
|
1701
|
+
}
|
|
1702
|
+
if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
|
|
1703
|
+
errors.push('symbol is required and must be a non-empty string');
|
|
1704
|
+
}
|
|
1705
|
+
if (signal._isScheduled === undefined || signal._isScheduled === null) {
|
|
1706
|
+
errors.push('_isScheduled is required');
|
|
1707
|
+
}
|
|
1708
|
+
if (signal.position === undefined || signal.position === null) {
|
|
1709
|
+
errors.push('position is required and must be "long" or "short"');
|
|
1710
|
+
}
|
|
1471
1711
|
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
1472
1712
|
if (!isFinite(currentPrice)) {
|
|
1473
1713
|
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
@@ -1717,26 +1957,42 @@ const GET_AVG_PRICE_FN = (candles) => {
|
|
|
1717
1957
|
? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
|
|
1718
1958
|
: sumPriceVolume / totalVolume;
|
|
1719
1959
|
};
|
|
1720
|
-
const WAIT_FOR_INIT_FN$
|
|
1960
|
+
const WAIT_FOR_INIT_FN$2 = async (self) => {
|
|
1721
1961
|
self.params.logger.debug("ClientStrategy waitForInit");
|
|
1722
1962
|
if (self.params.execution.context.backtest) {
|
|
1723
1963
|
return;
|
|
1724
1964
|
}
|
|
1965
|
+
// Restore pending signal
|
|
1725
1966
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1967
|
+
if (pendingSignal) {
|
|
1968
|
+
if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
self._pendingSignal = pendingSignal;
|
|
1975
|
+
// Call onActive callback for restored signal
|
|
1976
|
+
if (self.params.callbacks?.onActive) {
|
|
1977
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1978
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
|
|
1979
|
+
}
|
|
1734
1980
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
if (
|
|
1738
|
-
|
|
1739
|
-
|
|
1981
|
+
// Restore scheduled signal
|
|
1982
|
+
const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1983
|
+
if (scheduledSignal) {
|
|
1984
|
+
if (scheduledSignal.exchangeName !== self.params.method.context.exchangeName) {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (scheduledSignal.strategyName !== self.params.method.context.strategyName) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
self._scheduledSignal = scheduledSignal;
|
|
1991
|
+
// Call onSchedule callback for restored scheduled signal
|
|
1992
|
+
if (self.params.callbacks?.onSchedule) {
|
|
1993
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1994
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
|
|
1995
|
+
}
|
|
1740
1996
|
}
|
|
1741
1997
|
};
|
|
1742
1998
|
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
@@ -1753,7 +2009,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
|
|
|
1753
2009
|
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
1754
2010
|
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
1755
2011
|
});
|
|
1756
|
-
self.
|
|
2012
|
+
await self.setScheduledSignal(null);
|
|
1757
2013
|
if (self.params.callbacks?.onCancel) {
|
|
1758
2014
|
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
1759
2015
|
}
|
|
@@ -1808,7 +2064,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
|
|
|
1808
2064
|
averagePrice: currentPrice,
|
|
1809
2065
|
priceStopLoss: scheduled.priceStopLoss,
|
|
1810
2066
|
});
|
|
1811
|
-
self.
|
|
2067
|
+
await self.setScheduledSignal(null);
|
|
1812
2068
|
const result = {
|
|
1813
2069
|
action: "idle",
|
|
1814
2070
|
signal: null,
|
|
@@ -1829,7 +2085,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
1829
2085
|
symbol: self.params.execution.context.symbol,
|
|
1830
2086
|
signalId: scheduled.id,
|
|
1831
2087
|
});
|
|
1832
|
-
self.
|
|
2088
|
+
await self.setScheduledSignal(null);
|
|
1833
2089
|
return null;
|
|
1834
2090
|
}
|
|
1835
2091
|
// В LIVE режиме activationTimestamp - это текущее время при tick()
|
|
@@ -1857,10 +2113,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
1857
2113
|
symbol: self.params.execution.context.symbol,
|
|
1858
2114
|
signalId: scheduled.id,
|
|
1859
2115
|
});
|
|
1860
|
-
self.
|
|
2116
|
+
await self.setScheduledSignal(null);
|
|
1861
2117
|
return null;
|
|
1862
2118
|
}
|
|
1863
|
-
self.
|
|
2119
|
+
await self.setScheduledSignal(null);
|
|
1864
2120
|
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
1865
2121
|
const activatedSignal = {
|
|
1866
2122
|
...scheduled,
|
|
@@ -1998,6 +2254,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
1998
2254
|
if (self.params.callbacks?.onClose) {
|
|
1999
2255
|
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
2000
2256
|
}
|
|
2257
|
+
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2258
|
+
await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
|
|
2001
2259
|
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
2002
2260
|
strategyName: self.params.method.context.strategyName,
|
|
2003
2261
|
riskName: self.params.riskName,
|
|
@@ -2020,6 +2278,34 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2020
2278
|
return result;
|
|
2021
2279
|
};
|
|
2022
2280
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2281
|
+
// Calculate revenue percentage for partial fill/loss callbacks
|
|
2282
|
+
{
|
|
2283
|
+
let revenuePercent = 0;
|
|
2284
|
+
if (signal.position === "long") {
|
|
2285
|
+
// For long: positive if current > open, negative if current < open
|
|
2286
|
+
revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
|
|
2287
|
+
}
|
|
2288
|
+
else if (signal.position === "short") {
|
|
2289
|
+
// For short: positive if current < open, negative if current > open
|
|
2290
|
+
revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
|
|
2291
|
+
}
|
|
2292
|
+
// Call onPartialProfit if revenue is positive (but not reached TP yet)
|
|
2293
|
+
if (revenuePercent > 0) {
|
|
2294
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2295
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2296
|
+
if (self.params.callbacks?.onPartialProfit) {
|
|
2297
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
// Call onPartialLoss if revenue is negative (but not hit SL yet)
|
|
2301
|
+
if (revenuePercent < 0) {
|
|
2302
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2303
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2304
|
+
if (self.params.callbacks?.onPartialLoss) {
|
|
2305
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2023
2309
|
const result = {
|
|
2024
2310
|
action: "active",
|
|
2025
2311
|
signal: signal,
|
|
@@ -2058,7 +2344,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
2058
2344
|
averagePrice,
|
|
2059
2345
|
priceStopLoss: scheduled.priceStopLoss,
|
|
2060
2346
|
});
|
|
2061
|
-
self.
|
|
2347
|
+
await self.setScheduledSignal(null);
|
|
2062
2348
|
if (self.params.callbacks?.onCancel) {
|
|
2063
2349
|
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
2064
2350
|
}
|
|
@@ -2083,7 +2369,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2083
2369
|
symbol: self.params.execution.context.symbol,
|
|
2084
2370
|
signalId: scheduled.id,
|
|
2085
2371
|
});
|
|
2086
|
-
self.
|
|
2372
|
+
await self.setScheduledSignal(null);
|
|
2087
2373
|
return false;
|
|
2088
2374
|
}
|
|
2089
2375
|
// В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
|
|
@@ -2108,10 +2394,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
2108
2394
|
symbol: self.params.execution.context.symbol,
|
|
2109
2395
|
signalId: scheduled.id,
|
|
2110
2396
|
});
|
|
2111
|
-
self.
|
|
2397
|
+
await self.setScheduledSignal(null);
|
|
2112
2398
|
return false;
|
|
2113
2399
|
}
|
|
2114
|
-
self.
|
|
2400
|
+
await self.setScheduledSignal(null);
|
|
2115
2401
|
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
|
|
2116
2402
|
const activatedSignal = {
|
|
2117
2403
|
...scheduled,
|
|
@@ -2147,6 +2433,8 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2147
2433
|
if (self.params.callbacks?.onClose) {
|
|
2148
2434
|
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
2149
2435
|
}
|
|
2436
|
+
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
2437
|
+
await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
|
|
2150
2438
|
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
2151
2439
|
strategyName: self.params.method.context.strategyName,
|
|
2152
2440
|
riskName: self.params.riskName,
|
|
@@ -2288,6 +2576,33 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2288
2576
|
}
|
|
2289
2577
|
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
2290
2578
|
}
|
|
2579
|
+
// Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
|
|
2580
|
+
// Calculate revenue percentage
|
|
2581
|
+
{
|
|
2582
|
+
let revenuePercent = 0;
|
|
2583
|
+
if (signal.position === "long") {
|
|
2584
|
+
revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
|
|
2585
|
+
}
|
|
2586
|
+
else if (signal.position === "short") {
|
|
2587
|
+
revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
|
|
2588
|
+
}
|
|
2589
|
+
// Call onPartialProfit if revenue is positive (but not reached TP yet)
|
|
2590
|
+
if (revenuePercent > 0) {
|
|
2591
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2592
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2593
|
+
if (self.params.callbacks?.onPartialProfit) {
|
|
2594
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
// Call onPartialLoss if revenue is negative (but not hit SL yet)
|
|
2598
|
+
if (revenuePercent < 0) {
|
|
2599
|
+
// КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
|
|
2600
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2601
|
+
if (self.params.callbacks?.onPartialLoss) {
|
|
2602
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2291
2606
|
}
|
|
2292
2607
|
return null;
|
|
2293
2608
|
};
|
|
@@ -2334,7 +2649,7 @@ class ClientStrategy {
|
|
|
2334
2649
|
*
|
|
2335
2650
|
* @returns Promise that resolves when initialization is complete
|
|
2336
2651
|
*/
|
|
2337
|
-
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$
|
|
2652
|
+
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$2(this));
|
|
2338
2653
|
}
|
|
2339
2654
|
/**
|
|
2340
2655
|
* Updates pending signal and persists to disk in live mode.
|
|
@@ -2360,6 +2675,25 @@ class ClientStrategy {
|
|
|
2360
2675
|
}
|
|
2361
2676
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2362
2677
|
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Updates scheduled signal and persists to disk in live mode.
|
|
2680
|
+
*
|
|
2681
|
+
* Centralized method for all scheduled signal state changes.
|
|
2682
|
+
* Uses atomic file writes to prevent corruption.
|
|
2683
|
+
*
|
|
2684
|
+
* @param scheduledSignal - New scheduled signal state (null to clear)
|
|
2685
|
+
* @returns Promise that resolves when update is complete
|
|
2686
|
+
*/
|
|
2687
|
+
async setScheduledSignal(scheduledSignal) {
|
|
2688
|
+
this.params.logger.debug("ClientStrategy setScheduledSignal", {
|
|
2689
|
+
scheduledSignal,
|
|
2690
|
+
});
|
|
2691
|
+
this._scheduledSignal = scheduledSignal;
|
|
2692
|
+
if (this.params.execution.context.backtest) {
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
2696
|
+
}
|
|
2363
2697
|
/**
|
|
2364
2698
|
* Retrieves the current pending signal.
|
|
2365
2699
|
* If no signal is pending, returns null.
|
|
@@ -2434,7 +2768,7 @@ class ClientStrategy {
|
|
|
2434
2768
|
const signal = await GET_SIGNAL_FN(this);
|
|
2435
2769
|
if (signal) {
|
|
2436
2770
|
if (signal._isScheduled === true) {
|
|
2437
|
-
this.
|
|
2771
|
+
await this.setScheduledSignal(signal);
|
|
2438
2772
|
return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
|
|
2439
2773
|
}
|
|
2440
2774
|
await this.setPendingSignal(signal);
|
|
@@ -2610,7 +2944,7 @@ class ClientStrategy {
|
|
|
2610
2944
|
this._isStopped = true;
|
|
2611
2945
|
// Clear scheduled signal if exists
|
|
2612
2946
|
if (this._scheduledSignal) {
|
|
2613
|
-
this.
|
|
2947
|
+
await this.setScheduledSignal(null);
|
|
2614
2948
|
}
|
|
2615
2949
|
}
|
|
2616
2950
|
}
|
|
@@ -2649,6 +2983,7 @@ class StrategyConnectionService {
|
|
|
2649
2983
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
2650
2984
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
2651
2985
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
2986
|
+
this.partialConnectionService = inject(TYPES.partialConnectionService);
|
|
2652
2987
|
/**
|
|
2653
2988
|
* Retrieves memoized ClientStrategy instance for given strategy name.
|
|
2654
2989
|
*
|
|
@@ -2665,6 +3000,7 @@ class StrategyConnectionService {
|
|
|
2665
3000
|
execution: this.executionContextService,
|
|
2666
3001
|
method: this.methodContextService,
|
|
2667
3002
|
logger: this.loggerService,
|
|
3003
|
+
partial: this.partialConnectionService,
|
|
2668
3004
|
exchange: this.exchangeConnectionService,
|
|
2669
3005
|
risk: riskName ? this.riskConnectionService.getRisk(riskName) : NOOP_RISK,
|
|
2670
3006
|
riskName,
|
|
@@ -3151,7 +3487,7 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
|
|
|
3151
3487
|
* Uses singleshot pattern to ensure it only runs once.
|
|
3152
3488
|
* This function is exported for use in tests or other modules.
|
|
3153
3489
|
*/
|
|
3154
|
-
const WAIT_FOR_INIT_FN = async (self) => {
|
|
3490
|
+
const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
3155
3491
|
self.params.logger.debug("ClientRisk waitForInit");
|
|
3156
3492
|
const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName);
|
|
3157
3493
|
self._activePositions = new Map(persistedPositions);
|
|
@@ -3182,7 +3518,7 @@ class ClientRisk {
|
|
|
3182
3518
|
* Uses singleshot pattern to ensure initialization happens exactly once.
|
|
3183
3519
|
* Skips persistence in backtest mode.
|
|
3184
3520
|
*/
|
|
3185
|
-
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN(this));
|
|
3521
|
+
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
|
|
3186
3522
|
/**
|
|
3187
3523
|
* Checks if a signal should be allowed based on risk limits.
|
|
3188
3524
|
*
|
|
@@ -4357,7 +4693,7 @@ class BacktestLogicPrivateService {
|
|
|
4357
4693
|
const when = timeframes[i];
|
|
4358
4694
|
// Emit progress event if context is available
|
|
4359
4695
|
{
|
|
4360
|
-
await
|
|
4696
|
+
await progressBacktestEmitter.next({
|
|
4361
4697
|
exchangeName: this.methodContextService.context.exchangeName,
|
|
4362
4698
|
strategyName: this.methodContextService.context.strategyName,
|
|
4363
4699
|
symbol,
|
|
@@ -4491,7 +4827,7 @@ class BacktestLogicPrivateService {
|
|
|
4491
4827
|
}
|
|
4492
4828
|
// Emit final progress event (100%)
|
|
4493
4829
|
{
|
|
4494
|
-
await
|
|
4830
|
+
await progressBacktestEmitter.next({
|
|
4495
4831
|
exchangeName: this.methodContextService.context.exchangeName,
|
|
4496
4832
|
strategyName: this.methodContextService.context.strategyName,
|
|
4497
4833
|
symbol,
|
|
@@ -4732,6 +5068,16 @@ class WalkerLogicPrivateService {
|
|
|
4732
5068
|
strategiesTested,
|
|
4733
5069
|
totalStrategies: strategies.length,
|
|
4734
5070
|
};
|
|
5071
|
+
// Emit progress event
|
|
5072
|
+
await progressWalkerEmitter.next({
|
|
5073
|
+
walkerName: context.walkerName,
|
|
5074
|
+
exchangeName: context.exchangeName,
|
|
5075
|
+
frameName: context.frameName,
|
|
5076
|
+
symbol,
|
|
5077
|
+
totalStrategies: strategies.length,
|
|
5078
|
+
processedStrategies: strategiesTested,
|
|
5079
|
+
progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
|
|
5080
|
+
});
|
|
4735
5081
|
// Call onStrategyComplete callback if provided
|
|
4736
5082
|
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
4737
5083
|
walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
@@ -5077,7 +5423,7 @@ function isUnsafe$3(value) {
|
|
|
5077
5423
|
}
|
|
5078
5424
|
return false;
|
|
5079
5425
|
}
|
|
5080
|
-
const columns$
|
|
5426
|
+
const columns$4 = [
|
|
5081
5427
|
{
|
|
5082
5428
|
key: "signalId",
|
|
5083
5429
|
label: "Signal ID",
|
|
@@ -5155,7 +5501,7 @@ const columns$3 = [
|
|
|
5155
5501
|
* Storage class for accumulating closed signals per strategy.
|
|
5156
5502
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
5157
5503
|
*/
|
|
5158
|
-
let ReportStorage$
|
|
5504
|
+
let ReportStorage$4 = class ReportStorage {
|
|
5159
5505
|
constructor() {
|
|
5160
5506
|
/** Internal list of all closed signals for this strategy */
|
|
5161
5507
|
this._signalList = [];
|
|
@@ -5245,9 +5591,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
5245
5591
|
if (stats.totalSignals === 0) {
|
|
5246
5592
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
5247
5593
|
}
|
|
5248
|
-
const header = columns$
|
|
5249
|
-
const separator = columns$
|
|
5250
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
5594
|
+
const header = columns$4.map((col) => col.label);
|
|
5595
|
+
const separator = columns$4.map(() => "---");
|
|
5596
|
+
const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
|
|
5251
5597
|
const tableData = [header, separator, ...rows];
|
|
5252
5598
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5253
5599
|
return functoolsKit.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)`}`);
|
|
@@ -5256,9 +5602,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
5256
5602
|
* Saves strategy report to disk.
|
|
5257
5603
|
*
|
|
5258
5604
|
* @param strategyName - Strategy name
|
|
5259
|
-
* @param path - Directory path to save report (default: "./
|
|
5605
|
+
* @param path - Directory path to save report (default: "./dump/backtest")
|
|
5260
5606
|
*/
|
|
5261
|
-
async dump(strategyName, path$1 = "./
|
|
5607
|
+
async dump(strategyName, path$1 = "./dump/backtest") {
|
|
5262
5608
|
const markdown = await this.getReport(strategyName);
|
|
5263
5609
|
try {
|
|
5264
5610
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -5308,7 +5654,7 @@ class BacktestMarkdownService {
|
|
|
5308
5654
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
5309
5655
|
* Each strategy gets its own isolated storage instance.
|
|
5310
5656
|
*/
|
|
5311
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
5657
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$4());
|
|
5312
5658
|
/**
|
|
5313
5659
|
* Processes tick events and accumulates closed signals.
|
|
5314
5660
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -5386,20 +5732,20 @@ class BacktestMarkdownService {
|
|
|
5386
5732
|
* Delegates to ReportStorage.dump().
|
|
5387
5733
|
*
|
|
5388
5734
|
* @param strategyName - Strategy name to save report for
|
|
5389
|
-
* @param path - Directory path to save report (default: "./
|
|
5735
|
+
* @param path - Directory path to save report (default: "./dump/backtest")
|
|
5390
5736
|
*
|
|
5391
5737
|
* @example
|
|
5392
5738
|
* ```typescript
|
|
5393
5739
|
* const service = new BacktestMarkdownService();
|
|
5394
5740
|
*
|
|
5395
|
-
* // Save to default path: ./
|
|
5741
|
+
* // Save to default path: ./dump/backtest/my-strategy.md
|
|
5396
5742
|
* await service.dump("my-strategy");
|
|
5397
5743
|
*
|
|
5398
5744
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5399
5745
|
* await service.dump("my-strategy", "./custom/path");
|
|
5400
5746
|
* ```
|
|
5401
5747
|
*/
|
|
5402
|
-
this.dump = async (strategyName, path = "./
|
|
5748
|
+
this.dump = async (strategyName, path = "./dump/backtest") => {
|
|
5403
5749
|
this.loggerService.log("backtestMarkdownService dump", {
|
|
5404
5750
|
strategyName,
|
|
5405
5751
|
path,
|
|
@@ -5467,7 +5813,7 @@ function isUnsafe$2(value) {
|
|
|
5467
5813
|
}
|
|
5468
5814
|
return false;
|
|
5469
5815
|
}
|
|
5470
|
-
const columns$
|
|
5816
|
+
const columns$3 = [
|
|
5471
5817
|
{
|
|
5472
5818
|
key: "timestamp",
|
|
5473
5819
|
label: "Timestamp",
|
|
@@ -5541,12 +5887,12 @@ const columns$2 = [
|
|
|
5541
5887
|
},
|
|
5542
5888
|
];
|
|
5543
5889
|
/** Maximum number of events to store in live trading reports */
|
|
5544
|
-
const MAX_EVENTS$
|
|
5890
|
+
const MAX_EVENTS$3 = 250;
|
|
5545
5891
|
/**
|
|
5546
5892
|
* Storage class for accumulating all tick events per strategy.
|
|
5547
5893
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
5548
5894
|
*/
|
|
5549
|
-
let ReportStorage$
|
|
5895
|
+
let ReportStorage$3 = class ReportStorage {
|
|
5550
5896
|
constructor() {
|
|
5551
5897
|
/** Internal list of all tick events for this strategy */
|
|
5552
5898
|
this._eventList = [];
|
|
@@ -5574,7 +5920,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5574
5920
|
}
|
|
5575
5921
|
{
|
|
5576
5922
|
this._eventList.push(newEvent);
|
|
5577
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5923
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5578
5924
|
this._eventList.shift();
|
|
5579
5925
|
}
|
|
5580
5926
|
}
|
|
@@ -5598,7 +5944,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5598
5944
|
stopLoss: data.signal.priceStopLoss,
|
|
5599
5945
|
});
|
|
5600
5946
|
// Trim queue if exceeded MAX_EVENTS
|
|
5601
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5947
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5602
5948
|
this._eventList.shift();
|
|
5603
5949
|
}
|
|
5604
5950
|
}
|
|
@@ -5630,7 +5976,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5630
5976
|
else {
|
|
5631
5977
|
this._eventList.push(newEvent);
|
|
5632
5978
|
// Trim queue if exceeded MAX_EVENTS
|
|
5633
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5979
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5634
5980
|
this._eventList.shift();
|
|
5635
5981
|
}
|
|
5636
5982
|
}
|
|
@@ -5668,7 +6014,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5668
6014
|
else {
|
|
5669
6015
|
this._eventList.push(newEvent);
|
|
5670
6016
|
// Trim queue if exceeded MAX_EVENTS
|
|
5671
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6017
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
5672
6018
|
this._eventList.shift();
|
|
5673
6019
|
}
|
|
5674
6020
|
}
|
|
@@ -5765,9 +6111,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5765
6111
|
if (stats.totalEvents === 0) {
|
|
5766
6112
|
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
5767
6113
|
}
|
|
5768
|
-
const header = columns$
|
|
5769
|
-
const separator = columns$
|
|
5770
|
-
const rows = this._eventList.map((event) => columns$
|
|
6114
|
+
const header = columns$3.map((col) => col.label);
|
|
6115
|
+
const separator = columns$3.map(() => "---");
|
|
6116
|
+
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
5771
6117
|
const tableData = [header, separator, ...rows];
|
|
5772
6118
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5773
6119
|
return functoolsKit.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)`}`);
|
|
@@ -5776,9 +6122,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
5776
6122
|
* Saves strategy report to disk.
|
|
5777
6123
|
*
|
|
5778
6124
|
* @param strategyName - Strategy name
|
|
5779
|
-
* @param path - Directory path to save report (default: "./
|
|
6125
|
+
* @param path - Directory path to save report (default: "./dump/live")
|
|
5780
6126
|
*/
|
|
5781
|
-
async dump(strategyName, path$1 = "./
|
|
6127
|
+
async dump(strategyName, path$1 = "./dump/live") {
|
|
5782
6128
|
const markdown = await this.getReport(strategyName);
|
|
5783
6129
|
try {
|
|
5784
6130
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -5831,7 +6177,7 @@ class LiveMarkdownService {
|
|
|
5831
6177
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
5832
6178
|
* Each strategy gets its own isolated storage instance.
|
|
5833
6179
|
*/
|
|
5834
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
6180
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
|
|
5835
6181
|
/**
|
|
5836
6182
|
* Processes tick events and accumulates all event types.
|
|
5837
6183
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -5919,20 +6265,20 @@ class LiveMarkdownService {
|
|
|
5919
6265
|
* Delegates to ReportStorage.dump().
|
|
5920
6266
|
*
|
|
5921
6267
|
* @param strategyName - Strategy name to save report for
|
|
5922
|
-
* @param path - Directory path to save report (default: "./
|
|
6268
|
+
* @param path - Directory path to save report (default: "./dump/live")
|
|
5923
6269
|
*
|
|
5924
6270
|
* @example
|
|
5925
6271
|
* ```typescript
|
|
5926
6272
|
* const service = new LiveMarkdownService();
|
|
5927
6273
|
*
|
|
5928
|
-
* // Save to default path: ./
|
|
6274
|
+
* // Save to default path: ./dump/live/my-strategy.md
|
|
5929
6275
|
* await service.dump("my-strategy");
|
|
5930
6276
|
*
|
|
5931
6277
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5932
6278
|
* await service.dump("my-strategy", "./custom/path");
|
|
5933
6279
|
* ```
|
|
5934
6280
|
*/
|
|
5935
|
-
this.dump = async (strategyName, path = "./
|
|
6281
|
+
this.dump = async (strategyName, path = "./dump/live") => {
|
|
5936
6282
|
this.loggerService.log("liveMarkdownService dump", {
|
|
5937
6283
|
strategyName,
|
|
5938
6284
|
path,
|
|
@@ -5982,7 +6328,7 @@ class LiveMarkdownService {
|
|
|
5982
6328
|
}
|
|
5983
6329
|
}
|
|
5984
6330
|
|
|
5985
|
-
const columns$
|
|
6331
|
+
const columns$2 = [
|
|
5986
6332
|
{
|
|
5987
6333
|
key: "timestamp",
|
|
5988
6334
|
label: "Timestamp",
|
|
@@ -6040,12 +6386,12 @@ const columns$1 = [
|
|
|
6040
6386
|
},
|
|
6041
6387
|
];
|
|
6042
6388
|
/** Maximum number of events to store in schedule reports */
|
|
6043
|
-
const MAX_EVENTS$
|
|
6389
|
+
const MAX_EVENTS$2 = 250;
|
|
6044
6390
|
/**
|
|
6045
6391
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
6046
6392
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
6047
6393
|
*/
|
|
6048
|
-
let ReportStorage$
|
|
6394
|
+
let ReportStorage$2 = class ReportStorage {
|
|
6049
6395
|
constructor() {
|
|
6050
6396
|
/** Internal list of all scheduled events for this strategy */
|
|
6051
6397
|
this._eventList = [];
|
|
@@ -6069,7 +6415,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6069
6415
|
stopLoss: data.signal.priceStopLoss,
|
|
6070
6416
|
});
|
|
6071
6417
|
// Trim queue if exceeded MAX_EVENTS
|
|
6072
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6418
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
6073
6419
|
this._eventList.shift();
|
|
6074
6420
|
}
|
|
6075
6421
|
}
|
|
@@ -6105,7 +6451,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6105
6451
|
else {
|
|
6106
6452
|
this._eventList.push(newEvent);
|
|
6107
6453
|
// Trim queue if exceeded MAX_EVENTS
|
|
6108
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6454
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
6109
6455
|
this._eventList.shift();
|
|
6110
6456
|
}
|
|
6111
6457
|
}
|
|
@@ -6157,9 +6503,9 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6157
6503
|
if (stats.totalEvents === 0) {
|
|
6158
6504
|
return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
|
|
6159
6505
|
}
|
|
6160
|
-
const header = columns$
|
|
6161
|
-
const separator = columns$
|
|
6162
|
-
const rows = this._eventList.map((event) => columns$
|
|
6506
|
+
const header = columns$2.map((col) => col.label);
|
|
6507
|
+
const separator = columns$2.map(() => "---");
|
|
6508
|
+
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
6163
6509
|
const tableData = [header, separator, ...rows];
|
|
6164
6510
|
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
6165
6511
|
return functoolsKit.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`}`);
|
|
@@ -6168,9 +6514,9 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
6168
6514
|
* Saves strategy report to disk.
|
|
6169
6515
|
*
|
|
6170
6516
|
* @param strategyName - Strategy name
|
|
6171
|
-
* @param path - Directory path to save report (default: "./
|
|
6517
|
+
* @param path - Directory path to save report (default: "./dump/schedule")
|
|
6172
6518
|
*/
|
|
6173
|
-
async dump(strategyName, path$1 = "./
|
|
6519
|
+
async dump(strategyName, path$1 = "./dump/schedule") {
|
|
6174
6520
|
const markdown = await this.getReport(strategyName);
|
|
6175
6521
|
try {
|
|
6176
6522
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -6214,7 +6560,7 @@ class ScheduleMarkdownService {
|
|
|
6214
6560
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
6215
6561
|
* Each strategy gets its own isolated storage instance.
|
|
6216
6562
|
*/
|
|
6217
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
6563
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
6218
6564
|
/**
|
|
6219
6565
|
* Processes tick events and accumulates scheduled/cancelled events.
|
|
6220
6566
|
* Should be called from signalLiveEmitter subscription.
|
|
@@ -6289,20 +6635,20 @@ class ScheduleMarkdownService {
|
|
|
6289
6635
|
* Delegates to ReportStorage.dump().
|
|
6290
6636
|
*
|
|
6291
6637
|
* @param strategyName - Strategy name to save report for
|
|
6292
|
-
* @param path - Directory path to save report (default: "./
|
|
6638
|
+
* @param path - Directory path to save report (default: "./dump/schedule")
|
|
6293
6639
|
*
|
|
6294
6640
|
* @example
|
|
6295
6641
|
* ```typescript
|
|
6296
6642
|
* const service = new ScheduleMarkdownService();
|
|
6297
6643
|
*
|
|
6298
|
-
* // Save to default path: ./
|
|
6644
|
+
* // Save to default path: ./dump/schedule/my-strategy.md
|
|
6299
6645
|
* await service.dump("my-strategy");
|
|
6300
6646
|
*
|
|
6301
6647
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
6302
6648
|
* await service.dump("my-strategy", "./custom/path");
|
|
6303
6649
|
* ```
|
|
6304
6650
|
*/
|
|
6305
|
-
this.dump = async (strategyName, path = "./
|
|
6651
|
+
this.dump = async (strategyName, path = "./dump/schedule") => {
|
|
6306
6652
|
this.loggerService.log("scheduleMarkdownService dump", {
|
|
6307
6653
|
strategyName,
|
|
6308
6654
|
path,
|
|
@@ -6362,7 +6708,7 @@ function percentile(sortedArray, p) {
|
|
|
6362
6708
|
return sortedArray[Math.max(0, index)];
|
|
6363
6709
|
}
|
|
6364
6710
|
/** Maximum number of performance events to store per strategy */
|
|
6365
|
-
const MAX_EVENTS = 10000;
|
|
6711
|
+
const MAX_EVENTS$1 = 10000;
|
|
6366
6712
|
/**
|
|
6367
6713
|
* Storage class for accumulating performance metrics per strategy.
|
|
6368
6714
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -6380,7 +6726,7 @@ class PerformanceStorage {
|
|
|
6380
6726
|
addEvent(event) {
|
|
6381
6727
|
this._events.push(event);
|
|
6382
6728
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
6383
|
-
if (this._events.length > MAX_EVENTS) {
|
|
6729
|
+
if (this._events.length > MAX_EVENTS$1) {
|
|
6384
6730
|
this._events.shift();
|
|
6385
6731
|
}
|
|
6386
6732
|
}
|
|
@@ -6519,7 +6865,7 @@ class PerformanceStorage {
|
|
|
6519
6865
|
* @param strategyName - Strategy name
|
|
6520
6866
|
* @param path - Directory path to save report
|
|
6521
6867
|
*/
|
|
6522
|
-
async dump(strategyName, path$1 = "./
|
|
6868
|
+
async dump(strategyName, path$1 = "./dump/performance") {
|
|
6523
6869
|
const markdown = await this.getReport(strategyName);
|
|
6524
6870
|
try {
|
|
6525
6871
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -6632,14 +6978,14 @@ class PerformanceMarkdownService {
|
|
|
6632
6978
|
*
|
|
6633
6979
|
* @example
|
|
6634
6980
|
* ```typescript
|
|
6635
|
-
* // Save to default path: ./
|
|
6981
|
+
* // Save to default path: ./dump/performance/my-strategy.md
|
|
6636
6982
|
* await performanceService.dump("my-strategy");
|
|
6637
6983
|
*
|
|
6638
6984
|
* // Save to custom path
|
|
6639
6985
|
* await performanceService.dump("my-strategy", "./custom/path");
|
|
6640
6986
|
* ```
|
|
6641
6987
|
*/
|
|
6642
|
-
this.dump = async (strategyName, path = "./
|
|
6988
|
+
this.dump = async (strategyName, path = "./dump/performance") => {
|
|
6643
6989
|
this.loggerService.log("performanceMarkdownService dump", {
|
|
6644
6990
|
strategyName,
|
|
6645
6991
|
path,
|
|
@@ -6701,7 +7047,7 @@ function formatMetric(value) {
|
|
|
6701
7047
|
* Storage class for accumulating walker results.
|
|
6702
7048
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
6703
7049
|
*/
|
|
6704
|
-
class ReportStorage {
|
|
7050
|
+
let ReportStorage$1 = class ReportStorage {
|
|
6705
7051
|
constructor(walkerName) {
|
|
6706
7052
|
this.walkerName = walkerName;
|
|
6707
7053
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -6769,9 +7115,9 @@ class ReportStorage {
|
|
|
6769
7115
|
* @param symbol - Trading symbol
|
|
6770
7116
|
* @param metric - Metric being optimized
|
|
6771
7117
|
* @param context - Context with exchangeName and frameName
|
|
6772
|
-
* @param path - Directory path to save report (default: "./
|
|
7118
|
+
* @param path - Directory path to save report (default: "./dump/walker")
|
|
6773
7119
|
*/
|
|
6774
|
-
async dump(symbol, metric, context, path$1 = "./
|
|
7120
|
+
async dump(symbol, metric, context, path$1 = "./dump/walker") {
|
|
6775
7121
|
const markdown = await this.getReport(symbol, metric, context);
|
|
6776
7122
|
try {
|
|
6777
7123
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -6785,7 +7131,7 @@ class ReportStorage {
|
|
|
6785
7131
|
console.error(`Failed to save walker report:`, error);
|
|
6786
7132
|
}
|
|
6787
7133
|
}
|
|
6788
|
-
}
|
|
7134
|
+
};
|
|
6789
7135
|
/**
|
|
6790
7136
|
* Service for generating and saving walker markdown reports.
|
|
6791
7137
|
*
|
|
@@ -6810,7 +7156,7 @@ class WalkerMarkdownService {
|
|
|
6810
7156
|
* Memoized function to get or create ReportStorage for a walker.
|
|
6811
7157
|
* Each walker gets its own isolated storage instance.
|
|
6812
7158
|
*/
|
|
6813
|
-
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage(walkerName));
|
|
7159
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
|
|
6814
7160
|
/**
|
|
6815
7161
|
* Processes walker progress events and accumulates strategy results.
|
|
6816
7162
|
* Should be called from walkerEmitter.
|
|
@@ -6893,20 +7239,20 @@ class WalkerMarkdownService {
|
|
|
6893
7239
|
* @param symbol - Trading symbol
|
|
6894
7240
|
* @param metric - Metric being optimized
|
|
6895
7241
|
* @param context - Context with exchangeName and frameName
|
|
6896
|
-
* @param path - Directory path to save report (default: "./
|
|
7242
|
+
* @param path - Directory path to save report (default: "./dump/walker")
|
|
6897
7243
|
*
|
|
6898
7244
|
* @example
|
|
6899
7245
|
* ```typescript
|
|
6900
7246
|
* const service = new WalkerMarkdownService();
|
|
6901
7247
|
*
|
|
6902
|
-
* // Save to default path: ./
|
|
7248
|
+
* // Save to default path: ./dump/walker/my-walker.md
|
|
6903
7249
|
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
|
|
6904
7250
|
*
|
|
6905
7251
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
6906
7252
|
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" }, "./custom/path");
|
|
6907
7253
|
* ```
|
|
6908
7254
|
*/
|
|
6909
|
-
this.dump = async (walkerName, symbol, metric, context, path = "./
|
|
7255
|
+
this.dump = async (walkerName, symbol, metric, context, path = "./dump/walker") => {
|
|
6910
7256
|
this.loggerService.log("walkerMarkdownService dump", {
|
|
6911
7257
|
walkerName,
|
|
6912
7258
|
symbol,
|
|
@@ -6981,7 +7327,7 @@ function isUnsafe(value) {
|
|
|
6981
7327
|
}
|
|
6982
7328
|
return false;
|
|
6983
7329
|
}
|
|
6984
|
-
const columns = [
|
|
7330
|
+
const columns$1 = [
|
|
6985
7331
|
{
|
|
6986
7332
|
key: "symbol",
|
|
6987
7333
|
label: "Symbol",
|
|
@@ -7277,9 +7623,9 @@ class HeatmapStorage {
|
|
|
7277
7623
|
if (data.symbols.length === 0) {
|
|
7278
7624
|
return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
|
|
7279
7625
|
}
|
|
7280
|
-
const header = columns.map((col) => col.label);
|
|
7281
|
-
const separator = columns.map(() => "---");
|
|
7282
|
-
const rows = data.symbols.map((row) => columns.map((col) => col.format(row)));
|
|
7626
|
+
const header = columns$1.map((col) => col.label);
|
|
7627
|
+
const separator = columns$1.map(() => "---");
|
|
7628
|
+
const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
|
|
7283
7629
|
const tableData = [header, separator, ...rows];
|
|
7284
7630
|
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
7285
7631
|
return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
|
|
@@ -7288,9 +7634,9 @@ class HeatmapStorage {
|
|
|
7288
7634
|
* Saves heatmap report to disk.
|
|
7289
7635
|
*
|
|
7290
7636
|
* @param strategyName - Strategy name for filename
|
|
7291
|
-
* @param path - Directory path to save report (default: "./
|
|
7637
|
+
* @param path - Directory path to save report (default: "./dump/heatmap")
|
|
7292
7638
|
*/
|
|
7293
|
-
async dump(strategyName, path$1 = "./
|
|
7639
|
+
async dump(strategyName, path$1 = "./dump/heatmap") {
|
|
7294
7640
|
const markdown = await this.getReport(strategyName);
|
|
7295
7641
|
try {
|
|
7296
7642
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -7421,20 +7767,20 @@ class HeatMarkdownService {
|
|
|
7421
7767
|
* Default filename: {strategyName}.md
|
|
7422
7768
|
*
|
|
7423
7769
|
* @param strategyName - Strategy name to save heatmap report for
|
|
7424
|
-
* @param path - Optional directory path to save report (default: "./
|
|
7770
|
+
* @param path - Optional directory path to save report (default: "./dump/heatmap")
|
|
7425
7771
|
*
|
|
7426
7772
|
* @example
|
|
7427
7773
|
* ```typescript
|
|
7428
7774
|
* const service = new HeatMarkdownService();
|
|
7429
7775
|
*
|
|
7430
|
-
* // Save to default path: ./
|
|
7776
|
+
* // Save to default path: ./dump/heatmap/my-strategy.md
|
|
7431
7777
|
* await service.dump("my-strategy");
|
|
7432
7778
|
*
|
|
7433
7779
|
* // Save to custom path: ./reports/my-strategy.md
|
|
7434
7780
|
* await service.dump("my-strategy", "./reports");
|
|
7435
7781
|
* ```
|
|
7436
7782
|
*/
|
|
7437
|
-
this.dump = async (strategyName, path = "./
|
|
7783
|
+
this.dump = async (strategyName, path = "./dump/heatmap") => {
|
|
7438
7784
|
this.loggerService.log(HEATMAP_METHOD_NAME_DUMP, {
|
|
7439
7785
|
strategyName,
|
|
7440
7786
|
path,
|
|
@@ -7866,47 +8212,2310 @@ class RiskValidationService {
|
|
|
7866
8212
|
}
|
|
7867
8213
|
}
|
|
7868
8214
|
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
{
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
{
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
{
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
8215
|
+
/**
|
|
8216
|
+
* Default template service for generating optimizer code snippets.
|
|
8217
|
+
* Implements all IOptimizerTemplate methods with Ollama LLM integration.
|
|
8218
|
+
*
|
|
8219
|
+
* Features:
|
|
8220
|
+
* - Multi-timeframe analysis (1m, 5m, 15m, 1h)
|
|
8221
|
+
* - JSON structured output for signals
|
|
8222
|
+
* - Debug logging to ./dump/strategy
|
|
8223
|
+
* - CCXT exchange integration
|
|
8224
|
+
* - Walker-based strategy comparison
|
|
8225
|
+
*
|
|
8226
|
+
* Can be partially overridden in optimizer schema configuration.
|
|
8227
|
+
*/
|
|
8228
|
+
class OptimizerTemplateService {
|
|
8229
|
+
constructor() {
|
|
8230
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8231
|
+
/**
|
|
8232
|
+
* Generates the top banner with imports and constants.
|
|
8233
|
+
*
|
|
8234
|
+
* @param symbol - Trading pair symbol
|
|
8235
|
+
* @returns Shebang, imports, and WARN_KB constant
|
|
8236
|
+
*/
|
|
8237
|
+
this.getTopBanner = async (symbol) => {
|
|
8238
|
+
this.loggerService.log("optimizerTemplateService getTopBanner", {
|
|
8239
|
+
symbol,
|
|
8240
|
+
});
|
|
8241
|
+
return [
|
|
8242
|
+
"#!/usr/bin/env node",
|
|
8243
|
+
"",
|
|
8244
|
+
`import { Ollama } from "ollama";`,
|
|
8245
|
+
`import ccxt from "ccxt";`,
|
|
8246
|
+
`import {`,
|
|
8247
|
+
` addExchange,`,
|
|
8248
|
+
` addStrategy,`,
|
|
8249
|
+
` addFrame,`,
|
|
8250
|
+
` addWalker,`,
|
|
8251
|
+
` Walker,`,
|
|
8252
|
+
` Backtest,`,
|
|
8253
|
+
` getCandles,`,
|
|
8254
|
+
` listenSignalBacktest,`,
|
|
8255
|
+
` listenWalkerComplete,`,
|
|
8256
|
+
` listenDoneBacktest,`,
|
|
8257
|
+
` listenBacktestProgress,`,
|
|
8258
|
+
` listenWalkerProgress,`,
|
|
8259
|
+
` listenError,`,
|
|
8260
|
+
`} from "backtest-kit";`,
|
|
8261
|
+
`import { promises as fs } from "fs";`,
|
|
8262
|
+
`import { v4 as uuid } from "uuid";`,
|
|
8263
|
+
`import path from "path";`,
|
|
8264
|
+
``,
|
|
8265
|
+
`const WARN_KB = 100;`
|
|
8266
|
+
].join("\n");
|
|
8267
|
+
};
|
|
8268
|
+
/**
|
|
8269
|
+
* Generates default user message for LLM conversation.
|
|
8270
|
+
* Simple prompt to read and acknowledge data.
|
|
8271
|
+
*
|
|
8272
|
+
* @param symbol - Trading pair symbol
|
|
8273
|
+
* @param data - Fetched data array
|
|
8274
|
+
* @param name - Source name
|
|
8275
|
+
* @returns User message with JSON data
|
|
8276
|
+
*/
|
|
8277
|
+
this.getUserMessage = async (symbol, data, name) => {
|
|
8278
|
+
this.loggerService.log("optimizerTemplateService getUserMessage", {
|
|
8279
|
+
symbol,
|
|
8280
|
+
data,
|
|
8281
|
+
name,
|
|
8282
|
+
});
|
|
8283
|
+
return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
|
|
8284
|
+
};
|
|
8285
|
+
/**
|
|
8286
|
+
* Generates default assistant message for LLM conversation.
|
|
8287
|
+
* Simple acknowledgment response.
|
|
8288
|
+
*
|
|
8289
|
+
* @param symbol - Trading pair symbol
|
|
8290
|
+
* @param data - Fetched data array
|
|
8291
|
+
* @param name - Source name
|
|
8292
|
+
* @returns Assistant acknowledgment message
|
|
8293
|
+
*/
|
|
8294
|
+
this.getAssistantMessage = async (symbol, data, name) => {
|
|
8295
|
+
this.loggerService.log("optimizerTemplateService getAssistantMessage", {
|
|
8296
|
+
symbol,
|
|
8297
|
+
data,
|
|
8298
|
+
name,
|
|
8299
|
+
});
|
|
8300
|
+
return "ОК";
|
|
8301
|
+
};
|
|
8302
|
+
/**
|
|
8303
|
+
* Generates Walker configuration code.
|
|
8304
|
+
* Compares multiple strategies on test frame.
|
|
8305
|
+
*
|
|
8306
|
+
* @param walkerName - Unique walker identifier
|
|
8307
|
+
* @param exchangeName - Exchange to use for backtesting
|
|
8308
|
+
* @param frameName - Test frame name
|
|
8309
|
+
* @param strategies - Array of strategy names to compare
|
|
8310
|
+
* @returns Generated addWalker() call
|
|
8311
|
+
*/
|
|
8312
|
+
this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
|
|
8313
|
+
this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
|
|
8314
|
+
walkerName,
|
|
8315
|
+
exchangeName,
|
|
8316
|
+
frameName,
|
|
8317
|
+
strategies,
|
|
8318
|
+
});
|
|
8319
|
+
// Escape special characters to prevent code injection
|
|
8320
|
+
const escapedWalkerName = String(walkerName)
|
|
8321
|
+
.replace(/\\/g, '\\\\')
|
|
8322
|
+
.replace(/"/g, '\\"');
|
|
8323
|
+
const escapedExchangeName = String(exchangeName)
|
|
8324
|
+
.replace(/\\/g, '\\\\')
|
|
8325
|
+
.replace(/"/g, '\\"');
|
|
8326
|
+
const escapedFrameName = String(frameName)
|
|
8327
|
+
.replace(/\\/g, '\\\\')
|
|
8328
|
+
.replace(/"/g, '\\"');
|
|
8329
|
+
const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
|
|
8330
|
+
return [
|
|
8331
|
+
`addWalker({`,
|
|
8332
|
+
` walkerName: "${escapedWalkerName}",`,
|
|
8333
|
+
` exchangeName: "${escapedExchangeName}",`,
|
|
8334
|
+
` frameName: "${escapedFrameName}",`,
|
|
8335
|
+
` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
|
|
8336
|
+
`});`
|
|
8337
|
+
].join("\n");
|
|
8338
|
+
};
|
|
8339
|
+
/**
|
|
8340
|
+
* Generates Strategy configuration with LLM integration.
|
|
8341
|
+
* Includes multi-timeframe analysis and signal generation.
|
|
8342
|
+
*
|
|
8343
|
+
* @param strategyName - Unique strategy identifier
|
|
8344
|
+
* @param interval - Signal throttling interval (e.g., "5m")
|
|
8345
|
+
* @param prompt - Strategy logic from getPrompt()
|
|
8346
|
+
* @returns Generated addStrategy() call with getSignal() function
|
|
8347
|
+
*/
|
|
8348
|
+
this.getStrategyTemplate = async (strategyName, interval, prompt) => {
|
|
8349
|
+
this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
|
|
8350
|
+
strategyName,
|
|
8351
|
+
interval,
|
|
8352
|
+
prompt,
|
|
8353
|
+
});
|
|
8354
|
+
// Escape special characters to prevent code injection
|
|
8355
|
+
const escapedStrategyName = String(strategyName)
|
|
8356
|
+
.replace(/\\/g, '\\\\')
|
|
8357
|
+
.replace(/"/g, '\\"');
|
|
8358
|
+
const escapedInterval = String(interval)
|
|
8359
|
+
.replace(/\\/g, '\\\\')
|
|
8360
|
+
.replace(/"/g, '\\"');
|
|
8361
|
+
const escapedPrompt = String(prompt)
|
|
8362
|
+
.replace(/\\/g, '\\\\')
|
|
8363
|
+
.replace(/`/g, '\\`')
|
|
8364
|
+
.replace(/\$/g, '\\$');
|
|
8365
|
+
return [
|
|
8366
|
+
`addStrategy({`,
|
|
8367
|
+
` strategyName: "${escapedStrategyName}",`,
|
|
8368
|
+
` interval: "${escapedInterval}",`,
|
|
8369
|
+
` getSignal: async (symbol) => {`,
|
|
8370
|
+
` const messages = [];`,
|
|
8371
|
+
``,
|
|
8372
|
+
` // Загружаем данные всех таймфреймов`,
|
|
8373
|
+
` const microTermCandles = await getCandles(symbol, "1m", 30);`,
|
|
8374
|
+
` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
|
|
8375
|
+
` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
|
|
8376
|
+
` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
|
|
8377
|
+
``,
|
|
8378
|
+
` function formatCandles(candles, timeframe) {`,
|
|
8379
|
+
` return candles.map((c) =>`,
|
|
8380
|
+
` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
|
|
8381
|
+
` ).join("\\n");`,
|
|
8382
|
+
` }`,
|
|
8383
|
+
``,
|
|
8384
|
+
` // Сообщение 1: Среднесрочный тренд`,
|
|
8385
|
+
` messages.push(`,
|
|
8386
|
+
` {`,
|
|
8387
|
+
` role: "user",`,
|
|
8388
|
+
` content: [`,
|
|
8389
|
+
` \`\${symbol}\`,`,
|
|
8390
|
+
` "Проанализируй свечи 1h:",`,
|
|
8391
|
+
` "",`,
|
|
8392
|
+
` formatCandles(mediumTermCandles, "1h")`,
|
|
8393
|
+
` ].join("\\n"),`,
|
|
8394
|
+
` },`,
|
|
8395
|
+
` {`,
|
|
8396
|
+
` role: "assistant",`,
|
|
8397
|
+
` content: "Тренд 1h проанализирован",`,
|
|
8398
|
+
` }`,
|
|
8399
|
+
` );`,
|
|
8400
|
+
``,
|
|
8401
|
+
` // Сообщение 2: Краткосрочный тренд`,
|
|
8402
|
+
` messages.push(`,
|
|
8403
|
+
` {`,
|
|
8404
|
+
` role: "user",`,
|
|
8405
|
+
` content: [`,
|
|
8406
|
+
` "Проанализируй свечи 15m:",`,
|
|
8407
|
+
` "",`,
|
|
8408
|
+
` formatCandles(shortTermCandles, "15m")`,
|
|
8409
|
+
` ].join("\\n"),`,
|
|
8410
|
+
` },`,
|
|
8411
|
+
` {`,
|
|
8412
|
+
` role: "assistant",`,
|
|
8413
|
+
` content: "Тренд 15m проанализирован",`,
|
|
8414
|
+
` }`,
|
|
8415
|
+
` );`,
|
|
8416
|
+
``,
|
|
8417
|
+
` // Сообщение 3: Основной таймфрейм`,
|
|
8418
|
+
` messages.push(`,
|
|
8419
|
+
` {`,
|
|
8420
|
+
` role: "user",`,
|
|
8421
|
+
` content: [`,
|
|
8422
|
+
` "Проанализируй свечи 5m:",`,
|
|
8423
|
+
` "",`,
|
|
8424
|
+
` formatCandles(mainTermCandles, "5m")`,
|
|
8425
|
+
` ].join("\\n")`,
|
|
8426
|
+
` },`,
|
|
8427
|
+
` {`,
|
|
8428
|
+
` role: "assistant",`,
|
|
8429
|
+
` content: "Таймфрейм 5m проанализирован",`,
|
|
8430
|
+
` }`,
|
|
8431
|
+
` );`,
|
|
8432
|
+
``,
|
|
8433
|
+
` // Сообщение 4: Микро-структура`,
|
|
8434
|
+
` messages.push(`,
|
|
8435
|
+
` {`,
|
|
8436
|
+
` role: "user",`,
|
|
8437
|
+
` content: [`,
|
|
8438
|
+
` "Проанализируй свечи 1m:",`,
|
|
8439
|
+
` "",`,
|
|
8440
|
+
` formatCandles(microTermCandles, "1m")`,
|
|
8441
|
+
` ].join("\\n")`,
|
|
8442
|
+
` },`,
|
|
8443
|
+
` {`,
|
|
8444
|
+
` role: "assistant",`,
|
|
8445
|
+
` content: "Микроструктура 1m проанализирована",`,
|
|
8446
|
+
` }`,
|
|
8447
|
+
` );`,
|
|
8448
|
+
``,
|
|
8449
|
+
` // Сообщение 5: Запрос сигнала`,
|
|
8450
|
+
` messages.push(`,
|
|
8451
|
+
` {`,
|
|
8452
|
+
` role: "user",`,
|
|
8453
|
+
` content: [`,
|
|
8454
|
+
` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
|
|
8455
|
+
` "",`,
|
|
8456
|
+
` \`${escapedPrompt}\`,`,
|
|
8457
|
+
` "",`,
|
|
8458
|
+
` "Если сигналы противоречивы или тренд слабый то position: wait"`,
|
|
8459
|
+
` ].join("\\n"),`,
|
|
8460
|
+
` }`,
|
|
8461
|
+
` );`,
|
|
8462
|
+
``,
|
|
8463
|
+
` const resultId = uuid();`,
|
|
8464
|
+
``,
|
|
8465
|
+
` const result = await json(messages);`,
|
|
8466
|
+
``,
|
|
8467
|
+
` await dumpJson(resultId, messages, result);`,
|
|
8468
|
+
``,
|
|
8469
|
+
` return result;`,
|
|
8470
|
+
` },`,
|
|
8471
|
+
`});`
|
|
8472
|
+
].join("\n");
|
|
8473
|
+
};
|
|
8474
|
+
/**
|
|
8475
|
+
* Generates Exchange configuration code.
|
|
8476
|
+
* Uses CCXT Binance with standard formatters.
|
|
8477
|
+
*
|
|
8478
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8479
|
+
* @param exchangeName - Unique exchange identifier
|
|
8480
|
+
* @returns Generated addExchange() call with CCXT integration
|
|
8481
|
+
*/
|
|
8482
|
+
this.getExchangeTemplate = async (symbol, exchangeName) => {
|
|
8483
|
+
this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
|
|
8484
|
+
exchangeName,
|
|
8485
|
+
symbol,
|
|
8486
|
+
});
|
|
8487
|
+
// Escape special characters to prevent code injection
|
|
8488
|
+
const escapedExchangeName = String(exchangeName)
|
|
8489
|
+
.replace(/\\/g, '\\\\')
|
|
8490
|
+
.replace(/"/g, '\\"');
|
|
8491
|
+
return [
|
|
8492
|
+
`addExchange({`,
|
|
8493
|
+
` exchangeName: "${escapedExchangeName}",`,
|
|
8494
|
+
` getCandles: async (symbol, interval, since, limit) => {`,
|
|
8495
|
+
` const exchange = new ccxt.binance();`,
|
|
8496
|
+
` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
|
|
8497
|
+
` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
|
|
8498
|
+
` timestamp, open, high, low, close, volume`,
|
|
8499
|
+
` }));`,
|
|
8500
|
+
` },`,
|
|
8501
|
+
` formatPrice: async (symbol, price) => price.toFixed(2),`,
|
|
8502
|
+
` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
|
|
8503
|
+
`});`
|
|
8504
|
+
].join("\n");
|
|
8505
|
+
};
|
|
8506
|
+
/**
|
|
8507
|
+
* Generates Frame (timeframe) configuration code.
|
|
8508
|
+
*
|
|
8509
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8510
|
+
* @param frameName - Unique frame identifier
|
|
8511
|
+
* @param interval - Candle interval (e.g., "1m")
|
|
8512
|
+
* @param startDate - Frame start date
|
|
8513
|
+
* @param endDate - Frame end date
|
|
8514
|
+
* @returns Generated addFrame() call
|
|
8515
|
+
*/
|
|
8516
|
+
this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
|
|
8517
|
+
this.loggerService.log("optimizerTemplateService getFrameTemplate", {
|
|
8518
|
+
symbol,
|
|
8519
|
+
frameName,
|
|
8520
|
+
interval,
|
|
8521
|
+
startDate,
|
|
8522
|
+
endDate,
|
|
8523
|
+
});
|
|
8524
|
+
// Escape special characters to prevent code injection
|
|
8525
|
+
const escapedFrameName = String(frameName)
|
|
8526
|
+
.replace(/\\/g, '\\\\')
|
|
8527
|
+
.replace(/"/g, '\\"');
|
|
8528
|
+
const escapedInterval = String(interval)
|
|
8529
|
+
.replace(/\\/g, '\\\\')
|
|
8530
|
+
.replace(/"/g, '\\"');
|
|
8531
|
+
return [
|
|
8532
|
+
`addFrame({`,
|
|
8533
|
+
` frameName: "${escapedFrameName}",`,
|
|
8534
|
+
` interval: "${escapedInterval}",`,
|
|
8535
|
+
` startDate: new Date("${startDate.toISOString()}"),`,
|
|
8536
|
+
` endDate: new Date("${endDate.toISOString()}"),`,
|
|
8537
|
+
`});`
|
|
8538
|
+
].join("\n");
|
|
8539
|
+
};
|
|
8540
|
+
/**
|
|
8541
|
+
* Generates launcher code to run Walker with event listeners.
|
|
8542
|
+
* Includes progress tracking and completion handlers.
|
|
8543
|
+
*
|
|
8544
|
+
* @param symbol - Trading pair symbol
|
|
8545
|
+
* @param walkerName - Walker name to launch
|
|
8546
|
+
* @returns Generated Walker.background() call with listeners
|
|
8547
|
+
*/
|
|
8548
|
+
this.getLauncherTemplate = async (symbol, walkerName) => {
|
|
8549
|
+
this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
|
|
8550
|
+
symbol,
|
|
8551
|
+
walkerName,
|
|
8552
|
+
});
|
|
8553
|
+
// Escape special characters to prevent code injection
|
|
8554
|
+
const escapedSymbol = String(symbol)
|
|
8555
|
+
.replace(/\\/g, '\\\\')
|
|
8556
|
+
.replace(/"/g, '\\"');
|
|
8557
|
+
const escapedWalkerName = String(walkerName)
|
|
8558
|
+
.replace(/\\/g, '\\\\')
|
|
8559
|
+
.replace(/"/g, '\\"');
|
|
8560
|
+
return [
|
|
8561
|
+
`Walker.background("${escapedSymbol}", {`,
|
|
8562
|
+
` walkerName: "${escapedWalkerName}"`,
|
|
8563
|
+
`});`,
|
|
8564
|
+
``,
|
|
8565
|
+
`listenSignalBacktest((event) => {`,
|
|
8566
|
+
` console.log(event);`,
|
|
8567
|
+
`});`,
|
|
8568
|
+
``,
|
|
8569
|
+
`listenBacktestProgress((event) => {`,
|
|
8570
|
+
` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
|
|
8571
|
+
` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
|
|
8572
|
+
`});`,
|
|
8573
|
+
``,
|
|
8574
|
+
`listenWalkerProgress((event) => {`,
|
|
8575
|
+
` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
|
|
8576
|
+
` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
|
|
8577
|
+
` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
|
|
8578
|
+
`});`,
|
|
8579
|
+
``,
|
|
8580
|
+
`listenWalkerComplete((results) => {`,
|
|
8581
|
+
` console.log("Walker completed:", results.bestStrategy);`,
|
|
8582
|
+
` Walker.dump("${escapedSymbol}", results.walkerName);`,
|
|
8583
|
+
`});`,
|
|
8584
|
+
``,
|
|
8585
|
+
`listenDoneBacktest((event) => {`,
|
|
8586
|
+
` console.log("Backtest completed:", event.symbol);`,
|
|
8587
|
+
` Backtest.dump(event.strategyName);`,
|
|
8588
|
+
`});`,
|
|
8589
|
+
``,
|
|
8590
|
+
`listenError((error) => {`,
|
|
8591
|
+
` console.error("Error occurred:", error);`,
|
|
8592
|
+
`});`
|
|
8593
|
+
].join("\n");
|
|
8594
|
+
};
|
|
8595
|
+
/**
|
|
8596
|
+
* Generates dumpJson() helper function for debug output.
|
|
8597
|
+
* Saves LLM conversations and results to ./dump/strategy/{resultId}/
|
|
8598
|
+
*
|
|
8599
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8600
|
+
* @returns Generated async dumpJson() function
|
|
8601
|
+
*/
|
|
8602
|
+
this.getJsonDumpTemplate = async (symbol) => {
|
|
8603
|
+
this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
|
|
8604
|
+
symbol,
|
|
8605
|
+
});
|
|
8606
|
+
return [
|
|
8607
|
+
`async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
|
|
8608
|
+
` // Extract system messages and system reminders from existing data`,
|
|
8609
|
+
` const systemMessages = history.filter((m) => m.role === "system");`,
|
|
8610
|
+
` const userMessages = history.filter((m) => m.role === "user");`,
|
|
8611
|
+
` const subfolderPath = path.join(outputDir, resultId);`,
|
|
8612
|
+
``,
|
|
8613
|
+
` try {`,
|
|
8614
|
+
` await fs.access(subfolderPath);`,
|
|
8615
|
+
` return;`,
|
|
8616
|
+
` } catch {`,
|
|
8617
|
+
` await fs.mkdir(subfolderPath, { recursive: true });`,
|
|
8618
|
+
` }`,
|
|
8619
|
+
``,
|
|
8620
|
+
` {`,
|
|
8621
|
+
` let summary = "# Outline Result Summary\\n";`,
|
|
8622
|
+
``,
|
|
8623
|
+
` {`,
|
|
8624
|
+
` summary += "\\n";`,
|
|
8625
|
+
` summary += \`**ResultId**: \${resultId}\\n\`;`,
|
|
8626
|
+
` summary += "\\n";`,
|
|
8627
|
+
` }`,
|
|
8628
|
+
``,
|
|
8629
|
+
` if (result) {`,
|
|
8630
|
+
` summary += "## Output Data\\n\\n";`,
|
|
8631
|
+
` summary += "\`\`\`json\\n";`,
|
|
8632
|
+
` summary += JSON.stringify(result, null, 2);`,
|
|
8633
|
+
` summary += "\\n\`\`\`\\n\\n";`,
|
|
8634
|
+
` }`,
|
|
8635
|
+
``,
|
|
8636
|
+
` // Add system messages to summary`,
|
|
8637
|
+
` if (systemMessages.length > 0) {`,
|
|
8638
|
+
` summary += "## System Messages\\n\\n";`,
|
|
8639
|
+
` systemMessages.forEach((msg, idx) => {`,
|
|
8640
|
+
` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
|
|
8641
|
+
` summary += msg.content;`,
|
|
8642
|
+
` summary += "\\n";`,
|
|
8643
|
+
` });`,
|
|
8644
|
+
` }`,
|
|
8645
|
+
``,
|
|
8646
|
+
` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
|
|
8647
|
+
` await fs.writeFile(summaryFile, summary, "utf8");`,
|
|
8648
|
+
` }`,
|
|
8649
|
+
``,
|
|
8650
|
+
` {`,
|
|
8651
|
+
` await Promise.all(`,
|
|
8652
|
+
` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
|
|
8653
|
+
` const messageNum = String(idx + 1).padStart(2, "0");`,
|
|
8654
|
+
` const contentFileName = \`\${messageNum}_user_message.md\`;`,
|
|
8655
|
+
` const contentFilePath = path.join(subfolderPath, contentFileName);`,
|
|
8656
|
+
``,
|
|
8657
|
+
` {`,
|
|
8658
|
+
` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
|
|
8659
|
+
` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
|
|
8660
|
+
` if (messageSizeKb > WARN_KB) {`,
|
|
8661
|
+
` console.warn(`,
|
|
8662
|
+
` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
|
|
8663
|
+
` );`,
|
|
8664
|
+
` }`,
|
|
8665
|
+
` }`,
|
|
8666
|
+
``,
|
|
8667
|
+
` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
|
|
8668
|
+
` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
8669
|
+
` content += message.content;`,
|
|
8670
|
+
` content += "\\n";`,
|
|
8671
|
+
``,
|
|
8672
|
+
` await fs.writeFile(contentFilePath, content, "utf8");`,
|
|
8673
|
+
` })`,
|
|
8674
|
+
` );`,
|
|
8675
|
+
` }`,
|
|
8676
|
+
``,
|
|
8677
|
+
` {`,
|
|
8678
|
+
` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
|
|
8679
|
+
` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
|
|
8680
|
+
` const contentFilePath = path.join(subfolderPath, contentFileName);`,
|
|
8681
|
+
``,
|
|
8682
|
+
` let content = "# Full Outline Result\\n\\n";`,
|
|
8683
|
+
` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
|
|
8684
|
+
``,
|
|
8685
|
+
` if (result) {`,
|
|
8686
|
+
` content += "## Output Data\\n\\n";`,
|
|
8687
|
+
` content += "\`\`\`json\\n";`,
|
|
8688
|
+
` content += JSON.stringify(result, null, 2);`,
|
|
8689
|
+
` content += "\\n\`\`\`\\n";`,
|
|
8690
|
+
` }`,
|
|
8691
|
+
``,
|
|
8692
|
+
` await fs.writeFile(contentFilePath, content, "utf8");`,
|
|
8693
|
+
` }`,
|
|
8694
|
+
`}`
|
|
8695
|
+
].join("\n");
|
|
8696
|
+
};
|
|
8697
|
+
/**
|
|
8698
|
+
* Generates text() helper for LLM text generation.
|
|
8699
|
+
* Uses Ollama gpt-oss:20b model for market analysis.
|
|
8700
|
+
*
|
|
8701
|
+
* @param symbol - Trading pair symbol (used in prompt)
|
|
8702
|
+
* @returns Generated async text() function
|
|
8703
|
+
*/
|
|
8704
|
+
this.getTextTemplate = async (symbol) => {
|
|
8705
|
+
this.loggerService.log("optimizerTemplateService getTextTemplate", {
|
|
8706
|
+
symbol,
|
|
8707
|
+
});
|
|
8708
|
+
// Escape special characters in symbol to prevent code injection
|
|
8709
|
+
const escapedSymbol = String(symbol)
|
|
8710
|
+
.replace(/\\/g, '\\\\')
|
|
8711
|
+
.replace(/`/g, '\\`')
|
|
8712
|
+
.replace(/\$/g, '\\$')
|
|
8713
|
+
.toUpperCase();
|
|
8714
|
+
return [
|
|
8715
|
+
`async function text(messages) {`,
|
|
8716
|
+
` const ollama = new Ollama({`,
|
|
8717
|
+
` host: "https://ollama.com",`,
|
|
8718
|
+
` headers: {`,
|
|
8719
|
+
` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
|
|
8720
|
+
` },`,
|
|
8721
|
+
` });`,
|
|
8722
|
+
``,
|
|
8723
|
+
` const response = await ollama.chat({`,
|
|
8724
|
+
` model: "gpt-oss:20b",`,
|
|
8725
|
+
` messages: [`,
|
|
8726
|
+
` {`,
|
|
8727
|
+
` role: "system",`,
|
|
8728
|
+
` content: [`,
|
|
8729
|
+
` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
|
|
8730
|
+
` "только отчёт готовый для копипасты целиком",`,
|
|
8731
|
+
` "",`,
|
|
8732
|
+
` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
|
|
8733
|
+
` ].join("\\n"),`,
|
|
8734
|
+
` },`,
|
|
8735
|
+
` ...messages,`,
|
|
8736
|
+
` {`,
|
|
8737
|
+
` role: "user",`,
|
|
8738
|
+
` content: [`,
|
|
8739
|
+
` "На каких условиях мне купить ${escapedSymbol}?",`,
|
|
8740
|
+
` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
|
|
8741
|
+
` "Какой RR ставить для позиций?",`,
|
|
8742
|
+
` "Предпочтительны LONG или SHORT позиции?",`,
|
|
8743
|
+
` "",`,
|
|
8744
|
+
` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
|
|
8745
|
+
` ].join("\\n")`,
|
|
8746
|
+
` }`,
|
|
8747
|
+
` ]`,
|
|
8748
|
+
` });`,
|
|
8749
|
+
``,
|
|
8750
|
+
` const content = response.message.content.trim();`,
|
|
8751
|
+
` return content`,
|
|
8752
|
+
` .replace(/\\\\/g, '\\\\\\\\')`,
|
|
8753
|
+
` .replace(/\`/g, '\\\\\`')`,
|
|
8754
|
+
` .replace(/\\$/g, '\\\\$')`,
|
|
8755
|
+
` .replace(/"/g, '\\\\"')`,
|
|
8756
|
+
` .replace(/'/g, "\\\\'");`,
|
|
8757
|
+
`}`
|
|
8758
|
+
].join("\n");
|
|
8759
|
+
};
|
|
8760
|
+
/**
|
|
8761
|
+
* Generates json() helper for structured LLM output.
|
|
8762
|
+
* Uses Ollama with JSON schema for trading signals.
|
|
8763
|
+
*
|
|
8764
|
+
* Signal schema:
|
|
8765
|
+
* - position: "wait" | "long" | "short"
|
|
8766
|
+
* - note: strategy explanation
|
|
8767
|
+
* - priceOpen: entry price
|
|
8768
|
+
* - priceTakeProfit: target price
|
|
8769
|
+
* - priceStopLoss: stop price
|
|
8770
|
+
* - minuteEstimatedTime: expected duration (max 360 min)
|
|
8771
|
+
*
|
|
8772
|
+
* @param symbol - Trading pair symbol (unused, for consistency)
|
|
8773
|
+
* @returns Generated async json() function with signal schema
|
|
8774
|
+
*/
|
|
8775
|
+
this.getJsonTemplate = async (symbol) => {
|
|
8776
|
+
this.loggerService.log("optimizerTemplateService getJsonTemplate", {
|
|
8777
|
+
symbol,
|
|
8778
|
+
});
|
|
8779
|
+
return [
|
|
8780
|
+
`async function json(messages) {`,
|
|
8781
|
+
` const ollama = new Ollama({`,
|
|
8782
|
+
` host: "https://ollama.com",`,
|
|
8783
|
+
` headers: {`,
|
|
8784
|
+
` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
|
|
8785
|
+
` },`,
|
|
8786
|
+
` });`,
|
|
8787
|
+
``,
|
|
8788
|
+
` const response = await ollama.chat({`,
|
|
8789
|
+
` model: "gpt-oss:20b",`,
|
|
8790
|
+
` messages: [`,
|
|
8791
|
+
` {`,
|
|
8792
|
+
` role: "system",`,
|
|
8793
|
+
` content: [`,
|
|
8794
|
+
` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
|
|
8795
|
+
` "",`,
|
|
8796
|
+
` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
|
|
8797
|
+
` "",`,
|
|
8798
|
+
` "1. ТИПЫ ПОЗИЦИЙ:",`,
|
|
8799
|
+
` " - position='wait': нет четкого сигнала, жди лучших условий",`,
|
|
8800
|
+
` " - position='long': бычий сигнал, цена будет расти",`,
|
|
8801
|
+
` " - position='short': медвежий сигнал, цена будет падать",`,
|
|
8802
|
+
` "",`,
|
|
8803
|
+
` "2. ЦЕНА ВХОДА (priceOpen):",`,
|
|
8804
|
+
` " - Может быть текущей рыночной ценой для немедленного входа",`,
|
|
8805
|
+
` " - Может быть отложенной ценой для входа при достижении уровня",`,
|
|
8806
|
+
` " - Укажи оптимальную цену входа согласно технического анализа",`,
|
|
8807
|
+
` "",`,
|
|
8808
|
+
` "3. УРОВНИ ВЫХОДА:",`,
|
|
8809
|
+
` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
|
|
8810
|
+
` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
|
|
8811
|
+
` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
|
|
8812
|
+
` "",`,
|
|
8813
|
+
` "4. ВРЕМЕННЫЕ РАМКИ:",`,
|
|
8814
|
+
` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
|
|
8815
|
+
` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
|
|
8816
|
+
` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
|
|
8817
|
+
` ].join("\\n"),`,
|
|
8818
|
+
` },`,
|
|
8819
|
+
` ...messages,`,
|
|
8820
|
+
` ],`,
|
|
8821
|
+
` format: {`,
|
|
8822
|
+
` type: "object",`,
|
|
8823
|
+
` properties: {`,
|
|
8824
|
+
` position: {`,
|
|
8825
|
+
` type: "string",`,
|
|
8826
|
+
` enum: ["wait", "long", "short"],`,
|
|
8827
|
+
` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
|
|
8828
|
+
` },`,
|
|
8829
|
+
` note: {`,
|
|
8830
|
+
` type: "string",`,
|
|
8831
|
+
` description: "Professional trading recommendation with price levels",`,
|
|
8832
|
+
` },`,
|
|
8833
|
+
` priceOpen: {`,
|
|
8834
|
+
` type: "number",`,
|
|
8835
|
+
` description: "Entry price (current market price or limit order price)",`,
|
|
8836
|
+
` },`,
|
|
8837
|
+
` priceTakeProfit: {`,
|
|
8838
|
+
` type: "number",`,
|
|
8839
|
+
` description: "Take profit target price",`,
|
|
8840
|
+
` },`,
|
|
8841
|
+
` priceStopLoss: {`,
|
|
8842
|
+
` type: "number",`,
|
|
8843
|
+
` description: "Stop loss exit price",`,
|
|
8844
|
+
` },`,
|
|
8845
|
+
` minuteEstimatedTime: {`,
|
|
8846
|
+
` type: "number",`,
|
|
8847
|
+
` description: "Expected time to reach TP in minutes (max 360)",`,
|
|
8848
|
+
` },`,
|
|
8849
|
+
` },`,
|
|
8850
|
+
` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
|
|
8851
|
+
` },`,
|
|
8852
|
+
` });`,
|
|
8853
|
+
``,
|
|
8854
|
+
` const jsonResponse = JSON.parse(response.message.content.trim());`,
|
|
8855
|
+
` return jsonResponse;`,
|
|
8856
|
+
`}`
|
|
8857
|
+
].join("\n");
|
|
8858
|
+
};
|
|
8859
|
+
}
|
|
8860
|
+
}
|
|
8861
|
+
|
|
8862
|
+
/**
|
|
8863
|
+
* Service for managing optimizer schema registration and retrieval.
|
|
8864
|
+
* Provides validation and registry management for optimizer configurations.
|
|
8865
|
+
*
|
|
8866
|
+
* Uses ToolRegistry for immutable schema storage.
|
|
8867
|
+
*/
|
|
8868
|
+
class OptimizerSchemaService {
|
|
8869
|
+
constructor() {
|
|
8870
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8871
|
+
this._registry = new functoolsKit.ToolRegistry("optimizerSchema");
|
|
8872
|
+
/**
|
|
8873
|
+
* Registers a new optimizer schema.
|
|
8874
|
+
* Validates required fields before registration.
|
|
8875
|
+
*
|
|
8876
|
+
* @param key - Unique optimizer name
|
|
8877
|
+
* @param value - Optimizer schema configuration
|
|
8878
|
+
* @throws Error if schema validation fails
|
|
8879
|
+
*/
|
|
8880
|
+
this.register = (key, value) => {
|
|
8881
|
+
this.loggerService.log(`optimizerSchemaService register`, { key });
|
|
8882
|
+
this.validateShallow(value);
|
|
8883
|
+
this._registry = this._registry.register(key, value);
|
|
8884
|
+
};
|
|
8885
|
+
/**
|
|
8886
|
+
* Validates optimizer schema structure.
|
|
8887
|
+
* Checks required fields: optimizerName, rangeTrain, source, getPrompt.
|
|
8888
|
+
*
|
|
8889
|
+
* @param optimizerSchema - Schema to validate
|
|
8890
|
+
* @throws Error if validation fails
|
|
8891
|
+
*/
|
|
8892
|
+
this.validateShallow = (optimizerSchema) => {
|
|
8893
|
+
this.loggerService.log(`optimizerTemplateService validateShallow`, {
|
|
8894
|
+
optimizerSchema,
|
|
8895
|
+
});
|
|
8896
|
+
if (typeof optimizerSchema.optimizerName !== "string") {
|
|
8897
|
+
throw new Error(`optimizer template validation failed: missing optimizerName`);
|
|
8898
|
+
}
|
|
8899
|
+
if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
|
|
8900
|
+
throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8901
|
+
}
|
|
8902
|
+
if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
|
|
8903
|
+
throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8904
|
+
}
|
|
8905
|
+
if (typeof optimizerSchema.getPrompt !== "function") {
|
|
8906
|
+
throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
|
|
8907
|
+
}
|
|
8908
|
+
};
|
|
8909
|
+
/**
|
|
8910
|
+
* Partially overrides an existing optimizer schema.
|
|
8911
|
+
* Merges provided values with existing schema.
|
|
8912
|
+
*
|
|
8913
|
+
* @param key - Optimizer name to override
|
|
8914
|
+
* @param value - Partial schema values to merge
|
|
8915
|
+
* @returns Updated complete schema
|
|
8916
|
+
* @throws Error if optimizer not found
|
|
8917
|
+
*/
|
|
8918
|
+
this.override = (key, value) => {
|
|
8919
|
+
this.loggerService.log(`optimizerSchemaService override`, { key });
|
|
8920
|
+
this._registry = this._registry.override(key, value);
|
|
8921
|
+
return this._registry.get(key);
|
|
8922
|
+
};
|
|
8923
|
+
/**
|
|
8924
|
+
* Retrieves optimizer schema by name.
|
|
8925
|
+
*
|
|
8926
|
+
* @param key - Optimizer name
|
|
8927
|
+
* @returns Complete optimizer schema
|
|
8928
|
+
* @throws Error if optimizer not found
|
|
8929
|
+
*/
|
|
8930
|
+
this.get = (key) => {
|
|
8931
|
+
this.loggerService.log(`optimizerSchemaService get`, { key });
|
|
8932
|
+
return this._registry.get(key);
|
|
8933
|
+
};
|
|
8934
|
+
}
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
/**
|
|
8938
|
+
* Service for validating optimizer existence and managing optimizer registry.
|
|
8939
|
+
* Maintains a Map of registered optimizers for validation purposes.
|
|
8940
|
+
*
|
|
8941
|
+
* Uses memoization for efficient repeated validation checks.
|
|
8942
|
+
*/
|
|
8943
|
+
class OptimizerValidationService {
|
|
8944
|
+
constructor() {
|
|
8945
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
8946
|
+
this._optimizerMap = new Map();
|
|
8947
|
+
/**
|
|
8948
|
+
* Adds optimizer to validation registry.
|
|
8949
|
+
* Prevents duplicate optimizer names.
|
|
8950
|
+
*
|
|
8951
|
+
* @param optimizerName - Unique optimizer identifier
|
|
8952
|
+
* @param optimizerSchema - Complete optimizer schema
|
|
8953
|
+
* @throws Error if optimizer with same name already exists
|
|
8954
|
+
*/
|
|
8955
|
+
this.addOptimizer = (optimizerName, optimizerSchema) => {
|
|
8956
|
+
this.loggerService.log("optimizerValidationService addOptimizer", {
|
|
8957
|
+
optimizerName,
|
|
8958
|
+
optimizerSchema,
|
|
8959
|
+
});
|
|
8960
|
+
if (this._optimizerMap.has(optimizerName)) {
|
|
8961
|
+
throw new Error(`optimizer ${optimizerName} already exist`);
|
|
8962
|
+
}
|
|
8963
|
+
this._optimizerMap.set(optimizerName, optimizerSchema);
|
|
8964
|
+
};
|
|
8965
|
+
/**
|
|
8966
|
+
* Validates that optimizer exists in registry.
|
|
8967
|
+
* Memoized for performance on repeated checks.
|
|
8968
|
+
*
|
|
8969
|
+
* @param optimizerName - Optimizer name to validate
|
|
8970
|
+
* @param source - Source method name for error messages
|
|
8971
|
+
* @throws Error if optimizer not found
|
|
8972
|
+
*/
|
|
8973
|
+
this.validate = functoolsKit.memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
|
|
8974
|
+
this.loggerService.log("optimizerValidationService validate", {
|
|
8975
|
+
optimizerName,
|
|
8976
|
+
source,
|
|
8977
|
+
});
|
|
8978
|
+
const optimizer = this._optimizerMap.get(optimizerName);
|
|
8979
|
+
if (!optimizer) {
|
|
8980
|
+
throw new Error(`optimizer ${optimizerName} not found source=${source}`);
|
|
8981
|
+
}
|
|
8982
|
+
return true;
|
|
8983
|
+
});
|
|
8984
|
+
/**
|
|
8985
|
+
* Lists all registered optimizer schemas.
|
|
8986
|
+
*
|
|
8987
|
+
* @returns Array of all optimizer schemas
|
|
8988
|
+
*/
|
|
8989
|
+
this.list = async () => {
|
|
8990
|
+
this.loggerService.log("optimizerValidationService list");
|
|
8991
|
+
return Array.from(this._optimizerMap.values());
|
|
8992
|
+
};
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8995
|
+
|
|
8996
|
+
const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
|
|
8997
|
+
const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
|
|
8998
|
+
const METHOD_NAME_DUMP = "optimizerGlobalService dump";
|
|
8999
|
+
/**
|
|
9000
|
+
* Global service for optimizer operations with validation.
|
|
9001
|
+
* Entry point for public API, performs validation before delegating to ConnectionService.
|
|
9002
|
+
*
|
|
9003
|
+
* Workflow:
|
|
9004
|
+
* 1. Log operation
|
|
9005
|
+
* 2. Validate optimizer exists
|
|
9006
|
+
* 3. Delegate to OptimizerConnectionService
|
|
9007
|
+
*/
|
|
9008
|
+
class OptimizerGlobalService {
|
|
9009
|
+
constructor() {
|
|
9010
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9011
|
+
this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
|
|
9012
|
+
this.optimizerValidationService = inject(TYPES.optimizerValidationService);
|
|
9013
|
+
/**
|
|
9014
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9015
|
+
* Validates optimizer existence before execution.
|
|
9016
|
+
*
|
|
9017
|
+
* @param symbol - Trading pair symbol
|
|
9018
|
+
* @param optimizerName - Optimizer identifier
|
|
9019
|
+
* @returns Array of generated strategies with conversation context
|
|
9020
|
+
* @throws Error if optimizer not found
|
|
9021
|
+
*/
|
|
9022
|
+
this.getData = async (symbol, optimizerName) => {
|
|
9023
|
+
this.loggerService.log(METHOD_NAME_GET_DATA, {
|
|
9024
|
+
symbol,
|
|
9025
|
+
optimizerName,
|
|
9026
|
+
});
|
|
9027
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
|
|
9028
|
+
return await this.optimizerConnectionService.getData(symbol, optimizerName);
|
|
9029
|
+
};
|
|
9030
|
+
/**
|
|
9031
|
+
* Generates complete executable strategy code.
|
|
9032
|
+
* Validates optimizer existence before execution.
|
|
9033
|
+
*
|
|
9034
|
+
* @param symbol - Trading pair symbol
|
|
9035
|
+
* @param optimizerName - Optimizer identifier
|
|
9036
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9037
|
+
* @throws Error if optimizer not found
|
|
9038
|
+
*/
|
|
9039
|
+
this.getCode = async (symbol, optimizerName) => {
|
|
9040
|
+
this.loggerService.log(METHOD_NAME_GET_CODE, {
|
|
9041
|
+
symbol,
|
|
9042
|
+
optimizerName,
|
|
9043
|
+
});
|
|
9044
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
|
|
9045
|
+
return await this.optimizerConnectionService.getCode(symbol, optimizerName);
|
|
9046
|
+
};
|
|
9047
|
+
/**
|
|
9048
|
+
* Generates and saves strategy code to file.
|
|
9049
|
+
* Validates optimizer existence before execution.
|
|
9050
|
+
*
|
|
9051
|
+
* @param symbol - Trading pair symbol
|
|
9052
|
+
* @param optimizerName - Optimizer identifier
|
|
9053
|
+
* @param path - Output directory path (optional)
|
|
9054
|
+
* @throws Error if optimizer not found
|
|
9055
|
+
*/
|
|
9056
|
+
this.dump = async (symbol, optimizerName, path) => {
|
|
9057
|
+
this.loggerService.log(METHOD_NAME_DUMP, {
|
|
9058
|
+
symbol,
|
|
9059
|
+
optimizerName,
|
|
9060
|
+
path,
|
|
9061
|
+
});
|
|
9062
|
+
this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
|
|
9063
|
+
return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
|
|
9064
|
+
};
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
const ITERATION_LIMIT = 25;
|
|
9069
|
+
const DEFAULT_SOURCE_NAME = "unknown";
|
|
9070
|
+
const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
|
|
9071
|
+
/**
|
|
9072
|
+
* Default user message formatter.
|
|
9073
|
+
* Delegates to template's getUserMessage method.
|
|
9074
|
+
*
|
|
9075
|
+
* @param symbol - Trading pair symbol
|
|
9076
|
+
* @param data - Fetched data array
|
|
9077
|
+
* @param name - Source name
|
|
9078
|
+
* @param self - ClientOptimizer instance
|
|
9079
|
+
* @returns Formatted user message content
|
|
9080
|
+
*/
|
|
9081
|
+
const DEFAULT_USER_FN = async (symbol, data, name, self) => {
|
|
9082
|
+
return await self.params.template.getUserMessage(symbol, data, name);
|
|
9083
|
+
};
|
|
9084
|
+
/**
|
|
9085
|
+
* Default assistant message formatter.
|
|
9086
|
+
* Delegates to template's getAssistantMessage method.
|
|
9087
|
+
*
|
|
9088
|
+
* @param symbol - Trading pair symbol
|
|
9089
|
+
* @param data - Fetched data array
|
|
9090
|
+
* @param name - Source name
|
|
9091
|
+
* @param self - ClientOptimizer instance
|
|
9092
|
+
* @returns Formatted assistant message content
|
|
9093
|
+
*/
|
|
9094
|
+
const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
|
|
9095
|
+
return await self.params.template.getAssistantMessage(symbol, data, name);
|
|
9096
|
+
};
|
|
9097
|
+
/**
|
|
9098
|
+
* Resolves paginated data from source with deduplication.
|
|
9099
|
+
* Uses iterateDocuments to handle pagination automatically.
|
|
9100
|
+
*
|
|
9101
|
+
* @param fetch - Source fetch function
|
|
9102
|
+
* @param filterData - Filter arguments (symbol, dates)
|
|
9103
|
+
* @returns Deduplicated array of all fetched data
|
|
9104
|
+
*/
|
|
9105
|
+
const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
|
|
9106
|
+
const iterator = functoolsKit.iterateDocuments({
|
|
9107
|
+
limit: ITERATION_LIMIT,
|
|
9108
|
+
async createRequest({ limit, offset }) {
|
|
9109
|
+
return await fetch({
|
|
9110
|
+
symbol: filterData.symbol,
|
|
9111
|
+
startDate: filterData.startDate,
|
|
9112
|
+
endDate: filterData.endDate,
|
|
9113
|
+
limit,
|
|
9114
|
+
offset,
|
|
9115
|
+
});
|
|
9116
|
+
},
|
|
9117
|
+
});
|
|
9118
|
+
const distinct = functoolsKit.distinctDocuments(iterator, (data) => data.id);
|
|
9119
|
+
return await functoolsKit.resolveDocuments(distinct);
|
|
9120
|
+
};
|
|
9121
|
+
/**
|
|
9122
|
+
* Collects data from all sources and generates strategy metadata.
|
|
9123
|
+
* Iterates through training ranges, fetches data from each source,
|
|
9124
|
+
* builds LLM conversation history, and generates strategy prompts.
|
|
9125
|
+
*
|
|
9126
|
+
* @param symbol - Trading pair symbol
|
|
9127
|
+
* @param self - ClientOptimizer instance
|
|
9128
|
+
* @returns Array of generated strategies with conversation context
|
|
9129
|
+
*/
|
|
9130
|
+
const GET_STRATEGY_DATA_FN = async (symbol, self) => {
|
|
9131
|
+
const strategyList = [];
|
|
9132
|
+
for (const { startDate, endDate } of self.params.rangeTrain) {
|
|
9133
|
+
const messageList = [];
|
|
9134
|
+
for (const source of self.params.source) {
|
|
9135
|
+
if (typeof source === "function") {
|
|
9136
|
+
const data = await RESOLVE_PAGINATION_FN(source, {
|
|
9137
|
+
symbol,
|
|
9138
|
+
startDate,
|
|
9139
|
+
endDate,
|
|
9140
|
+
});
|
|
9141
|
+
if (self.params.callbacks?.onSourceData) {
|
|
9142
|
+
await self.params.callbacks.onSourceData(symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
|
|
9143
|
+
}
|
|
9144
|
+
const [userContent, assistantContent] = await Promise.all([
|
|
9145
|
+
DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
|
|
9146
|
+
DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
|
|
9147
|
+
]);
|
|
9148
|
+
messageList.push({
|
|
9149
|
+
role: "user",
|
|
9150
|
+
content: userContent,
|
|
9151
|
+
}, {
|
|
9152
|
+
role: "assistant",
|
|
9153
|
+
content: assistantContent,
|
|
9154
|
+
});
|
|
9155
|
+
}
|
|
9156
|
+
else {
|
|
9157
|
+
const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
|
|
9158
|
+
const data = await RESOLVE_PAGINATION_FN(fetch, {
|
|
9159
|
+
symbol,
|
|
9160
|
+
startDate,
|
|
9161
|
+
endDate,
|
|
9162
|
+
});
|
|
9163
|
+
if (self.params.callbacks?.onSourceData) {
|
|
9164
|
+
await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
|
|
9165
|
+
}
|
|
9166
|
+
const [userContent, assistantContent] = await Promise.all([
|
|
9167
|
+
user(symbol, data, name, self),
|
|
9168
|
+
assistant(symbol, data, name, self),
|
|
9169
|
+
]);
|
|
9170
|
+
messageList.push({
|
|
9171
|
+
role: "user",
|
|
9172
|
+
content: userContent,
|
|
9173
|
+
}, {
|
|
9174
|
+
role: "assistant",
|
|
9175
|
+
content: assistantContent,
|
|
9176
|
+
});
|
|
9177
|
+
}
|
|
9178
|
+
const name = "name" in source
|
|
9179
|
+
? source.name || DEFAULT_SOURCE_NAME
|
|
9180
|
+
: DEFAULT_SOURCE_NAME;
|
|
9181
|
+
strategyList.push({
|
|
9182
|
+
symbol,
|
|
9183
|
+
name,
|
|
9184
|
+
messages: messageList,
|
|
9185
|
+
strategy: await self.params.getPrompt(symbol, messageList),
|
|
9186
|
+
});
|
|
9187
|
+
}
|
|
9188
|
+
}
|
|
9189
|
+
if (self.params.callbacks?.onData) {
|
|
9190
|
+
await self.params.callbacks.onData(symbol, strategyList);
|
|
9191
|
+
}
|
|
9192
|
+
return strategyList;
|
|
9193
|
+
};
|
|
9194
|
+
/**
|
|
9195
|
+
* Generates complete executable strategy code.
|
|
9196
|
+
* Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
|
|
9197
|
+
*
|
|
9198
|
+
* @param symbol - Trading pair symbol
|
|
9199
|
+
* @param self - ClientOptimizer instance
|
|
9200
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9201
|
+
*/
|
|
9202
|
+
const GET_STRATEGY_CODE_FN = async (symbol, self) => {
|
|
9203
|
+
const strategyData = await self.getData(symbol);
|
|
9204
|
+
const prefix = CREATE_PREFIX_FN();
|
|
9205
|
+
const sections = [];
|
|
9206
|
+
const exchangeName = `${prefix}_exchange`;
|
|
9207
|
+
// 1. Top banner with imports
|
|
9208
|
+
{
|
|
9209
|
+
sections.push(await self.params.template.getTopBanner(symbol));
|
|
9210
|
+
sections.push("");
|
|
9211
|
+
}
|
|
9212
|
+
// 2. JSON dump helper function
|
|
9213
|
+
{
|
|
9214
|
+
sections.push(await self.params.template.getJsonDumpTemplate(symbol));
|
|
9215
|
+
sections.push("");
|
|
9216
|
+
}
|
|
9217
|
+
// 3. Helper functions (text and json)
|
|
9218
|
+
{
|
|
9219
|
+
sections.push(await self.params.template.getTextTemplate(symbol));
|
|
9220
|
+
sections.push("");
|
|
9221
|
+
}
|
|
9222
|
+
{
|
|
9223
|
+
sections.push(await self.params.template.getJsonTemplate(symbol));
|
|
9224
|
+
sections.push("");
|
|
9225
|
+
}
|
|
9226
|
+
// 4. Exchange template (assuming first strategy has exchange info)
|
|
9227
|
+
{
|
|
9228
|
+
sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
|
|
9229
|
+
sections.push("");
|
|
9230
|
+
}
|
|
9231
|
+
// 5. Train frame templates
|
|
9232
|
+
{
|
|
9233
|
+
for (let i = 0; i < self.params.rangeTrain.length; i++) {
|
|
9234
|
+
const range = self.params.rangeTrain[i];
|
|
9235
|
+
const frameName = `${prefix}_train_frame-${i + 1}`;
|
|
9236
|
+
sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
|
|
9237
|
+
range.startDate, range.endDate));
|
|
9238
|
+
sections.push("");
|
|
9239
|
+
}
|
|
9240
|
+
}
|
|
9241
|
+
// 6. Test frame template
|
|
9242
|
+
{
|
|
9243
|
+
const testFrameName = `${prefix}_test_frame`;
|
|
9244
|
+
sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
|
|
9245
|
+
self.params.rangeTest.startDate, self.params.rangeTest.endDate));
|
|
9246
|
+
sections.push("");
|
|
9247
|
+
}
|
|
9248
|
+
// 7. Strategy templates for each generated strategy
|
|
9249
|
+
{
|
|
9250
|
+
for (let i = 0; i < strategyData.length; i++) {
|
|
9251
|
+
const strategy = strategyData[i];
|
|
9252
|
+
const strategyName = `${prefix}_strategy-${i + 1}`;
|
|
9253
|
+
const interval = "5m"; // default interval
|
|
9254
|
+
sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
|
|
9255
|
+
sections.push("");
|
|
9256
|
+
}
|
|
9257
|
+
}
|
|
9258
|
+
// 8. Walker template (uses test frame for validation)
|
|
9259
|
+
{
|
|
9260
|
+
const walkerName = `${prefix}_walker`;
|
|
9261
|
+
const testFrameName = `${prefix}_test_frame`;
|
|
9262
|
+
const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
|
|
9263
|
+
sections.push(await self.params.template.getWalkerTemplate(walkerName, `${prefix}_${exchangeName}`, testFrameName, strategies));
|
|
9264
|
+
sections.push("");
|
|
9265
|
+
}
|
|
9266
|
+
// 9. Launcher template
|
|
9267
|
+
{
|
|
9268
|
+
const walkerName = `${prefix}_walker`;
|
|
9269
|
+
sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
|
|
9270
|
+
sections.push("");
|
|
9271
|
+
}
|
|
9272
|
+
const code = sections.join("\n");
|
|
9273
|
+
if (self.params.callbacks?.onCode) {
|
|
9274
|
+
await self.params.callbacks.onCode(symbol, code);
|
|
9275
|
+
}
|
|
9276
|
+
return code;
|
|
9277
|
+
};
|
|
9278
|
+
/**
|
|
9279
|
+
* Saves generated strategy code to file.
|
|
9280
|
+
* Creates directory if needed, writes .mjs file with generated code.
|
|
9281
|
+
*
|
|
9282
|
+
* @param symbol - Trading pair symbol
|
|
9283
|
+
* @param path - Output directory path
|
|
9284
|
+
* @param self - ClientOptimizer instance
|
|
9285
|
+
*/
|
|
9286
|
+
const GET_STRATEGY_DUMP_FN = async (symbol, path$1, self) => {
|
|
9287
|
+
const report = await self.getCode(symbol);
|
|
9288
|
+
try {
|
|
9289
|
+
const dir = path.join(process.cwd(), path$1);
|
|
9290
|
+
await fs.mkdir(dir, { recursive: true });
|
|
9291
|
+
const filename = `${self.params.optimizerName}_${symbol}.mjs`;
|
|
9292
|
+
const filepath = path.join(dir, filename);
|
|
9293
|
+
await fs.writeFile(filepath, report, "utf-8");
|
|
9294
|
+
self.params.logger.info(`Optimizer report saved: ${filepath}`);
|
|
9295
|
+
if (self.params.callbacks?.onDump) {
|
|
9296
|
+
await self.params.callbacks.onDump(symbol, filepath);
|
|
9297
|
+
}
|
|
9298
|
+
}
|
|
9299
|
+
catch (error) {
|
|
9300
|
+
self.params.logger.warn(`Failed to save optimizer report:`, error);
|
|
9301
|
+
throw error;
|
|
9302
|
+
}
|
|
9303
|
+
};
|
|
9304
|
+
/**
|
|
9305
|
+
* Client implementation for optimizer operations.
|
|
9306
|
+
*
|
|
9307
|
+
* Features:
|
|
9308
|
+
* - Data collection from multiple sources with pagination
|
|
9309
|
+
* - LLM conversation history building
|
|
9310
|
+
* - Strategy code generation with templates
|
|
9311
|
+
* - File export with callbacks
|
|
9312
|
+
*
|
|
9313
|
+
* Used by OptimizerConnectionService to create optimizer instances.
|
|
9314
|
+
*/
|
|
9315
|
+
class ClientOptimizer {
|
|
9316
|
+
constructor(params) {
|
|
9317
|
+
this.params = params;
|
|
9318
|
+
/**
|
|
9319
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9320
|
+
* Processes each training range and builds LLM conversation history.
|
|
9321
|
+
*
|
|
9322
|
+
* @param symbol - Trading pair symbol
|
|
9323
|
+
* @returns Array of generated strategies with conversation context
|
|
9324
|
+
*/
|
|
9325
|
+
this.getData = async (symbol) => {
|
|
9326
|
+
this.params.logger.debug("ClientOptimizer getData", {
|
|
9327
|
+
symbol,
|
|
9328
|
+
});
|
|
9329
|
+
return await GET_STRATEGY_DATA_FN(symbol, this);
|
|
9330
|
+
};
|
|
9331
|
+
/**
|
|
9332
|
+
* Generates complete executable strategy code.
|
|
9333
|
+
* Includes imports, helpers, strategies, walker, and launcher.
|
|
9334
|
+
*
|
|
9335
|
+
* @param symbol - Trading pair symbol
|
|
9336
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9337
|
+
*/
|
|
9338
|
+
this.getCode = async (symbol) => {
|
|
9339
|
+
this.params.logger.debug("ClientOptimizer getCode", {
|
|
9340
|
+
symbol,
|
|
9341
|
+
});
|
|
9342
|
+
return await GET_STRATEGY_CODE_FN(symbol, this);
|
|
9343
|
+
};
|
|
9344
|
+
/**
|
|
9345
|
+
* Generates and saves strategy code to file.
|
|
9346
|
+
* Creates directory if needed, writes .mjs file.
|
|
9347
|
+
*
|
|
9348
|
+
* @param symbol - Trading pair symbol
|
|
9349
|
+
* @param path - Output directory path (default: "./")
|
|
9350
|
+
*/
|
|
9351
|
+
this.dump = async (symbol, path = "./") => {
|
|
9352
|
+
this.params.logger.debug("ClientOptimizer dump", {
|
|
9353
|
+
symbol,
|
|
9354
|
+
path,
|
|
9355
|
+
});
|
|
9356
|
+
return await GET_STRATEGY_DUMP_FN(symbol, path, this);
|
|
9357
|
+
};
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
|
|
9361
|
+
/**
|
|
9362
|
+
* Service for creating and caching optimizer client instances.
|
|
9363
|
+
* Handles dependency injection and template merging.
|
|
9364
|
+
*
|
|
9365
|
+
* Features:
|
|
9366
|
+
* - Memoized optimizer instances (one per optimizerName)
|
|
9367
|
+
* - Template merging (custom + defaults)
|
|
9368
|
+
* - Logger injection
|
|
9369
|
+
* - Delegates to ClientOptimizer for actual operations
|
|
9370
|
+
*/
|
|
9371
|
+
class OptimizerConnectionService {
|
|
9372
|
+
constructor() {
|
|
9373
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9374
|
+
this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
|
|
9375
|
+
this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
|
|
9376
|
+
/**
|
|
9377
|
+
* Creates or retrieves cached optimizer instance.
|
|
9378
|
+
* Memoized by optimizerName for performance.
|
|
9379
|
+
*
|
|
9380
|
+
* Merges custom templates from schema with defaults from OptimizerTemplateService.
|
|
9381
|
+
*
|
|
9382
|
+
* @param optimizerName - Unique optimizer identifier
|
|
9383
|
+
* @returns ClientOptimizer instance with resolved dependencies
|
|
9384
|
+
*/
|
|
9385
|
+
this.getOptimizer = functoolsKit.memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
|
|
9386
|
+
const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks } = this.optimizerSchemaService.get(optimizerName);
|
|
9387
|
+
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;
|
|
9388
|
+
const template = {
|
|
9389
|
+
getAssistantMessage,
|
|
9390
|
+
getExchangeTemplate,
|
|
9391
|
+
getFrameTemplate,
|
|
9392
|
+
getJsonDumpTemplate,
|
|
9393
|
+
getJsonTemplate,
|
|
9394
|
+
getLauncherTemplate,
|
|
9395
|
+
getStrategyTemplate,
|
|
9396
|
+
getTextTemplate,
|
|
9397
|
+
getWalkerTemplate,
|
|
9398
|
+
getTopBanner,
|
|
9399
|
+
getUserMessage,
|
|
9400
|
+
};
|
|
9401
|
+
return new ClientOptimizer({
|
|
9402
|
+
optimizerName,
|
|
9403
|
+
logger: this.loggerService,
|
|
9404
|
+
getPrompt,
|
|
9405
|
+
rangeTest,
|
|
9406
|
+
rangeTrain,
|
|
9407
|
+
source,
|
|
9408
|
+
template,
|
|
9409
|
+
callbacks,
|
|
9410
|
+
});
|
|
9411
|
+
});
|
|
9412
|
+
/**
|
|
9413
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
9414
|
+
*
|
|
9415
|
+
* @param symbol - Trading pair symbol
|
|
9416
|
+
* @param optimizerName - Optimizer identifier
|
|
9417
|
+
* @returns Array of generated strategies with conversation context
|
|
9418
|
+
*/
|
|
9419
|
+
this.getData = async (symbol, optimizerName) => {
|
|
9420
|
+
this.loggerService.log("optimizerConnectionService getData", {
|
|
9421
|
+
symbol,
|
|
9422
|
+
optimizerName,
|
|
9423
|
+
});
|
|
9424
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9425
|
+
return await optimizer.getData(symbol);
|
|
9426
|
+
};
|
|
9427
|
+
/**
|
|
9428
|
+
* Generates complete executable strategy code.
|
|
9429
|
+
*
|
|
9430
|
+
* @param symbol - Trading pair symbol
|
|
9431
|
+
* @param optimizerName - Optimizer identifier
|
|
9432
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
9433
|
+
*/
|
|
9434
|
+
this.getCode = async (symbol, optimizerName) => {
|
|
9435
|
+
this.loggerService.log("optimizerConnectionService getCode", {
|
|
9436
|
+
symbol,
|
|
9437
|
+
optimizerName,
|
|
9438
|
+
});
|
|
9439
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9440
|
+
return await optimizer.getCode(symbol);
|
|
9441
|
+
};
|
|
9442
|
+
/**
|
|
9443
|
+
* Generates and saves strategy code to file.
|
|
9444
|
+
*
|
|
9445
|
+
* @param symbol - Trading pair symbol
|
|
9446
|
+
* @param optimizerName - Optimizer identifier
|
|
9447
|
+
* @param path - Output directory path (optional)
|
|
9448
|
+
*/
|
|
9449
|
+
this.dump = async (symbol, optimizerName, path) => {
|
|
9450
|
+
this.loggerService.log("optimizerConnectionService getCode", {
|
|
9451
|
+
symbol,
|
|
9452
|
+
optimizerName,
|
|
9453
|
+
});
|
|
9454
|
+
const optimizer = this.getOptimizer(optimizerName);
|
|
9455
|
+
return await optimizer.dump(symbol, path);
|
|
9456
|
+
};
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
|
|
9460
|
+
/**
|
|
9461
|
+
* Symbol marker indicating that partial state needs initialization.
|
|
9462
|
+
* Used as sentinel value for _states before waitForInit() is called.
|
|
9463
|
+
*/
|
|
9464
|
+
const NEED_FETCH = Symbol("need_fetch");
|
|
9465
|
+
/**
|
|
9466
|
+
* Array of profit level milestones to track (10%, 20%, ..., 100%).
|
|
9467
|
+
* Each level is checked during profit() method to emit events for newly reached levels.
|
|
9468
|
+
*/
|
|
9469
|
+
const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
9470
|
+
/**
|
|
9471
|
+
* Array of loss level milestones to track (-10%, -20%, ..., -100%).
|
|
9472
|
+
* Each level is checked during loss() method to emit events for newly reached levels.
|
|
9473
|
+
*/
|
|
9474
|
+
const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
9475
|
+
/**
|
|
9476
|
+
* Internal profit handler function for ClientPartial.
|
|
9477
|
+
*
|
|
9478
|
+
* Checks which profit levels have been reached and emits events for new levels only.
|
|
9479
|
+
* Uses Set-based deduplication to prevent duplicate events.
|
|
9480
|
+
*
|
|
9481
|
+
* @param symbol - Trading pair symbol
|
|
9482
|
+
* @param data - Signal row data
|
|
9483
|
+
* @param currentPrice - Current market price
|
|
9484
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9485
|
+
* @param backtest - True if backtest mode
|
|
9486
|
+
* @param when - Event timestamp
|
|
9487
|
+
* @param self - ClientPartial instance reference
|
|
9488
|
+
*/
|
|
9489
|
+
const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
|
|
9490
|
+
if (self._states === NEED_FETCH) {
|
|
9491
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9492
|
+
}
|
|
9493
|
+
let state = self._states.get(data.id);
|
|
9494
|
+
if (!state) {
|
|
9495
|
+
state = {
|
|
9496
|
+
profitLevels: new Set(),
|
|
9497
|
+
lossLevels: new Set(),
|
|
9498
|
+
};
|
|
9499
|
+
self._states.set(data.id, state);
|
|
9500
|
+
}
|
|
9501
|
+
let shouldPersist = false;
|
|
9502
|
+
for (const level of PROFIT_LEVELS) {
|
|
9503
|
+
if (revenuePercent >= level && !state.profitLevels.has(level)) {
|
|
9504
|
+
state.profitLevels.add(level);
|
|
9505
|
+
shouldPersist = true;
|
|
9506
|
+
self.params.logger.debug("ClientPartial profit level reached", {
|
|
9507
|
+
symbol,
|
|
9508
|
+
signalId: data.id,
|
|
9509
|
+
level,
|
|
9510
|
+
revenuePercent,
|
|
9511
|
+
backtest,
|
|
9512
|
+
});
|
|
9513
|
+
await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
9514
|
+
}
|
|
9515
|
+
}
|
|
9516
|
+
if (shouldPersist) {
|
|
9517
|
+
await self._persistState(symbol);
|
|
9518
|
+
}
|
|
9519
|
+
};
|
|
9520
|
+
/**
|
|
9521
|
+
* Internal loss handler function for ClientPartial.
|
|
9522
|
+
*
|
|
9523
|
+
* Checks which loss levels have been reached and emits events for new levels only.
|
|
9524
|
+
* Uses Set-based deduplication to prevent duplicate events.
|
|
9525
|
+
* Converts negative lossPercent to absolute value for level comparison.
|
|
9526
|
+
*
|
|
9527
|
+
* @param symbol - Trading pair symbol
|
|
9528
|
+
* @param data - Signal row data
|
|
9529
|
+
* @param currentPrice - Current market price
|
|
9530
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9531
|
+
* @param backtest - True if backtest mode
|
|
9532
|
+
* @param when - Event timestamp
|
|
9533
|
+
* @param self - ClientPartial instance reference
|
|
9534
|
+
*/
|
|
9535
|
+
const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
|
|
9536
|
+
if (self._states === NEED_FETCH) {
|
|
9537
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9538
|
+
}
|
|
9539
|
+
let state = self._states.get(data.id);
|
|
9540
|
+
if (!state) {
|
|
9541
|
+
state = {
|
|
9542
|
+
profitLevels: new Set(),
|
|
9543
|
+
lossLevels: new Set(),
|
|
9544
|
+
};
|
|
9545
|
+
self._states.set(data.id, state);
|
|
9546
|
+
}
|
|
9547
|
+
const absLoss = Math.abs(lossPercent);
|
|
9548
|
+
let shouldPersist = false;
|
|
9549
|
+
for (const level of LOSS_LEVELS) {
|
|
9550
|
+
if (absLoss >= level && !state.lossLevels.has(level)) {
|
|
9551
|
+
state.lossLevels.add(level);
|
|
9552
|
+
shouldPersist = true;
|
|
9553
|
+
self.params.logger.debug("ClientPartial loss level reached", {
|
|
9554
|
+
symbol,
|
|
9555
|
+
signalId: data.id,
|
|
9556
|
+
level,
|
|
9557
|
+
lossPercent,
|
|
9558
|
+
backtest,
|
|
9559
|
+
});
|
|
9560
|
+
await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
9561
|
+
}
|
|
9562
|
+
}
|
|
9563
|
+
if (shouldPersist) {
|
|
9564
|
+
await self._persistState(symbol);
|
|
9565
|
+
}
|
|
9566
|
+
};
|
|
9567
|
+
/**
|
|
9568
|
+
* Internal initialization function for ClientPartial.
|
|
9569
|
+
*
|
|
9570
|
+
* Loads persisted partial state from disk and restores in-memory Maps.
|
|
9571
|
+
* Converts serialized arrays back to Sets for O(1) lookups.
|
|
9572
|
+
*
|
|
9573
|
+
* @param symbol - Trading pair symbol
|
|
9574
|
+
* @param self - ClientPartial instance reference
|
|
9575
|
+
*/
|
|
9576
|
+
const WAIT_FOR_INIT_FN = async (symbol, self) => {
|
|
9577
|
+
self.params.logger.debug("ClientPartial waitForInit", { symbol });
|
|
9578
|
+
if (self._states === NEED_FETCH) {
|
|
9579
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9580
|
+
}
|
|
9581
|
+
const partialData = await PersistPartialAdapter.readPartialData(symbol);
|
|
9582
|
+
for (const [signalId, data] of Object.entries(partialData)) {
|
|
9583
|
+
const state = {
|
|
9584
|
+
profitLevels: new Set(data.profitLevels),
|
|
9585
|
+
lossLevels: new Set(data.lossLevels),
|
|
9586
|
+
};
|
|
9587
|
+
self._states.set(signalId, state);
|
|
9588
|
+
}
|
|
9589
|
+
self.params.logger.info("ClientPartial restored state", {
|
|
9590
|
+
symbol,
|
|
9591
|
+
signalCount: Object.keys(partialData).length,
|
|
9592
|
+
});
|
|
9593
|
+
};
|
|
9594
|
+
/**
|
|
9595
|
+
* Client implementation for partial profit/loss level tracking.
|
|
9596
|
+
*
|
|
9597
|
+
* Features:
|
|
9598
|
+
* - Tracks profit and loss level milestones (10%, 20%, 30%, etc) per signal
|
|
9599
|
+
* - Deduplicates events using Set-based state per signal ID
|
|
9600
|
+
* - Persists state to disk for crash recovery in live mode
|
|
9601
|
+
* - Emits events via onProfit/onLoss callbacks for each newly reached level
|
|
9602
|
+
*
|
|
9603
|
+
* Architecture:
|
|
9604
|
+
* - Created per signal ID by PartialConnectionService (memoized)
|
|
9605
|
+
* - State stored in Map<signalId, IPartialState> with Set<PartialLevel>
|
|
9606
|
+
* - Persistence handled by PersistPartialAdapter (atomic file writes)
|
|
9607
|
+
*
|
|
9608
|
+
* Lifecycle:
|
|
9609
|
+
* 1. Construction: Initialize empty Map
|
|
9610
|
+
* 2. waitForInit(): Load persisted state from disk
|
|
9611
|
+
* 3. profit()/loss(): Check levels, emit events, persist changes
|
|
9612
|
+
* 4. clear(): Remove signal state, persist, clean up memoized instance
|
|
9613
|
+
*
|
|
9614
|
+
* @example
|
|
9615
|
+
* ```typescript
|
|
9616
|
+
* import { ClientPartial } from "./client/ClientPartial";
|
|
9617
|
+
*
|
|
9618
|
+
* const partial = new ClientPartial({
|
|
9619
|
+
* logger: loggerService,
|
|
9620
|
+
* onProfit: async (symbol, data, price, level, backtest, timestamp) => {
|
|
9621
|
+
* console.log(`Signal ${data.id} reached ${level}% profit at ${price}`);
|
|
9622
|
+
* // Emit to partialProfitSubject
|
|
9623
|
+
* },
|
|
9624
|
+
* onLoss: async (symbol, data, price, level, backtest, timestamp) => {
|
|
9625
|
+
* console.log(`Signal ${data.id} reached -${level}% loss at ${price}`);
|
|
9626
|
+
* // Emit to partialLossSubject
|
|
9627
|
+
* }
|
|
9628
|
+
* });
|
|
9629
|
+
*
|
|
9630
|
+
* // Initialize from persisted state
|
|
9631
|
+
* await partial.waitForInit("BTCUSDT");
|
|
9632
|
+
*
|
|
9633
|
+
* // During signal monitoring in ClientStrategy
|
|
9634
|
+
* const signal = { id: "abc123", priceOpen: 50000, position: "long", ... };
|
|
9635
|
+
*
|
|
9636
|
+
* // Price rises to $55000 (10% profit)
|
|
9637
|
+
* await partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
9638
|
+
* // Emits onProfit callback for 10% level
|
|
9639
|
+
*
|
|
9640
|
+
* // Price rises to $61000 (22% profit)
|
|
9641
|
+
* await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
|
|
9642
|
+
* // Emits onProfit for 20% level only (10% already emitted)
|
|
9643
|
+
*
|
|
9644
|
+
* // Signal closes
|
|
9645
|
+
* await partial.clear("BTCUSDT", signal, 61000);
|
|
9646
|
+
* // State removed, changes persisted
|
|
9647
|
+
* ```
|
|
9648
|
+
*/
|
|
9649
|
+
class ClientPartial {
|
|
9650
|
+
/**
|
|
9651
|
+
* Creates new ClientPartial instance.
|
|
9652
|
+
*
|
|
9653
|
+
* @param params - Partial parameters (logger, onProfit, onLoss callbacks)
|
|
9654
|
+
*/
|
|
9655
|
+
constructor(params) {
|
|
9656
|
+
this.params = params;
|
|
9657
|
+
/**
|
|
9658
|
+
* Map of signal IDs to partial profit/loss state.
|
|
9659
|
+
* Uses NEED_FETCH sentinel before initialization.
|
|
9660
|
+
*
|
|
9661
|
+
* Each state contains:
|
|
9662
|
+
* - profitLevels: Set of reached profit levels (10, 20, 30, etc)
|
|
9663
|
+
* - lossLevels: Set of reached loss levels (10, 20, 30, etc)
|
|
9664
|
+
*/
|
|
9665
|
+
this._states = NEED_FETCH;
|
|
9666
|
+
/**
|
|
9667
|
+
* Initializes partial state by loading from disk.
|
|
9668
|
+
*
|
|
9669
|
+
* Uses singleshot pattern to ensure initialization happens exactly once per symbol.
|
|
9670
|
+
* Reads persisted state from PersistPartialAdapter and restores to _states Map.
|
|
9671
|
+
*
|
|
9672
|
+
* Must be called before profit()/loss()/clear() methods.
|
|
9673
|
+
*
|
|
9674
|
+
* @param symbol - Trading pair symbol
|
|
9675
|
+
* @returns Promise that resolves when initialization is complete
|
|
9676
|
+
*
|
|
9677
|
+
* @example
|
|
9678
|
+
* ```typescript
|
|
9679
|
+
* const partial = new ClientPartial(params);
|
|
9680
|
+
* await partial.waitForInit("BTCUSDT"); // Load persisted state
|
|
9681
|
+
* // Now profit()/loss() can be called
|
|
9682
|
+
* ```
|
|
9683
|
+
*/
|
|
9684
|
+
this.waitForInit = functoolsKit.singleshot(async (symbol) => await WAIT_FOR_INIT_FN(symbol, this));
|
|
9685
|
+
this._states = new Map();
|
|
9686
|
+
}
|
|
9687
|
+
/**
|
|
9688
|
+
* Persists current partial state to disk.
|
|
9689
|
+
*
|
|
9690
|
+
* Converts in-memory Maps and Sets to JSON-serializable format:
|
|
9691
|
+
* - Map<signalId, IPartialState> → Record<signalId, IPartialData>
|
|
9692
|
+
* - Set<PartialLevel> → PartialLevel[]
|
|
9693
|
+
*
|
|
9694
|
+
* Called automatically after profit/loss level changes or clear().
|
|
9695
|
+
* Uses atomic file writes via PersistPartialAdapter.
|
|
9696
|
+
*
|
|
9697
|
+
* @param symbol - Trading pair symbol
|
|
9698
|
+
* @returns Promise that resolves when persistence is complete
|
|
9699
|
+
*/
|
|
9700
|
+
async _persistState(symbol) {
|
|
9701
|
+
this.params.logger.debug("ClientPartial persistState", { symbol });
|
|
9702
|
+
if (this._states === NEED_FETCH) {
|
|
9703
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9704
|
+
}
|
|
9705
|
+
const partialData = {};
|
|
9706
|
+
for (const [signalId, state] of this._states.entries()) {
|
|
9707
|
+
partialData[signalId] = {
|
|
9708
|
+
profitLevels: Array.from(state.profitLevels),
|
|
9709
|
+
lossLevels: Array.from(state.lossLevels),
|
|
9710
|
+
};
|
|
9711
|
+
}
|
|
9712
|
+
await PersistPartialAdapter.writePartialData(partialData, symbol);
|
|
9713
|
+
}
|
|
9714
|
+
/**
|
|
9715
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
9716
|
+
*
|
|
9717
|
+
* Called by ClientStrategy during signal monitoring when revenuePercent > 0.
|
|
9718
|
+
* Iterates through PROFIT_LEVELS (10%, 20%, 30%, etc) and checks which
|
|
9719
|
+
* levels have been reached but not yet emitted (Set-based deduplication).
|
|
9720
|
+
*
|
|
9721
|
+
* For each new level:
|
|
9722
|
+
* 1. Adds level to state.profitLevels Set
|
|
9723
|
+
* 2. Logs debug message
|
|
9724
|
+
* 3. Calls params.onProfit callback (emits to partialProfitSubject)
|
|
9725
|
+
*
|
|
9726
|
+
* After all levels processed, persists state to disk if any new levels were found.
|
|
9727
|
+
*
|
|
9728
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9729
|
+
* @param data - Signal row data
|
|
9730
|
+
* @param currentPrice - Current market price
|
|
9731
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9732
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9733
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9734
|
+
* @returns Promise that resolves when profit processing is complete
|
|
9735
|
+
*
|
|
9736
|
+
* @example
|
|
9737
|
+
* ```typescript
|
|
9738
|
+
* // Signal at $50000, price rises to $61000 (22% profit)
|
|
9739
|
+
* await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
|
|
9740
|
+
* // Emits events for 10% and 20% levels (if not already emitted)
|
|
9741
|
+
* // State persisted to disk
|
|
9742
|
+
* ```
|
|
9743
|
+
*/
|
|
9744
|
+
async profit(symbol, data, currentPrice, revenuePercent, backtest, when) {
|
|
9745
|
+
this.params.logger.debug("ClientPartial profit", {
|
|
9746
|
+
symbol,
|
|
9747
|
+
signalId: data.id,
|
|
9748
|
+
currentPrice,
|
|
9749
|
+
revenuePercent,
|
|
9750
|
+
backtest,
|
|
9751
|
+
when,
|
|
9752
|
+
});
|
|
9753
|
+
return await HANDLE_PROFIT_FN(symbol, data, currentPrice, revenuePercent, backtest, when, this);
|
|
9754
|
+
}
|
|
9755
|
+
/**
|
|
9756
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
9757
|
+
*
|
|
9758
|
+
* Called by ClientStrategy during signal monitoring when revenuePercent < 0.
|
|
9759
|
+
* Converts negative lossPercent to absolute value and iterates through
|
|
9760
|
+
* LOSS_LEVELS (10%, 20%, 30%, etc) to check which levels have been reached
|
|
9761
|
+
* but not yet emitted (Set-based deduplication).
|
|
9762
|
+
*
|
|
9763
|
+
* For each new level:
|
|
9764
|
+
* 1. Adds level to state.lossLevels Set
|
|
9765
|
+
* 2. Logs debug message
|
|
9766
|
+
* 3. Calls params.onLoss callback (emits to partialLossSubject)
|
|
9767
|
+
*
|
|
9768
|
+
* After all levels processed, persists state to disk if any new levels were found.
|
|
9769
|
+
*
|
|
9770
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9771
|
+
* @param data - Signal row data
|
|
9772
|
+
* @param currentPrice - Current market price
|
|
9773
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9774
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9775
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9776
|
+
* @returns Promise that resolves when loss processing is complete
|
|
9777
|
+
*
|
|
9778
|
+
* @example
|
|
9779
|
+
* ```typescript
|
|
9780
|
+
* // Signal at $50000, price drops to $39000 (-22% loss)
|
|
9781
|
+
* await partial.loss("BTCUSDT", signal, 39000, -22.0, false, new Date());
|
|
9782
|
+
* // Emits events for 10% and 20% loss levels (if not already emitted)
|
|
9783
|
+
* // State persisted to disk
|
|
9784
|
+
* ```
|
|
9785
|
+
*/
|
|
9786
|
+
async loss(symbol, data, currentPrice, lossPercent, backtest, when) {
|
|
9787
|
+
this.params.logger.debug("ClientPartial loss", {
|
|
9788
|
+
symbol,
|
|
9789
|
+
signalId: data.id,
|
|
9790
|
+
currentPrice,
|
|
9791
|
+
lossPercent,
|
|
9792
|
+
backtest,
|
|
9793
|
+
when,
|
|
9794
|
+
});
|
|
9795
|
+
return await HANDLE_LOSS_FN(symbol, data, currentPrice, lossPercent, backtest, when, this);
|
|
9796
|
+
}
|
|
9797
|
+
/**
|
|
9798
|
+
* Clears partial profit/loss state for a signal when it closes.
|
|
9799
|
+
*
|
|
9800
|
+
* Called by ClientStrategy when signal completes (TP/SL/time_expired).
|
|
9801
|
+
* Removes signal's state from _states Map and persists changes to disk.
|
|
9802
|
+
*
|
|
9803
|
+
* After clear() completes:
|
|
9804
|
+
* - Signal state removed from memory (_states.delete)
|
|
9805
|
+
* - Changes persisted to disk (PersistPartialAdapter.writePartialData)
|
|
9806
|
+
* - Memoized ClientPartial instance cleared in PartialConnectionService
|
|
9807
|
+
*
|
|
9808
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9809
|
+
* @param data - Signal row data
|
|
9810
|
+
* @param priceClose - Final closing price
|
|
9811
|
+
* @returns Promise that resolves when clear is complete
|
|
9812
|
+
* @throws Error if ClientPartial not initialized (waitForInit not called)
|
|
9813
|
+
*
|
|
9814
|
+
* @example
|
|
9815
|
+
* ```typescript
|
|
9816
|
+
* // Signal closes at take profit
|
|
9817
|
+
* await partial.clear("BTCUSDT", signal, 52000);
|
|
9818
|
+
* // State removed: _states.delete(signal.id)
|
|
9819
|
+
* // Persisted: ./dump/data/partial/BTCUSDT/levels.json updated
|
|
9820
|
+
* // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
|
|
9821
|
+
* ```
|
|
9822
|
+
*/
|
|
9823
|
+
async clear(symbol, data, priceClose) {
|
|
9824
|
+
this.params.logger.log("ClientPartial clear", {
|
|
9825
|
+
symbol,
|
|
9826
|
+
data,
|
|
9827
|
+
priceClose,
|
|
9828
|
+
});
|
|
9829
|
+
if (this._states === NEED_FETCH) {
|
|
9830
|
+
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
9831
|
+
}
|
|
9832
|
+
this._states.delete(data.id);
|
|
9833
|
+
await this._persistState(symbol);
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
/**
|
|
9838
|
+
* Callback function for emitting profit events to partialProfitSubject.
|
|
9839
|
+
*
|
|
9840
|
+
* Called by ClientPartial when a new profit level is reached.
|
|
9841
|
+
* Emits PartialProfitContract event to all subscribers.
|
|
9842
|
+
*
|
|
9843
|
+
* @param symbol - Trading pair symbol
|
|
9844
|
+
* @param data - Signal row data
|
|
9845
|
+
* @param currentPrice - Current market price
|
|
9846
|
+
* @param level - Profit level reached
|
|
9847
|
+
* @param backtest - True if backtest mode
|
|
9848
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
9849
|
+
*/
|
|
9850
|
+
const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
9851
|
+
symbol,
|
|
9852
|
+
data,
|
|
9853
|
+
currentPrice,
|
|
9854
|
+
level,
|
|
9855
|
+
backtest,
|
|
9856
|
+
timestamp,
|
|
9857
|
+
});
|
|
9858
|
+
/**
|
|
9859
|
+
* Callback function for emitting loss events to partialLossSubject.
|
|
9860
|
+
*
|
|
9861
|
+
* Called by ClientPartial when a new loss level is reached.
|
|
9862
|
+
* Emits PartialLossContract event to all subscribers.
|
|
9863
|
+
*
|
|
9864
|
+
* @param symbol - Trading pair symbol
|
|
9865
|
+
* @param data - Signal row data
|
|
9866
|
+
* @param currentPrice - Current market price
|
|
9867
|
+
* @param level - Loss level reached
|
|
9868
|
+
* @param backtest - True if backtest mode
|
|
9869
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
9870
|
+
*/
|
|
9871
|
+
const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
9872
|
+
symbol,
|
|
9873
|
+
data,
|
|
9874
|
+
currentPrice,
|
|
9875
|
+
level,
|
|
9876
|
+
backtest,
|
|
9877
|
+
timestamp,
|
|
9878
|
+
});
|
|
9879
|
+
/**
|
|
9880
|
+
* Connection service for partial profit/loss tracking.
|
|
9881
|
+
*
|
|
9882
|
+
* Provides memoized ClientPartial instances per signal ID.
|
|
9883
|
+
* Acts as factory and lifetime manager for ClientPartial objects.
|
|
9884
|
+
*
|
|
9885
|
+
* Features:
|
|
9886
|
+
* - Creates one ClientPartial instance per signal ID (memoized)
|
|
9887
|
+
* - Configures instances with logger and event emitter callbacks
|
|
9888
|
+
* - Delegates profit/loss/clear operations to appropriate ClientPartial
|
|
9889
|
+
* - Cleans up memoized instances when signals are cleared
|
|
9890
|
+
*
|
|
9891
|
+
* Architecture:
|
|
9892
|
+
* - Injected into ClientStrategy via PartialGlobalService
|
|
9893
|
+
* - Uses memoize from functools-kit for instance caching
|
|
9894
|
+
* - Emits events to partialProfitSubject/partialLossSubject
|
|
9895
|
+
*
|
|
9896
|
+
* @example
|
|
9897
|
+
* ```typescript
|
|
9898
|
+
* // Service injected via DI
|
|
9899
|
+
* const service = inject<PartialConnectionService>(TYPES.partialConnectionService);
|
|
9900
|
+
*
|
|
9901
|
+
* // Called by ClientStrategy during signal monitoring
|
|
9902
|
+
* await service.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
9903
|
+
* // Creates or reuses ClientPartial for signal.id
|
|
9904
|
+
* // Delegates to ClientPartial.profit()
|
|
9905
|
+
*
|
|
9906
|
+
* // When signal closes
|
|
9907
|
+
* await service.clear("BTCUSDT", signal, 52000);
|
|
9908
|
+
* // Clears signal state and removes memoized instance
|
|
9909
|
+
* ```
|
|
9910
|
+
*/
|
|
9911
|
+
class PartialConnectionService {
|
|
9912
|
+
constructor() {
|
|
9913
|
+
/**
|
|
9914
|
+
* Logger service injected from DI container.
|
|
9915
|
+
*/
|
|
9916
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
9917
|
+
/**
|
|
9918
|
+
* Memoized factory function for ClientPartial instances.
|
|
9919
|
+
*
|
|
9920
|
+
* Creates one ClientPartial per signal ID with configured callbacks.
|
|
9921
|
+
* Instances are cached until clear() is called.
|
|
9922
|
+
*
|
|
9923
|
+
* Key format: signalId
|
|
9924
|
+
* Value: ClientPartial instance with logger and event emitters
|
|
9925
|
+
*/
|
|
9926
|
+
this.getPartial = functoolsKit.memoize(([signalId]) => `${signalId}`, () => {
|
|
9927
|
+
return new ClientPartial({
|
|
9928
|
+
logger: this.loggerService,
|
|
9929
|
+
onProfit: COMMIT_PROFIT_FN,
|
|
9930
|
+
onLoss: COMMIT_LOSS_FN,
|
|
9931
|
+
});
|
|
9932
|
+
});
|
|
9933
|
+
/**
|
|
9934
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
9935
|
+
*
|
|
9936
|
+
* Retrieves or creates ClientPartial for signal ID, initializes it if needed,
|
|
9937
|
+
* then delegates to ClientPartial.profit() method.
|
|
9938
|
+
*
|
|
9939
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9940
|
+
* @param data - Signal row data
|
|
9941
|
+
* @param currentPrice - Current market price
|
|
9942
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
9943
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9944
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9945
|
+
* @returns Promise that resolves when profit processing is complete
|
|
9946
|
+
*/
|
|
9947
|
+
this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
|
|
9948
|
+
this.loggerService.log("partialConnectionService profit", {
|
|
9949
|
+
symbol,
|
|
9950
|
+
data,
|
|
9951
|
+
currentPrice,
|
|
9952
|
+
revenuePercent,
|
|
9953
|
+
backtest,
|
|
9954
|
+
when,
|
|
9955
|
+
});
|
|
9956
|
+
const partial = this.getPartial(data.id);
|
|
9957
|
+
await partial.waitForInit(symbol);
|
|
9958
|
+
return await partial.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
|
|
9959
|
+
};
|
|
9960
|
+
/**
|
|
9961
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
9962
|
+
*
|
|
9963
|
+
* Retrieves or creates ClientPartial for signal ID, initializes it if needed,
|
|
9964
|
+
* then delegates to ClientPartial.loss() method.
|
|
9965
|
+
*
|
|
9966
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
9967
|
+
* @param data - Signal row data
|
|
9968
|
+
* @param currentPrice - Current market price
|
|
9969
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
9970
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
9971
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
9972
|
+
* @returns Promise that resolves when loss processing is complete
|
|
9973
|
+
*/
|
|
9974
|
+
this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
|
|
9975
|
+
this.loggerService.log("partialConnectionService loss", {
|
|
9976
|
+
symbol,
|
|
9977
|
+
data,
|
|
9978
|
+
currentPrice,
|
|
9979
|
+
lossPercent,
|
|
9980
|
+
backtest,
|
|
9981
|
+
when,
|
|
9982
|
+
});
|
|
9983
|
+
const partial = this.getPartial(data.id);
|
|
9984
|
+
await partial.waitForInit(symbol);
|
|
9985
|
+
return await partial.loss(symbol, data, currentPrice, lossPercent, backtest, when);
|
|
9986
|
+
};
|
|
9987
|
+
/**
|
|
9988
|
+
* Clears partial profit/loss state when signal closes.
|
|
9989
|
+
*
|
|
9990
|
+
* Retrieves ClientPartial for signal ID, initializes if needed,
|
|
9991
|
+
* delegates clear operation, then removes memoized instance.
|
|
9992
|
+
*
|
|
9993
|
+
* Sequence:
|
|
9994
|
+
* 1. Get ClientPartial from memoize cache
|
|
9995
|
+
* 2. Ensure initialization (waitForInit)
|
|
9996
|
+
* 3. Call ClientPartial.clear() - removes state, persists to disk
|
|
9997
|
+
* 4. Clear memoized instance - prevents memory leaks
|
|
9998
|
+
*
|
|
9999
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10000
|
+
* @param data - Signal row data
|
|
10001
|
+
* @param priceClose - Final closing price
|
|
10002
|
+
* @returns Promise that resolves when clear is complete
|
|
10003
|
+
*/
|
|
10004
|
+
this.clear = async (symbol, data, priceClose) => {
|
|
10005
|
+
this.loggerService.log("partialConnectionService profit", {
|
|
10006
|
+
symbol,
|
|
10007
|
+
data,
|
|
10008
|
+
priceClose,
|
|
10009
|
+
});
|
|
10010
|
+
const partial = this.getPartial(data.id);
|
|
10011
|
+
await partial.waitForInit(symbol);
|
|
10012
|
+
await partial.clear(symbol, data, priceClose);
|
|
10013
|
+
this.getPartial.clear(data.id);
|
|
10014
|
+
};
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
10017
|
+
|
|
10018
|
+
const columns = [
|
|
10019
|
+
{
|
|
10020
|
+
key: "action",
|
|
10021
|
+
label: "Action",
|
|
10022
|
+
format: (data) => data.action.toUpperCase(),
|
|
10023
|
+
},
|
|
10024
|
+
{
|
|
10025
|
+
key: "symbol",
|
|
10026
|
+
label: "Symbol",
|
|
10027
|
+
format: (data) => data.symbol,
|
|
10028
|
+
},
|
|
10029
|
+
{
|
|
10030
|
+
key: "signalId",
|
|
10031
|
+
label: "Signal ID",
|
|
10032
|
+
format: (data) => data.signalId,
|
|
10033
|
+
},
|
|
10034
|
+
{
|
|
10035
|
+
key: "position",
|
|
10036
|
+
label: "Position",
|
|
10037
|
+
format: (data) => data.position.toUpperCase(),
|
|
10038
|
+
},
|
|
10039
|
+
{
|
|
10040
|
+
key: "level",
|
|
10041
|
+
label: "Level %",
|
|
10042
|
+
format: (data) => data.action === "profit" ? `+${data.level}%` : `-${data.level}%`,
|
|
10043
|
+
},
|
|
10044
|
+
{
|
|
10045
|
+
key: "currentPrice",
|
|
10046
|
+
label: "Current Price",
|
|
10047
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
10048
|
+
},
|
|
10049
|
+
{
|
|
10050
|
+
key: "timestamp",
|
|
10051
|
+
label: "Timestamp",
|
|
10052
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
10053
|
+
},
|
|
10054
|
+
{
|
|
10055
|
+
key: "mode",
|
|
10056
|
+
label: "Mode",
|
|
10057
|
+
format: (data) => (data.backtest ? "Backtest" : "Live"),
|
|
10058
|
+
},
|
|
10059
|
+
];
|
|
10060
|
+
/** Maximum number of events to store in partial reports */
|
|
10061
|
+
const MAX_EVENTS = 250;
|
|
10062
|
+
/**
|
|
10063
|
+
* Storage class for accumulating partial profit/loss events per symbol.
|
|
10064
|
+
* Maintains a chronological list of profit and loss level events.
|
|
10065
|
+
*/
|
|
10066
|
+
class ReportStorage {
|
|
10067
|
+
constructor() {
|
|
10068
|
+
/** Internal list of all partial events for this symbol */
|
|
10069
|
+
this._eventList = [];
|
|
10070
|
+
}
|
|
10071
|
+
/**
|
|
10072
|
+
* Adds a profit event to the storage.
|
|
10073
|
+
*
|
|
10074
|
+
* @param symbol - Trading pair symbol
|
|
10075
|
+
* @param data - Signal row data
|
|
10076
|
+
* @param currentPrice - Current market price
|
|
10077
|
+
* @param level - Profit level reached
|
|
10078
|
+
* @param backtest - True if backtest mode
|
|
10079
|
+
*/
|
|
10080
|
+
addProfitEvent(symbol, data, currentPrice, level, backtest, timestamp) {
|
|
10081
|
+
this._eventList.push({
|
|
10082
|
+
timestamp,
|
|
10083
|
+
action: "profit",
|
|
10084
|
+
symbol,
|
|
10085
|
+
signalId: data.id,
|
|
10086
|
+
position: data.position,
|
|
10087
|
+
currentPrice,
|
|
10088
|
+
level,
|
|
10089
|
+
backtest,
|
|
10090
|
+
});
|
|
10091
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
10092
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
10093
|
+
this._eventList.shift();
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
/**
|
|
10097
|
+
* Adds a loss event to the storage.
|
|
10098
|
+
*
|
|
10099
|
+
* @param symbol - Trading pair symbol
|
|
10100
|
+
* @param data - Signal row data
|
|
10101
|
+
* @param currentPrice - Current market price
|
|
10102
|
+
* @param level - Loss level reached
|
|
10103
|
+
* @param backtest - True if backtest mode
|
|
10104
|
+
*/
|
|
10105
|
+
addLossEvent(symbol, data, currentPrice, level, backtest, timestamp) {
|
|
10106
|
+
this._eventList.push({
|
|
10107
|
+
timestamp,
|
|
10108
|
+
action: "loss",
|
|
10109
|
+
symbol,
|
|
10110
|
+
signalId: data.id,
|
|
10111
|
+
position: data.position,
|
|
10112
|
+
currentPrice,
|
|
10113
|
+
level,
|
|
10114
|
+
backtest,
|
|
10115
|
+
});
|
|
10116
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
10117
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
10118
|
+
this._eventList.shift();
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
/**
|
|
10122
|
+
* Calculates statistical data from partial profit/loss events (Controller).
|
|
10123
|
+
*
|
|
10124
|
+
* @returns Statistical data (empty object if no events)
|
|
10125
|
+
*/
|
|
10126
|
+
async getData() {
|
|
10127
|
+
if (this._eventList.length === 0) {
|
|
10128
|
+
return {
|
|
10129
|
+
eventList: [],
|
|
10130
|
+
totalEvents: 0,
|
|
10131
|
+
totalProfit: 0,
|
|
10132
|
+
totalLoss: 0,
|
|
10133
|
+
};
|
|
10134
|
+
}
|
|
10135
|
+
const profitEvents = this._eventList.filter((e) => e.action === "profit");
|
|
10136
|
+
const lossEvents = this._eventList.filter((e) => e.action === "loss");
|
|
10137
|
+
return {
|
|
10138
|
+
eventList: this._eventList,
|
|
10139
|
+
totalEvents: this._eventList.length,
|
|
10140
|
+
totalProfit: profitEvents.length,
|
|
10141
|
+
totalLoss: lossEvents.length,
|
|
10142
|
+
};
|
|
10143
|
+
}
|
|
10144
|
+
/**
|
|
10145
|
+
* Generates markdown report with all partial events for a symbol (View).
|
|
10146
|
+
*
|
|
10147
|
+
* @param symbol - Trading pair symbol
|
|
10148
|
+
* @returns Markdown formatted report with all events
|
|
10149
|
+
*/
|
|
10150
|
+
async getReport(symbol) {
|
|
10151
|
+
const stats = await this.getData();
|
|
10152
|
+
if (stats.totalEvents === 0) {
|
|
10153
|
+
return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", "No partial profit/loss events recorded yet.");
|
|
10154
|
+
}
|
|
10155
|
+
const header = columns.map((col) => col.label);
|
|
10156
|
+
const separator = columns.map(() => "---");
|
|
10157
|
+
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
10158
|
+
const tableData = [header, separator, ...rows];
|
|
10159
|
+
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
10160
|
+
return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
|
|
10161
|
+
}
|
|
10162
|
+
/**
|
|
10163
|
+
* Saves symbol report to disk.
|
|
10164
|
+
*
|
|
10165
|
+
* @param symbol - Trading pair symbol
|
|
10166
|
+
* @param path - Directory path to save report (default: "./dump/partial")
|
|
10167
|
+
*/
|
|
10168
|
+
async dump(symbol, path$1 = "./dump/partial") {
|
|
10169
|
+
const markdown = await this.getReport(symbol);
|
|
10170
|
+
try {
|
|
10171
|
+
const dir = path.join(process.cwd(), path$1);
|
|
10172
|
+
await fs.mkdir(dir, { recursive: true });
|
|
10173
|
+
const filename = `${symbol}.md`;
|
|
10174
|
+
const filepath = path.join(dir, filename);
|
|
10175
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
10176
|
+
console.log(`Partial profit/loss report saved: ${filepath}`);
|
|
10177
|
+
}
|
|
10178
|
+
catch (error) {
|
|
10179
|
+
console.error(`Failed to save markdown report:`, error);
|
|
10180
|
+
}
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
/**
|
|
10184
|
+
* Service for generating and saving partial profit/loss markdown reports.
|
|
10185
|
+
*
|
|
10186
|
+
* Features:
|
|
10187
|
+
* - Listens to partial profit and loss events via partialProfitSubject/partialLossSubject
|
|
10188
|
+
* - Accumulates all events (profit, loss) per symbol
|
|
10189
|
+
* - Generates markdown tables with detailed event information
|
|
10190
|
+
* - Provides statistics (total profit/loss events)
|
|
10191
|
+
* - Saves reports to disk in dump/partial/{symbol}.md
|
|
10192
|
+
*
|
|
10193
|
+
* @example
|
|
10194
|
+
* ```typescript
|
|
10195
|
+
* const service = new PartialMarkdownService();
|
|
10196
|
+
*
|
|
10197
|
+
* // Service automatically subscribes to subjects on init
|
|
10198
|
+
* // No manual callback setup needed
|
|
10199
|
+
*
|
|
10200
|
+
* // Later: generate and save report
|
|
10201
|
+
* await service.dump("BTCUSDT");
|
|
10202
|
+
* ```
|
|
10203
|
+
*/
|
|
10204
|
+
class PartialMarkdownService {
|
|
10205
|
+
constructor() {
|
|
10206
|
+
/** Logger service for debug output */
|
|
10207
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
10208
|
+
/**
|
|
10209
|
+
* Memoized function to get or create ReportStorage for a symbol.
|
|
10210
|
+
* Each symbol gets its own isolated storage instance.
|
|
10211
|
+
*/
|
|
10212
|
+
this.getStorage = functoolsKit.memoize(([symbol]) => `${symbol}`, () => new ReportStorage());
|
|
10213
|
+
/**
|
|
10214
|
+
* Processes profit events and accumulates them.
|
|
10215
|
+
* Should be called from partialProfitSubject subscription.
|
|
10216
|
+
*
|
|
10217
|
+
* @param data - Profit event data
|
|
10218
|
+
*
|
|
10219
|
+
* @example
|
|
10220
|
+
* ```typescript
|
|
10221
|
+
* const service = new PartialMarkdownService();
|
|
10222
|
+
* // Service automatically subscribes in init()
|
|
10223
|
+
* ```
|
|
10224
|
+
*/
|
|
10225
|
+
this.tickProfit = async (data) => {
|
|
10226
|
+
this.loggerService.log("partialMarkdownService tickProfit", {
|
|
10227
|
+
data,
|
|
10228
|
+
});
|
|
10229
|
+
const storage = this.getStorage(data.symbol);
|
|
10230
|
+
storage.addProfitEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
|
|
10231
|
+
};
|
|
10232
|
+
/**
|
|
10233
|
+
* Processes loss events and accumulates them.
|
|
10234
|
+
* Should be called from partialLossSubject subscription.
|
|
10235
|
+
*
|
|
10236
|
+
* @param data - Loss event data
|
|
10237
|
+
*
|
|
10238
|
+
* @example
|
|
10239
|
+
* ```typescript
|
|
10240
|
+
* const service = new PartialMarkdownService();
|
|
10241
|
+
* // Service automatically subscribes in init()
|
|
10242
|
+
* ```
|
|
10243
|
+
*/
|
|
10244
|
+
this.tickLoss = async (data) => {
|
|
10245
|
+
this.loggerService.log("partialMarkdownService tickLoss", {
|
|
10246
|
+
data,
|
|
10247
|
+
});
|
|
10248
|
+
const storage = this.getStorage(data.symbol);
|
|
10249
|
+
storage.addLossEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
|
|
10250
|
+
};
|
|
10251
|
+
/**
|
|
10252
|
+
* Gets statistical data from all partial profit/loss events for a symbol.
|
|
10253
|
+
* Delegates to ReportStorage.getData().
|
|
10254
|
+
*
|
|
10255
|
+
* @param symbol - Trading pair symbol to get data for
|
|
10256
|
+
* @returns Statistical data object with all metrics
|
|
10257
|
+
*
|
|
10258
|
+
* @example
|
|
10259
|
+
* ```typescript
|
|
10260
|
+
* const service = new PartialMarkdownService();
|
|
10261
|
+
* const stats = await service.getData("BTCUSDT");
|
|
10262
|
+
* console.log(stats.totalProfit, stats.totalLoss);
|
|
10263
|
+
* ```
|
|
10264
|
+
*/
|
|
10265
|
+
this.getData = async (symbol) => {
|
|
10266
|
+
this.loggerService.log("partialMarkdownService getData", {
|
|
10267
|
+
symbol,
|
|
10268
|
+
});
|
|
10269
|
+
const storage = this.getStorage(symbol);
|
|
10270
|
+
return storage.getData();
|
|
10271
|
+
};
|
|
10272
|
+
/**
|
|
10273
|
+
* Generates markdown report with all partial events for a symbol.
|
|
10274
|
+
* Delegates to ReportStorage.getReport().
|
|
10275
|
+
*
|
|
10276
|
+
* @param symbol - Trading pair symbol to generate report for
|
|
10277
|
+
* @returns Markdown formatted report string with table of all events
|
|
10278
|
+
*
|
|
10279
|
+
* @example
|
|
10280
|
+
* ```typescript
|
|
10281
|
+
* const service = new PartialMarkdownService();
|
|
10282
|
+
* const markdown = await service.getReport("BTCUSDT");
|
|
10283
|
+
* console.log(markdown);
|
|
10284
|
+
* ```
|
|
10285
|
+
*/
|
|
10286
|
+
this.getReport = async (symbol) => {
|
|
10287
|
+
this.loggerService.log("partialMarkdownService getReport", {
|
|
10288
|
+
symbol,
|
|
10289
|
+
});
|
|
10290
|
+
const storage = this.getStorage(symbol);
|
|
10291
|
+
return storage.getReport(symbol);
|
|
10292
|
+
};
|
|
10293
|
+
/**
|
|
10294
|
+
* Saves symbol report to disk.
|
|
10295
|
+
* Creates directory if it doesn't exist.
|
|
10296
|
+
* Delegates to ReportStorage.dump().
|
|
10297
|
+
*
|
|
10298
|
+
* @param symbol - Trading pair symbol to save report for
|
|
10299
|
+
* @param path - Directory path to save report (default: "./dump/partial")
|
|
10300
|
+
*
|
|
10301
|
+
* @example
|
|
10302
|
+
* ```typescript
|
|
10303
|
+
* const service = new PartialMarkdownService();
|
|
10304
|
+
*
|
|
10305
|
+
* // Save to default path: ./dump/partial/BTCUSDT.md
|
|
10306
|
+
* await service.dump("BTCUSDT");
|
|
10307
|
+
*
|
|
10308
|
+
* // Save to custom path: ./custom/path/BTCUSDT.md
|
|
10309
|
+
* await service.dump("BTCUSDT", "./custom/path");
|
|
10310
|
+
* ```
|
|
10311
|
+
*/
|
|
10312
|
+
this.dump = async (symbol, path = "./dump/partial") => {
|
|
10313
|
+
this.loggerService.log("partialMarkdownService dump", {
|
|
10314
|
+
symbol,
|
|
10315
|
+
path,
|
|
10316
|
+
});
|
|
10317
|
+
const storage = this.getStorage(symbol);
|
|
10318
|
+
await storage.dump(symbol, path);
|
|
10319
|
+
};
|
|
10320
|
+
/**
|
|
10321
|
+
* Clears accumulated event data from storage.
|
|
10322
|
+
* If symbol is provided, clears only that symbol's data.
|
|
10323
|
+
* If symbol is omitted, clears all symbols' data.
|
|
10324
|
+
*
|
|
10325
|
+
* @param symbol - Optional symbol to clear specific symbol data
|
|
10326
|
+
*
|
|
10327
|
+
* @example
|
|
10328
|
+
* ```typescript
|
|
10329
|
+
* const service = new PartialMarkdownService();
|
|
10330
|
+
*
|
|
10331
|
+
* // Clear specific symbol data
|
|
10332
|
+
* await service.clear("BTCUSDT");
|
|
10333
|
+
*
|
|
10334
|
+
* // Clear all symbols' data
|
|
10335
|
+
* await service.clear();
|
|
10336
|
+
* ```
|
|
10337
|
+
*/
|
|
10338
|
+
this.clear = async (symbol) => {
|
|
10339
|
+
this.loggerService.log("partialMarkdownService clear", {
|
|
10340
|
+
symbol,
|
|
10341
|
+
});
|
|
10342
|
+
this.getStorage.clear(symbol);
|
|
10343
|
+
};
|
|
10344
|
+
/**
|
|
10345
|
+
* Initializes the service by subscribing to partial profit/loss events.
|
|
10346
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
10347
|
+
* Automatically called on first use.
|
|
10348
|
+
*
|
|
10349
|
+
* @example
|
|
10350
|
+
* ```typescript
|
|
10351
|
+
* const service = new PartialMarkdownService();
|
|
10352
|
+
* await service.init(); // Subscribe to profit/loss events
|
|
10353
|
+
* ```
|
|
10354
|
+
*/
|
|
10355
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
10356
|
+
this.loggerService.log("partialMarkdownService init");
|
|
10357
|
+
partialProfitSubject.subscribe(this.tickProfit);
|
|
10358
|
+
partialLossSubject.subscribe(this.tickLoss);
|
|
10359
|
+
});
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
|
|
10363
|
+
/**
|
|
10364
|
+
* Global service for partial profit/loss tracking.
|
|
10365
|
+
*
|
|
10366
|
+
* Thin delegation layer that forwards operations to PartialConnectionService.
|
|
10367
|
+
* Provides centralized logging for all partial operations at the global level.
|
|
10368
|
+
*
|
|
10369
|
+
* Architecture:
|
|
10370
|
+
* - Injected into ClientStrategy constructor via IStrategyParams
|
|
10371
|
+
* - Delegates all operations to PartialConnectionService
|
|
10372
|
+
* - Logs operations at "partialGlobalService" level before delegation
|
|
10373
|
+
*
|
|
10374
|
+
* Purpose:
|
|
10375
|
+
* - Single injection point for ClientStrategy (dependency injection pattern)
|
|
10376
|
+
* - Centralized logging for monitoring partial operations
|
|
10377
|
+
* - Layer of abstraction between strategy and connection layer
|
|
10378
|
+
*
|
|
10379
|
+
* @example
|
|
10380
|
+
* ```typescript
|
|
10381
|
+
* // Service injected into ClientStrategy via DI
|
|
10382
|
+
* const strategy = new ClientStrategy({
|
|
10383
|
+
* partial: partialGlobalService,
|
|
10384
|
+
* ...
|
|
10385
|
+
* });
|
|
10386
|
+
*
|
|
10387
|
+
* // Called during signal monitoring
|
|
10388
|
+
* await strategy.params.partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
|
|
10389
|
+
* // Logs at global level → delegates to PartialConnectionService
|
|
10390
|
+
* ```
|
|
10391
|
+
*/
|
|
10392
|
+
class PartialGlobalService {
|
|
10393
|
+
constructor() {
|
|
10394
|
+
/**
|
|
10395
|
+
* Logger service injected from DI container.
|
|
10396
|
+
* Used for logging operations at global service level.
|
|
10397
|
+
*/
|
|
10398
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
10399
|
+
/**
|
|
10400
|
+
* Connection service injected from DI container.
|
|
10401
|
+
* Handles actual ClientPartial instance creation and management.
|
|
10402
|
+
*/
|
|
10403
|
+
this.partialConnectionService = inject(TYPES.partialConnectionService);
|
|
10404
|
+
/**
|
|
10405
|
+
* Processes profit state and emits events for newly reached profit levels.
|
|
10406
|
+
*
|
|
10407
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10408
|
+
*
|
|
10409
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10410
|
+
* @param data - Signal row data
|
|
10411
|
+
* @param currentPrice - Current market price
|
|
10412
|
+
* @param revenuePercent - Current profit percentage (positive value)
|
|
10413
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
10414
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
10415
|
+
* @returns Promise that resolves when profit processing is complete
|
|
10416
|
+
*/
|
|
10417
|
+
this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
|
|
10418
|
+
this.loggerService.log("partialGlobalService profit", {
|
|
10419
|
+
symbol,
|
|
10420
|
+
data,
|
|
10421
|
+
currentPrice,
|
|
10422
|
+
revenuePercent,
|
|
10423
|
+
backtest,
|
|
10424
|
+
when,
|
|
10425
|
+
});
|
|
10426
|
+
return await this.partialConnectionService.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
|
|
10427
|
+
};
|
|
10428
|
+
/**
|
|
10429
|
+
* Processes loss state and emits events for newly reached loss levels.
|
|
10430
|
+
*
|
|
10431
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10432
|
+
*
|
|
10433
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10434
|
+
* @param data - Signal row data
|
|
10435
|
+
* @param currentPrice - Current market price
|
|
10436
|
+
* @param lossPercent - Current loss percentage (negative value)
|
|
10437
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
10438
|
+
* @param when - Event timestamp (current time for live, candle time for backtest)
|
|
10439
|
+
* @returns Promise that resolves when loss processing is complete
|
|
10440
|
+
*/
|
|
10441
|
+
this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
|
|
10442
|
+
this.loggerService.log("partialGlobalService loss", {
|
|
10443
|
+
symbol,
|
|
10444
|
+
data,
|
|
10445
|
+
currentPrice,
|
|
10446
|
+
lossPercent,
|
|
10447
|
+
backtest,
|
|
10448
|
+
when,
|
|
10449
|
+
});
|
|
10450
|
+
return await this.partialConnectionService.loss(symbol, data, currentPrice, lossPercent, backtest, when);
|
|
10451
|
+
};
|
|
10452
|
+
/**
|
|
10453
|
+
* Clears partial profit/loss state when signal closes.
|
|
10454
|
+
*
|
|
10455
|
+
* Logs operation at global service level, then delegates to PartialConnectionService.
|
|
10456
|
+
*
|
|
10457
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
10458
|
+
* @param data - Signal row data
|
|
10459
|
+
* @param priceClose - Final closing price
|
|
10460
|
+
* @returns Promise that resolves when clear is complete
|
|
10461
|
+
*/
|
|
10462
|
+
this.clear = async (symbol, data, priceClose) => {
|
|
10463
|
+
this.loggerService.log("partialGlobalService profit", {
|
|
10464
|
+
symbol,
|
|
10465
|
+
data,
|
|
10466
|
+
priceClose,
|
|
10467
|
+
});
|
|
10468
|
+
return await this.partialConnectionService.clear(symbol, data, priceClose);
|
|
10469
|
+
};
|
|
10470
|
+
}
|
|
10471
|
+
}
|
|
10472
|
+
|
|
10473
|
+
{
|
|
10474
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
10475
|
+
}
|
|
10476
|
+
{
|
|
10477
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
10478
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
10479
|
+
}
|
|
10480
|
+
{
|
|
10481
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
10482
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
10483
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
10484
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
10485
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
10486
|
+
provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
|
|
10487
|
+
provide(TYPES.partialConnectionService, () => new PartialConnectionService());
|
|
10488
|
+
}
|
|
10489
|
+
{
|
|
10490
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
10491
|
+
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
10492
|
+
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
10493
|
+
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
|
|
10494
|
+
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
|
|
10495
|
+
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
|
|
10496
|
+
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
|
|
10497
|
+
}
|
|
10498
|
+
{
|
|
10499
|
+
provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
|
|
10500
|
+
provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
|
|
10501
|
+
provide(TYPES.frameGlobalService, () => new FrameGlobalService());
|
|
10502
|
+
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
10503
|
+
provide(TYPES.riskGlobalService, () => new RiskGlobalService());
|
|
10504
|
+
provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
|
|
10505
|
+
provide(TYPES.partialGlobalService, () => new PartialGlobalService());
|
|
10506
|
+
}
|
|
10507
|
+
{
|
|
10508
|
+
provide(TYPES.liveCommandService, () => new LiveCommandService());
|
|
10509
|
+
provide(TYPES.backtestCommandService, () => new BacktestCommandService());
|
|
10510
|
+
provide(TYPES.walkerCommandService, () => new WalkerCommandService());
|
|
10511
|
+
}
|
|
10512
|
+
{
|
|
10513
|
+
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
|
|
10514
|
+
provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
|
|
10515
|
+
provide(TYPES.walkerLogicPrivateService, () => new WalkerLogicPrivateService());
|
|
10516
|
+
}
|
|
10517
|
+
{
|
|
10518
|
+
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
|
|
7910
10519
|
provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
|
|
7911
10520
|
provide(TYPES.walkerLogicPublicService, () => new WalkerLogicPublicService());
|
|
7912
10521
|
}
|
|
@@ -7917,6 +10526,7 @@ class RiskValidationService {
|
|
|
7917
10526
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
7918
10527
|
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
7919
10528
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
10529
|
+
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
7920
10530
|
}
|
|
7921
10531
|
{
|
|
7922
10532
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -7925,6 +10535,10 @@ class RiskValidationService {
|
|
|
7925
10535
|
provide(TYPES.walkerValidationService, () => new WalkerValidationService());
|
|
7926
10536
|
provide(TYPES.sizingValidationService, () => new SizingValidationService());
|
|
7927
10537
|
provide(TYPES.riskValidationService, () => new RiskValidationService());
|
|
10538
|
+
provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
|
|
10539
|
+
}
|
|
10540
|
+
{
|
|
10541
|
+
provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
|
|
7928
10542
|
}
|
|
7929
10543
|
|
|
7930
10544
|
const baseServices = {
|
|
@@ -7940,6 +10554,8 @@ const connectionServices = {
|
|
|
7940
10554
|
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
7941
10555
|
sizingConnectionService: inject(TYPES.sizingConnectionService),
|
|
7942
10556
|
riskConnectionService: inject(TYPES.riskConnectionService),
|
|
10557
|
+
optimizerConnectionService: inject(TYPES.optimizerConnectionService),
|
|
10558
|
+
partialConnectionService: inject(TYPES.partialConnectionService),
|
|
7943
10559
|
};
|
|
7944
10560
|
const schemaServices = {
|
|
7945
10561
|
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
@@ -7948,6 +10564,7 @@ const schemaServices = {
|
|
|
7948
10564
|
walkerSchemaService: inject(TYPES.walkerSchemaService),
|
|
7949
10565
|
sizingSchemaService: inject(TYPES.sizingSchemaService),
|
|
7950
10566
|
riskSchemaService: inject(TYPES.riskSchemaService),
|
|
10567
|
+
optimizerSchemaService: inject(TYPES.optimizerSchemaService),
|
|
7951
10568
|
};
|
|
7952
10569
|
const globalServices = {
|
|
7953
10570
|
exchangeGlobalService: inject(TYPES.exchangeGlobalService),
|
|
@@ -7955,6 +10572,8 @@ const globalServices = {
|
|
|
7955
10572
|
frameGlobalService: inject(TYPES.frameGlobalService),
|
|
7956
10573
|
sizingGlobalService: inject(TYPES.sizingGlobalService),
|
|
7957
10574
|
riskGlobalService: inject(TYPES.riskGlobalService),
|
|
10575
|
+
optimizerGlobalService: inject(TYPES.optimizerGlobalService),
|
|
10576
|
+
partialGlobalService: inject(TYPES.partialGlobalService),
|
|
7958
10577
|
};
|
|
7959
10578
|
const commandServices = {
|
|
7960
10579
|
liveCommandService: inject(TYPES.liveCommandService),
|
|
@@ -7978,6 +10597,7 @@ const markdownServices = {
|
|
|
7978
10597
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
7979
10598
|
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
7980
10599
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
10600
|
+
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
7981
10601
|
};
|
|
7982
10602
|
const validationServices = {
|
|
7983
10603
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -7986,6 +10606,10 @@ const validationServices = {
|
|
|
7986
10606
|
walkerValidationService: inject(TYPES.walkerValidationService),
|
|
7987
10607
|
sizingValidationService: inject(TYPES.sizingValidationService),
|
|
7988
10608
|
riskValidationService: inject(TYPES.riskValidationService),
|
|
10609
|
+
optimizerValidationService: inject(TYPES.optimizerValidationService),
|
|
10610
|
+
};
|
|
10611
|
+
const templateServices = {
|
|
10612
|
+
optimizerTemplateService: inject(TYPES.optimizerTemplateService),
|
|
7989
10613
|
};
|
|
7990
10614
|
const backtest = {
|
|
7991
10615
|
...baseServices,
|
|
@@ -7998,6 +10622,7 @@ const backtest = {
|
|
|
7998
10622
|
...logicPublicServices,
|
|
7999
10623
|
...markdownServices,
|
|
8000
10624
|
...validationServices,
|
|
10625
|
+
...templateServices,
|
|
8001
10626
|
};
|
|
8002
10627
|
init();
|
|
8003
10628
|
var backtest$1 = backtest;
|
|
@@ -8043,6 +10668,7 @@ const ADD_FRAME_METHOD_NAME = "add.addFrame";
|
|
|
8043
10668
|
const ADD_WALKER_METHOD_NAME = "add.addWalker";
|
|
8044
10669
|
const ADD_SIZING_METHOD_NAME = "add.addSizing";
|
|
8045
10670
|
const ADD_RISK_METHOD_NAME = "add.addRisk";
|
|
10671
|
+
const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizer";
|
|
8046
10672
|
/**
|
|
8047
10673
|
* Registers a trading strategy in the framework.
|
|
8048
10674
|
*
|
|
@@ -8331,8 +10957,102 @@ function addRisk(riskSchema) {
|
|
|
8331
10957
|
backtest$1.loggerService.info(ADD_RISK_METHOD_NAME, {
|
|
8332
10958
|
riskSchema,
|
|
8333
10959
|
});
|
|
8334
|
-
backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
|
|
8335
|
-
backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
|
|
10960
|
+
backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
|
|
10961
|
+
backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
|
|
10962
|
+
}
|
|
10963
|
+
/**
|
|
10964
|
+
* Registers an optimizer configuration in the framework.
|
|
10965
|
+
*
|
|
10966
|
+
* The optimizer generates trading strategies by:
|
|
10967
|
+
* - Collecting data from multiple sources across training periods
|
|
10968
|
+
* - Building LLM conversation history with fetched data
|
|
10969
|
+
* - Generating strategy prompts using getPrompt()
|
|
10970
|
+
* - Creating executable backtest code with templates
|
|
10971
|
+
*
|
|
10972
|
+
* The optimizer produces a complete .mjs file containing:
|
|
10973
|
+
* - Exchange, Frame, Strategy, and Walker configurations
|
|
10974
|
+
* - Multi-timeframe analysis logic
|
|
10975
|
+
* - LLM integration for signal generation
|
|
10976
|
+
* - Event listeners for progress tracking
|
|
10977
|
+
*
|
|
10978
|
+
* @param optimizerSchema - Optimizer configuration object
|
|
10979
|
+
* @param optimizerSchema.optimizerName - Unique optimizer identifier
|
|
10980
|
+
* @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
|
|
10981
|
+
* @param optimizerSchema.rangeTest - Testing time range for strategy validation
|
|
10982
|
+
* @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
|
|
10983
|
+
* @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
|
|
10984
|
+
* @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
|
|
10985
|
+
* @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
|
|
10986
|
+
*
|
|
10987
|
+
* @example
|
|
10988
|
+
* ```typescript
|
|
10989
|
+
* // Basic optimizer with single data source
|
|
10990
|
+
* addOptimizer({
|
|
10991
|
+
* optimizerName: "llm-strategy-generator",
|
|
10992
|
+
* rangeTrain: [
|
|
10993
|
+
* {
|
|
10994
|
+
* note: "Bull market period",
|
|
10995
|
+
* startDate: new Date("2024-01-01"),
|
|
10996
|
+
* endDate: new Date("2024-01-31"),
|
|
10997
|
+
* },
|
|
10998
|
+
* {
|
|
10999
|
+
* note: "Bear market period",
|
|
11000
|
+
* startDate: new Date("2024-02-01"),
|
|
11001
|
+
* endDate: new Date("2024-02-28"),
|
|
11002
|
+
* },
|
|
11003
|
+
* ],
|
|
11004
|
+
* rangeTest: {
|
|
11005
|
+
* note: "Validation period",
|
|
11006
|
+
* startDate: new Date("2024-03-01"),
|
|
11007
|
+
* endDate: new Date("2024-03-31"),
|
|
11008
|
+
* },
|
|
11009
|
+
* source: [
|
|
11010
|
+
* {
|
|
11011
|
+
* name: "historical-backtests",
|
|
11012
|
+
* fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
|
|
11013
|
+
* // Fetch historical backtest results from database
|
|
11014
|
+
* return await db.backtests.find({
|
|
11015
|
+
* symbol,
|
|
11016
|
+
* date: { $gte: startDate, $lte: endDate },
|
|
11017
|
+
* })
|
|
11018
|
+
* .skip(offset)
|
|
11019
|
+
* .limit(limit);
|
|
11020
|
+
* },
|
|
11021
|
+
* user: async (symbol, data, name) => {
|
|
11022
|
+
* return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
|
|
11023
|
+
* },
|
|
11024
|
+
* assistant: async (symbol, data, name) => {
|
|
11025
|
+
* return "Historical data analyzed successfully";
|
|
11026
|
+
* },
|
|
11027
|
+
* },
|
|
11028
|
+
* ],
|
|
11029
|
+
* getPrompt: async (symbol, messages) => {
|
|
11030
|
+
* // Generate strategy prompt from conversation
|
|
11031
|
+
* return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
|
|
11032
|
+
* },
|
|
11033
|
+
* callbacks: {
|
|
11034
|
+
* onData: (symbol, strategyData) => {
|
|
11035
|
+
* console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
|
|
11036
|
+
* },
|
|
11037
|
+
* onCode: (symbol, code) => {
|
|
11038
|
+
* console.log(`Generated ${code.length} characters of code for ${symbol}`);
|
|
11039
|
+
* },
|
|
11040
|
+
* onDump: (symbol, filepath) => {
|
|
11041
|
+
* console.log(`Saved strategy to ${filepath}`);
|
|
11042
|
+
* },
|
|
11043
|
+
* onSourceData: (symbol, sourceName, data, startDate, endDate) => {
|
|
11044
|
+
* console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
|
|
11045
|
+
* },
|
|
11046
|
+
* },
|
|
11047
|
+
* });
|
|
11048
|
+
* ```
|
|
11049
|
+
*/
|
|
11050
|
+
function addOptimizer(optimizerSchema) {
|
|
11051
|
+
backtest$1.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
|
|
11052
|
+
optimizerSchema,
|
|
11053
|
+
});
|
|
11054
|
+
backtest$1.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
|
|
11055
|
+
backtest$1.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
|
|
8336
11056
|
}
|
|
8337
11057
|
|
|
8338
11058
|
const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
|
|
@@ -8341,6 +11061,7 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
|
|
|
8341
11061
|
const LIST_WALKERS_METHOD_NAME = "list.listWalkers";
|
|
8342
11062
|
const LIST_SIZINGS_METHOD_NAME = "list.listSizings";
|
|
8343
11063
|
const LIST_RISKS_METHOD_NAME = "list.listRisks";
|
|
11064
|
+
const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizers";
|
|
8344
11065
|
/**
|
|
8345
11066
|
* Returns a list of all registered exchange schemas.
|
|
8346
11067
|
*
|
|
@@ -8538,6 +11259,46 @@ async function listRisks() {
|
|
|
8538
11259
|
backtest$1.loggerService.log(LIST_RISKS_METHOD_NAME);
|
|
8539
11260
|
return await backtest$1.riskValidationService.list();
|
|
8540
11261
|
}
|
|
11262
|
+
/**
|
|
11263
|
+
* Returns a list of all registered optimizer schemas.
|
|
11264
|
+
*
|
|
11265
|
+
* Retrieves all optimizers that have been registered via addOptimizer().
|
|
11266
|
+
* Useful for debugging, documentation, or building dynamic UIs.
|
|
11267
|
+
*
|
|
11268
|
+
* @returns Array of optimizer schemas with their configurations
|
|
11269
|
+
*
|
|
11270
|
+
* @example
|
|
11271
|
+
* ```typescript
|
|
11272
|
+
* import { listOptimizers, addOptimizer } from "backtest-kit";
|
|
11273
|
+
*
|
|
11274
|
+
* addOptimizer({
|
|
11275
|
+
* optimizerName: "llm-strategy-generator",
|
|
11276
|
+
* note: "Generates trading strategies using LLM",
|
|
11277
|
+
* rangeTrain: [
|
|
11278
|
+
* {
|
|
11279
|
+
* note: "Training period 1",
|
|
11280
|
+
* startDate: new Date("2024-01-01"),
|
|
11281
|
+
* endDate: new Date("2024-01-31"),
|
|
11282
|
+
* },
|
|
11283
|
+
* ],
|
|
11284
|
+
* rangeTest: {
|
|
11285
|
+
* note: "Testing period",
|
|
11286
|
+
* startDate: new Date("2024-02-01"),
|
|
11287
|
+
* endDate: new Date("2024-02-28"),
|
|
11288
|
+
* },
|
|
11289
|
+
* source: [],
|
|
11290
|
+
* getPrompt: async (symbol, messages) => "Generate strategy",
|
|
11291
|
+
* });
|
|
11292
|
+
*
|
|
11293
|
+
* const optimizers = listOptimizers();
|
|
11294
|
+
* console.log(optimizers);
|
|
11295
|
+
* // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
|
|
11296
|
+
* ```
|
|
11297
|
+
*/
|
|
11298
|
+
async function listOptimizers() {
|
|
11299
|
+
backtest$1.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
|
|
11300
|
+
return await backtest$1.optimizerValidationService.list();
|
|
11301
|
+
}
|
|
8541
11302
|
|
|
8542
11303
|
const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
|
|
8543
11304
|
const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
|
|
@@ -8552,12 +11313,17 @@ const LISTEN_DONE_BACKTEST_METHOD_NAME = "event.listenDoneBacktest";
|
|
|
8552
11313
|
const LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME = "event.listenDoneBacktestOnce";
|
|
8553
11314
|
const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
|
|
8554
11315
|
const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
|
|
8555
|
-
const LISTEN_PROGRESS_METHOD_NAME = "event.
|
|
11316
|
+
const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
|
|
11317
|
+
const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
|
|
8556
11318
|
const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
|
|
8557
11319
|
const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
|
|
8558
11320
|
const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
|
|
8559
11321
|
const LISTEN_WALKER_COMPLETE_METHOD_NAME = "event.listenWalkerComplete";
|
|
8560
11322
|
const LISTEN_VALIDATION_METHOD_NAME = "event.listenValidation";
|
|
11323
|
+
const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
|
|
11324
|
+
const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
|
|
11325
|
+
const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
|
|
11326
|
+
const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
|
|
8561
11327
|
/**
|
|
8562
11328
|
* Subscribes to all signal events with queued async processing.
|
|
8563
11329
|
*
|
|
@@ -8943,9 +11709,9 @@ function listenDoneWalkerOnce(filterFn, fn) {
|
|
|
8943
11709
|
*
|
|
8944
11710
|
* @example
|
|
8945
11711
|
* ```typescript
|
|
8946
|
-
* import {
|
|
11712
|
+
* import { listenBacktestProgress, Backtest } from "backtest-kit";
|
|
8947
11713
|
*
|
|
8948
|
-
* const unsubscribe =
|
|
11714
|
+
* const unsubscribe = listenBacktestProgress((event) => {
|
|
8949
11715
|
* console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
|
|
8950
11716
|
* console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
|
|
8951
11717
|
* console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
|
|
@@ -8961,9 +11727,43 @@ function listenDoneWalkerOnce(filterFn, fn) {
|
|
|
8961
11727
|
* unsubscribe();
|
|
8962
11728
|
* ```
|
|
8963
11729
|
*/
|
|
8964
|
-
function
|
|
11730
|
+
function listenBacktestProgress(fn) {
|
|
8965
11731
|
backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
|
|
8966
|
-
return
|
|
11732
|
+
return progressBacktestEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
11733
|
+
}
|
|
11734
|
+
/**
|
|
11735
|
+
* Subscribes to walker progress events with queued async processing.
|
|
11736
|
+
*
|
|
11737
|
+
* Emits during Walker.run() execution after each strategy completes.
|
|
11738
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
11739
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
11740
|
+
*
|
|
11741
|
+
* @param fn - Callback function to handle walker progress events
|
|
11742
|
+
* @returns Unsubscribe function to stop listening to events
|
|
11743
|
+
*
|
|
11744
|
+
* @example
|
|
11745
|
+
* ```typescript
|
|
11746
|
+
* import { listenWalkerProgress, Walker } from "backtest-kit";
|
|
11747
|
+
*
|
|
11748
|
+
* const unsubscribe = listenWalkerProgress((event) => {
|
|
11749
|
+
* console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
|
|
11750
|
+
* console.log(`${event.processedStrategies} / ${event.totalStrategies} strategies`);
|
|
11751
|
+
* console.log(`Walker: ${event.walkerName}, Symbol: ${event.symbol}`);
|
|
11752
|
+
* });
|
|
11753
|
+
*
|
|
11754
|
+
* Walker.run("BTCUSDT", {
|
|
11755
|
+
* walkerName: "my-walker",
|
|
11756
|
+
* exchangeName: "binance",
|
|
11757
|
+
* frameName: "1d-backtest"
|
|
11758
|
+
* });
|
|
11759
|
+
*
|
|
11760
|
+
* // Later: stop listening
|
|
11761
|
+
* unsubscribe();
|
|
11762
|
+
* ```
|
|
11763
|
+
*/
|
|
11764
|
+
function listenWalkerProgress(fn) {
|
|
11765
|
+
backtest$1.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
|
|
11766
|
+
return progressWalkerEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
8967
11767
|
}
|
|
8968
11768
|
/**
|
|
8969
11769
|
* Subscribes to performance metric events with queued async processing.
|
|
@@ -9141,6 +11941,130 @@ function listenValidation(fn) {
|
|
|
9141
11941
|
backtest$1.loggerService.log(LISTEN_VALIDATION_METHOD_NAME);
|
|
9142
11942
|
return validationSubject.subscribe(functoolsKit.queued(async (error) => fn(error)));
|
|
9143
11943
|
}
|
|
11944
|
+
/**
|
|
11945
|
+
* Subscribes to partial profit level events with queued async processing.
|
|
11946
|
+
*
|
|
11947
|
+
* Emits when a signal reaches a profit level milestone (10%, 20%, 30%, etc).
|
|
11948
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
11949
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
11950
|
+
*
|
|
11951
|
+
* @param fn - Callback function to handle partial profit events
|
|
11952
|
+
* @returns Unsubscribe function to stop listening to events
|
|
11953
|
+
*
|
|
11954
|
+
* @example
|
|
11955
|
+
* ```typescript
|
|
11956
|
+
* import { listenPartialProfit } from "./function/event";
|
|
11957
|
+
*
|
|
11958
|
+
* const unsubscribe = listenPartialProfit((event) => {
|
|
11959
|
+
* console.log(`Signal ${event.data.id} reached ${event.level}% profit`);
|
|
11960
|
+
* console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
|
|
11961
|
+
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
11962
|
+
* });
|
|
11963
|
+
*
|
|
11964
|
+
* // Later: stop listening
|
|
11965
|
+
* unsubscribe();
|
|
11966
|
+
* ```
|
|
11967
|
+
*/
|
|
11968
|
+
function listenPartialProfit(fn) {
|
|
11969
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_METHOD_NAME);
|
|
11970
|
+
return partialProfitSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
11971
|
+
}
|
|
11972
|
+
/**
|
|
11973
|
+
* Subscribes to filtered partial profit level events with one-time execution.
|
|
11974
|
+
*
|
|
11975
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
11976
|
+
* and automatically unsubscribes. Useful for waiting for specific profit conditions.
|
|
11977
|
+
*
|
|
11978
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
11979
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
11980
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
11981
|
+
*
|
|
11982
|
+
* @example
|
|
11983
|
+
* ```typescript
|
|
11984
|
+
* import { listenPartialProfitOnce } from "./function/event";
|
|
11985
|
+
*
|
|
11986
|
+
* // Wait for first 50% profit level on any signal
|
|
11987
|
+
* listenPartialProfitOnce(
|
|
11988
|
+
* (event) => event.level === 50,
|
|
11989
|
+
* (event) => console.log("50% profit reached:", event.data.id)
|
|
11990
|
+
* );
|
|
11991
|
+
*
|
|
11992
|
+
* // Wait for 30% profit on BTCUSDT
|
|
11993
|
+
* const cancel = listenPartialProfitOnce(
|
|
11994
|
+
* (event) => event.symbol === "BTCUSDT" && event.level === 30,
|
|
11995
|
+
* (event) => console.log("BTCUSDT hit 30% profit")
|
|
11996
|
+
* );
|
|
11997
|
+
*
|
|
11998
|
+
* // Cancel if needed before event fires
|
|
11999
|
+
* cancel();
|
|
12000
|
+
* ```
|
|
12001
|
+
*/
|
|
12002
|
+
function listenPartialProfitOnce(filterFn, fn) {
|
|
12003
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME);
|
|
12004
|
+
return partialProfitSubject.filter(filterFn).once(fn);
|
|
12005
|
+
}
|
|
12006
|
+
/**
|
|
12007
|
+
* Subscribes to partial loss level events with queued async processing.
|
|
12008
|
+
*
|
|
12009
|
+
* Emits when a signal reaches a loss level milestone (10%, 20%, 30%, etc).
|
|
12010
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
12011
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
12012
|
+
*
|
|
12013
|
+
* @param fn - Callback function to handle partial loss events
|
|
12014
|
+
* @returns Unsubscribe function to stop listening to events
|
|
12015
|
+
*
|
|
12016
|
+
* @example
|
|
12017
|
+
* ```typescript
|
|
12018
|
+
* import { listenPartialLoss } from "./function/event";
|
|
12019
|
+
*
|
|
12020
|
+
* const unsubscribe = listenPartialLoss((event) => {
|
|
12021
|
+
* console.log(`Signal ${event.data.id} reached ${event.level}% loss`);
|
|
12022
|
+
* console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
|
|
12023
|
+
* console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
|
|
12024
|
+
* });
|
|
12025
|
+
*
|
|
12026
|
+
* // Later: stop listening
|
|
12027
|
+
* unsubscribe();
|
|
12028
|
+
* ```
|
|
12029
|
+
*/
|
|
12030
|
+
function listenPartialLoss(fn) {
|
|
12031
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_METHOD_NAME);
|
|
12032
|
+
return partialLossSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
12033
|
+
}
|
|
12034
|
+
/**
|
|
12035
|
+
* Subscribes to filtered partial loss level events with one-time execution.
|
|
12036
|
+
*
|
|
12037
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
12038
|
+
* and automatically unsubscribes. Useful for waiting for specific loss conditions.
|
|
12039
|
+
*
|
|
12040
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
12041
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
12042
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
12043
|
+
*
|
|
12044
|
+
* @example
|
|
12045
|
+
* ```typescript
|
|
12046
|
+
* import { listenPartialLossOnce } from "./function/event";
|
|
12047
|
+
*
|
|
12048
|
+
* // Wait for first 20% loss level on any signal
|
|
12049
|
+
* listenPartialLossOnce(
|
|
12050
|
+
* (event) => event.level === 20,
|
|
12051
|
+
* (event) => console.log("20% loss reached:", event.data.id)
|
|
12052
|
+
* );
|
|
12053
|
+
*
|
|
12054
|
+
* // Wait for 10% loss on ETHUSDT in live mode
|
|
12055
|
+
* const cancel = listenPartialLossOnce(
|
|
12056
|
+
* (event) => event.symbol === "ETHUSDT" && event.level === 10 && !event.backtest,
|
|
12057
|
+
* (event) => console.log("ETHUSDT hit 10% loss in live mode")
|
|
12058
|
+
* );
|
|
12059
|
+
*
|
|
12060
|
+
* // Cancel if needed before event fires
|
|
12061
|
+
* cancel();
|
|
12062
|
+
* ```
|
|
12063
|
+
*/
|
|
12064
|
+
function listenPartialLossOnce(filterFn, fn) {
|
|
12065
|
+
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
|
|
12066
|
+
return partialLossSubject.filter(filterFn).once(fn);
|
|
12067
|
+
}
|
|
9144
12068
|
|
|
9145
12069
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
9146
12070
|
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
@@ -9437,11 +12361,11 @@ class BacktestUtils {
|
|
|
9437
12361
|
* Saves strategy report to disk.
|
|
9438
12362
|
*
|
|
9439
12363
|
* @param strategyName - Strategy name to save report for
|
|
9440
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12364
|
+
* @param path - Optional directory path to save report (default: "./dump/backtest")
|
|
9441
12365
|
*
|
|
9442
12366
|
* @example
|
|
9443
12367
|
* ```typescript
|
|
9444
|
-
* // Save to default path: ./
|
|
12368
|
+
* // Save to default path: ./dump/backtest/my-strategy.md
|
|
9445
12369
|
* await Backtest.dump("my-strategy");
|
|
9446
12370
|
*
|
|
9447
12371
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9646,11 +12570,11 @@ class LiveUtils {
|
|
|
9646
12570
|
* Saves strategy report to disk.
|
|
9647
12571
|
*
|
|
9648
12572
|
* @param strategyName - Strategy name to save report for
|
|
9649
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12573
|
+
* @param path - Optional directory path to save report (default: "./dump/live")
|
|
9650
12574
|
*
|
|
9651
12575
|
* @example
|
|
9652
12576
|
* ```typescript
|
|
9653
|
-
* // Save to default path: ./
|
|
12577
|
+
* // Save to default path: ./dump/live/my-strategy.md
|
|
9654
12578
|
* await Live.dump("my-strategy");
|
|
9655
12579
|
*
|
|
9656
12580
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9686,7 +12610,6 @@ const Live = new LiveUtils();
|
|
|
9686
12610
|
const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
|
|
9687
12611
|
const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
|
|
9688
12612
|
const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
|
|
9689
|
-
const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
|
|
9690
12613
|
/**
|
|
9691
12614
|
* Utility class for scheduled signals reporting operations.
|
|
9692
12615
|
*
|
|
@@ -9754,11 +12677,11 @@ class ScheduleUtils {
|
|
|
9754
12677
|
* Saves strategy report to disk.
|
|
9755
12678
|
*
|
|
9756
12679
|
* @param strategyName - Strategy name to save report for
|
|
9757
|
-
* @param path - Optional directory path to save report (default: "./
|
|
12680
|
+
* @param path - Optional directory path to save report (default: "./dump/schedule")
|
|
9758
12681
|
*
|
|
9759
12682
|
* @example
|
|
9760
12683
|
* ```typescript
|
|
9761
|
-
* // Save to default path: ./
|
|
12684
|
+
* // Save to default path: ./dump/schedule/my-strategy.md
|
|
9762
12685
|
* await Schedule.dump("my-strategy");
|
|
9763
12686
|
*
|
|
9764
12687
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
@@ -9772,28 +12695,6 @@ class ScheduleUtils {
|
|
|
9772
12695
|
});
|
|
9773
12696
|
await backtest$1.scheduleMarkdownService.dump(strategyName, path);
|
|
9774
12697
|
};
|
|
9775
|
-
/**
|
|
9776
|
-
* Clears accumulated scheduled signal data from storage.
|
|
9777
|
-
* If strategyName is provided, clears only that strategy's data.
|
|
9778
|
-
* If strategyName is omitted, clears all strategies' data.
|
|
9779
|
-
*
|
|
9780
|
-
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
9781
|
-
*
|
|
9782
|
-
* @example
|
|
9783
|
-
* ```typescript
|
|
9784
|
-
* // Clear specific strategy data
|
|
9785
|
-
* await Schedule.clear("my-strategy");
|
|
9786
|
-
*
|
|
9787
|
-
* // Clear all strategies' data
|
|
9788
|
-
* await Schedule.clear();
|
|
9789
|
-
* ```
|
|
9790
|
-
*/
|
|
9791
|
-
this.clear = async (strategyName) => {
|
|
9792
|
-
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
|
|
9793
|
-
strategyName,
|
|
9794
|
-
});
|
|
9795
|
-
await backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9796
|
-
};
|
|
9797
12698
|
}
|
|
9798
12699
|
}
|
|
9799
12700
|
/**
|
|
@@ -9900,21 +12801,21 @@ class Performance {
|
|
|
9900
12801
|
* Saves performance report to disk.
|
|
9901
12802
|
*
|
|
9902
12803
|
* Creates directory if it doesn't exist.
|
|
9903
|
-
* Default path: ./
|
|
12804
|
+
* Default path: ./dump/performance/{strategyName}.md
|
|
9904
12805
|
*
|
|
9905
12806
|
* @param strategyName - Strategy name to save report for
|
|
9906
12807
|
* @param path - Optional custom directory path
|
|
9907
12808
|
*
|
|
9908
12809
|
* @example
|
|
9909
12810
|
* ```typescript
|
|
9910
|
-
* // Save to default path: ./
|
|
12811
|
+
* // Save to default path: ./dump/performance/my-strategy.md
|
|
9911
12812
|
* await Performance.dump("my-strategy");
|
|
9912
12813
|
*
|
|
9913
12814
|
* // Save to custom path: ./reports/perf/my-strategy.md
|
|
9914
12815
|
* await Performance.dump("my-strategy", "./reports/perf");
|
|
9915
12816
|
* ```
|
|
9916
12817
|
*/
|
|
9917
|
-
static async dump(strategyName, path = "./
|
|
12818
|
+
static async dump(strategyName, path = "./dump/performance") {
|
|
9918
12819
|
return backtest$1.performanceMarkdownService.dump(strategyName, path);
|
|
9919
12820
|
}
|
|
9920
12821
|
/**
|
|
@@ -10116,11 +13017,11 @@ class WalkerUtils {
|
|
|
10116
13017
|
*
|
|
10117
13018
|
* @param symbol - Trading symbol
|
|
10118
13019
|
* @param walkerName - Walker name to save report for
|
|
10119
|
-
* @param path - Optional directory path to save report (default: "./
|
|
13020
|
+
* @param path - Optional directory path to save report (default: "./dump/walker")
|
|
10120
13021
|
*
|
|
10121
13022
|
* @example
|
|
10122
13023
|
* ```typescript
|
|
10123
|
-
* // Save to default path: ./
|
|
13024
|
+
* // Save to default path: ./dump/walker/my-walker.md
|
|
10124
13025
|
* await Walker.dump("BTCUSDT", "my-walker");
|
|
10125
13026
|
*
|
|
10126
13027
|
* // Save to custom path: ./custom/path/my-walker.md
|
|
@@ -10249,11 +13150,11 @@ class HeatUtils {
|
|
|
10249
13150
|
* Default filename: {strategyName}.md
|
|
10250
13151
|
*
|
|
10251
13152
|
* @param strategyName - Strategy name to save heatmap report for
|
|
10252
|
-
* @param path - Optional directory path to save report (default: "./
|
|
13153
|
+
* @param path - Optional directory path to save report (default: "./dump/heatmap")
|
|
10253
13154
|
*
|
|
10254
13155
|
* @example
|
|
10255
13156
|
* ```typescript
|
|
10256
|
-
* // Save to default path: ./
|
|
13157
|
+
* // Save to default path: ./dump/heatmap/my-strategy.md
|
|
10257
13158
|
* await Heat.dump("my-strategy");
|
|
10258
13159
|
*
|
|
10259
13160
|
* // Save to custom path: ./reports/my-strategy.md
|
|
@@ -10416,20 +13317,279 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
|
|
|
10416
13317
|
};
|
|
10417
13318
|
const PositionSize = PositionSizeUtils;
|
|
10418
13319
|
|
|
13320
|
+
const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
|
|
13321
|
+
const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
|
|
13322
|
+
const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
|
|
13323
|
+
/**
|
|
13324
|
+
* Public API utilities for optimizer operations.
|
|
13325
|
+
* Provides high-level methods for strategy generation and code export.
|
|
13326
|
+
*
|
|
13327
|
+
* Usage:
|
|
13328
|
+
* ```typescript
|
|
13329
|
+
* import { Optimizer } from "backtest-kit";
|
|
13330
|
+
*
|
|
13331
|
+
* // Get strategy data
|
|
13332
|
+
* const strategies = await Optimizer.getData("BTCUSDT", {
|
|
13333
|
+
* optimizerName: "my-optimizer"
|
|
13334
|
+
* });
|
|
13335
|
+
*
|
|
13336
|
+
* // Generate code
|
|
13337
|
+
* const code = await Optimizer.getCode("BTCUSDT", {
|
|
13338
|
+
* optimizerName: "my-optimizer"
|
|
13339
|
+
* });
|
|
13340
|
+
*
|
|
13341
|
+
* // Save to file
|
|
13342
|
+
* await Optimizer.dump("BTCUSDT", {
|
|
13343
|
+
* optimizerName: "my-optimizer"
|
|
13344
|
+
* }, "./output");
|
|
13345
|
+
* ```
|
|
13346
|
+
*/
|
|
13347
|
+
class OptimizerUtils {
|
|
13348
|
+
constructor() {
|
|
13349
|
+
/**
|
|
13350
|
+
* Fetches data from all sources and generates strategy metadata.
|
|
13351
|
+
* Processes each training range and builds LLM conversation history.
|
|
13352
|
+
*
|
|
13353
|
+
* @param symbol - Trading pair symbol
|
|
13354
|
+
* @param context - Context with optimizerName
|
|
13355
|
+
* @returns Array of generated strategies with conversation context
|
|
13356
|
+
* @throws Error if optimizer not found
|
|
13357
|
+
*/
|
|
13358
|
+
this.getData = async (symbol, context) => {
|
|
13359
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
|
|
13360
|
+
symbol,
|
|
13361
|
+
context,
|
|
13362
|
+
});
|
|
13363
|
+
return await backtest$1.optimizerGlobalService.getData(symbol, context.optimizerName);
|
|
13364
|
+
};
|
|
13365
|
+
/**
|
|
13366
|
+
* Generates complete executable strategy code.
|
|
13367
|
+
* Includes imports, helpers, strategies, walker, and launcher.
|
|
13368
|
+
*
|
|
13369
|
+
* @param symbol - Trading pair symbol
|
|
13370
|
+
* @param context - Context with optimizerName
|
|
13371
|
+
* @returns Generated TypeScript/JavaScript code as string
|
|
13372
|
+
* @throws Error if optimizer not found
|
|
13373
|
+
*/
|
|
13374
|
+
this.getCode = async (symbol, context) => {
|
|
13375
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
|
|
13376
|
+
symbol,
|
|
13377
|
+
context,
|
|
13378
|
+
});
|
|
13379
|
+
return await backtest$1.optimizerGlobalService.getCode(symbol, context.optimizerName);
|
|
13380
|
+
};
|
|
13381
|
+
/**
|
|
13382
|
+
* Generates and saves strategy code to file.
|
|
13383
|
+
* Creates directory if needed, writes .mjs file.
|
|
13384
|
+
*
|
|
13385
|
+
* Format: `{optimizerName}_{symbol}.mjs`
|
|
13386
|
+
*
|
|
13387
|
+
* @param symbol - Trading pair symbol
|
|
13388
|
+
* @param context - Context with optimizerName
|
|
13389
|
+
* @param path - Output directory path (default: "./")
|
|
13390
|
+
* @throws Error if optimizer not found or file write fails
|
|
13391
|
+
*/
|
|
13392
|
+
this.dump = async (symbol, context, path) => {
|
|
13393
|
+
backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
|
|
13394
|
+
symbol,
|
|
13395
|
+
context,
|
|
13396
|
+
path,
|
|
13397
|
+
});
|
|
13398
|
+
await backtest$1.optimizerGlobalService.dump(symbol, context.optimizerName, path);
|
|
13399
|
+
};
|
|
13400
|
+
}
|
|
13401
|
+
}
|
|
13402
|
+
/**
|
|
13403
|
+
* Singleton instance of OptimizerUtils.
|
|
13404
|
+
* Public API for optimizer operations.
|
|
13405
|
+
*
|
|
13406
|
+
* @example
|
|
13407
|
+
* ```typescript
|
|
13408
|
+
* import { Optimizer } from "backtest-kit";
|
|
13409
|
+
*
|
|
13410
|
+
* await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
|
|
13411
|
+
* ```
|
|
13412
|
+
*/
|
|
13413
|
+
const Optimizer = new OptimizerUtils();
|
|
13414
|
+
|
|
13415
|
+
const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
|
|
13416
|
+
const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
|
|
13417
|
+
const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
|
|
13418
|
+
/**
|
|
13419
|
+
* Utility class for accessing partial profit/loss reports and statistics.
|
|
13420
|
+
*
|
|
13421
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
13422
|
+
* accumulated by PartialMarkdownService from partial profit/loss events.
|
|
13423
|
+
*
|
|
13424
|
+
* Features:
|
|
13425
|
+
* - Statistical data extraction (total profit/loss events count)
|
|
13426
|
+
* - Markdown report generation with event tables
|
|
13427
|
+
* - File export to disk
|
|
13428
|
+
*
|
|
13429
|
+
* Data source:
|
|
13430
|
+
* - PartialMarkdownService listens to partialProfitSubject/partialLossSubject
|
|
13431
|
+
* - Accumulates events in ReportStorage (max 250 events per symbol)
|
|
13432
|
+
* - Events include: timestamp, action, symbol, signalId, position, level, price, mode
|
|
13433
|
+
*
|
|
13434
|
+
* @example
|
|
13435
|
+
* ```typescript
|
|
13436
|
+
* import { Partial } from "./classes/Partial";
|
|
13437
|
+
*
|
|
13438
|
+
* // Get statistical data for BTCUSDT
|
|
13439
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13440
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
13441
|
+
* console.log(`Profit events: ${stats.totalProfit}`);
|
|
13442
|
+
* console.log(`Loss events: ${stats.totalLoss}`);
|
|
13443
|
+
*
|
|
13444
|
+
* // Generate markdown report
|
|
13445
|
+
* const markdown = await Partial.getReport("BTCUSDT");
|
|
13446
|
+
* console.log(markdown); // Formatted table with all events
|
|
13447
|
+
*
|
|
13448
|
+
* // Export report to file
|
|
13449
|
+
* await Partial.dump("BTCUSDT"); // Saves to ./dump/partial/BTCUSDT.md
|
|
13450
|
+
* await Partial.dump("BTCUSDT", "./custom/path"); // Custom directory
|
|
13451
|
+
* ```
|
|
13452
|
+
*/
|
|
13453
|
+
class PartialUtils {
|
|
13454
|
+
constructor() {
|
|
13455
|
+
/**
|
|
13456
|
+
* Retrieves statistical data from accumulated partial profit/loss events.
|
|
13457
|
+
*
|
|
13458
|
+
* Delegates to PartialMarkdownService.getData() which reads from ReportStorage.
|
|
13459
|
+
* Returns aggregated metrics calculated from all profit and loss events.
|
|
13460
|
+
*
|
|
13461
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13462
|
+
* @returns Promise resolving to PartialStatistics object with counts and event list
|
|
13463
|
+
*
|
|
13464
|
+
* @example
|
|
13465
|
+
* ```typescript
|
|
13466
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13467
|
+
*
|
|
13468
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
13469
|
+
* console.log(`Profit events: ${stats.totalProfit} (${(stats.totalProfit / stats.totalEvents * 100).toFixed(1)}%)`);
|
|
13470
|
+
* console.log(`Loss events: ${stats.totalLoss} (${(stats.totalLoss / stats.totalEvents * 100).toFixed(1)}%)`);
|
|
13471
|
+
*
|
|
13472
|
+
* // Iterate through all events
|
|
13473
|
+
* for (const event of stats.eventList) {
|
|
13474
|
+
* console.log(`${event.action.toUpperCase()}: Signal ${event.signalId} reached ${event.level}%`);
|
|
13475
|
+
* }
|
|
13476
|
+
* ```
|
|
13477
|
+
*/
|
|
13478
|
+
this.getData = async (symbol) => {
|
|
13479
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol });
|
|
13480
|
+
return await backtest$1.partialMarkdownService.getData(symbol);
|
|
13481
|
+
};
|
|
13482
|
+
/**
|
|
13483
|
+
* Generates markdown report with all partial profit/loss events for a symbol.
|
|
13484
|
+
*
|
|
13485
|
+
* Creates formatted table containing:
|
|
13486
|
+
* - Action (PROFIT/LOSS)
|
|
13487
|
+
* - Symbol
|
|
13488
|
+
* - Signal ID
|
|
13489
|
+
* - Position (LONG/SHORT)
|
|
13490
|
+
* - Level % (+10%, -20%, etc)
|
|
13491
|
+
* - Current Price
|
|
13492
|
+
* - Timestamp (ISO 8601)
|
|
13493
|
+
* - Mode (Backtest/Live)
|
|
13494
|
+
*
|
|
13495
|
+
* Also includes summary statistics at the end.
|
|
13496
|
+
*
|
|
13497
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13498
|
+
* @returns Promise resolving to markdown formatted report string
|
|
13499
|
+
*
|
|
13500
|
+
* @example
|
|
13501
|
+
* ```typescript
|
|
13502
|
+
* const markdown = await Partial.getReport("BTCUSDT");
|
|
13503
|
+
* console.log(markdown);
|
|
13504
|
+
*
|
|
13505
|
+
* // Output:
|
|
13506
|
+
* // # Partial Profit/Loss Report: BTCUSDT
|
|
13507
|
+
* //
|
|
13508
|
+
* // | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
|
|
13509
|
+
* // | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
13510
|
+
* // | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
|
|
13511
|
+
* // | LOSS | BTCUSDT | abc123 | LONG | -10% | 49000.00000000 USD | 2024-01-15T11:00:00.000Z | Backtest |
|
|
13512
|
+
* //
|
|
13513
|
+
* // **Total events:** 2
|
|
13514
|
+
* // **Profit events:** 1
|
|
13515
|
+
* // **Loss events:** 1
|
|
13516
|
+
* ```
|
|
13517
|
+
*/
|
|
13518
|
+
this.getReport = async (symbol) => {
|
|
13519
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol });
|
|
13520
|
+
return await backtest$1.partialMarkdownService.getReport(symbol);
|
|
13521
|
+
};
|
|
13522
|
+
/**
|
|
13523
|
+
* Generates and saves markdown report to file.
|
|
13524
|
+
*
|
|
13525
|
+
* Creates directory if it doesn't exist.
|
|
13526
|
+
* Filename format: {symbol}.md (e.g., "BTCUSDT.md")
|
|
13527
|
+
*
|
|
13528
|
+
* Delegates to PartialMarkdownService.dump() which:
|
|
13529
|
+
* 1. Generates markdown report via getReport()
|
|
13530
|
+
* 2. Creates output directory (recursive mkdir)
|
|
13531
|
+
* 3. Writes file with UTF-8 encoding
|
|
13532
|
+
* 4. Logs success/failure to console
|
|
13533
|
+
*
|
|
13534
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
13535
|
+
* @param path - Output directory path (default: "./dump/partial")
|
|
13536
|
+
* @returns Promise that resolves when file is written
|
|
13537
|
+
*
|
|
13538
|
+
* @example
|
|
13539
|
+
* ```typescript
|
|
13540
|
+
* // Save to default path: ./dump/partial/BTCUSDT.md
|
|
13541
|
+
* await Partial.dump("BTCUSDT");
|
|
13542
|
+
*
|
|
13543
|
+
* // Save to custom path: ./reports/partial/BTCUSDT.md
|
|
13544
|
+
* await Partial.dump("BTCUSDT", "./reports/partial");
|
|
13545
|
+
*
|
|
13546
|
+
* // After multiple symbols backtested, export all reports
|
|
13547
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
|
|
13548
|
+
* await Partial.dump(symbol, "./backtest-results");
|
|
13549
|
+
* }
|
|
13550
|
+
* ```
|
|
13551
|
+
*/
|
|
13552
|
+
this.dump = async (symbol, path) => {
|
|
13553
|
+
backtest$1.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, path });
|
|
13554
|
+
await backtest$1.partialMarkdownService.dump(symbol, path);
|
|
13555
|
+
};
|
|
13556
|
+
}
|
|
13557
|
+
}
|
|
13558
|
+
/**
|
|
13559
|
+
* Global singleton instance of PartialUtils.
|
|
13560
|
+
* Provides static-like access to partial profit/loss reporting methods.
|
|
13561
|
+
*
|
|
13562
|
+
* @example
|
|
13563
|
+
* ```typescript
|
|
13564
|
+
* import { Partial } from "backtest-kit";
|
|
13565
|
+
*
|
|
13566
|
+
* // Usage same as PartialUtils methods
|
|
13567
|
+
* const stats = await Partial.getData("BTCUSDT");
|
|
13568
|
+
* const report = await Partial.getReport("BTCUSDT");
|
|
13569
|
+
* await Partial.dump("BTCUSDT");
|
|
13570
|
+
* ```
|
|
13571
|
+
*/
|
|
13572
|
+
const Partial = new PartialUtils();
|
|
13573
|
+
|
|
10419
13574
|
exports.Backtest = Backtest;
|
|
10420
13575
|
exports.ExecutionContextService = ExecutionContextService;
|
|
10421
13576
|
exports.Heat = Heat;
|
|
10422
13577
|
exports.Live = Live;
|
|
10423
13578
|
exports.MethodContextService = MethodContextService;
|
|
13579
|
+
exports.Optimizer = Optimizer;
|
|
13580
|
+
exports.Partial = Partial;
|
|
10424
13581
|
exports.Performance = Performance;
|
|
10425
13582
|
exports.PersistBase = PersistBase;
|
|
13583
|
+
exports.PersistPartialAdapter = PersistPartialAdapter;
|
|
10426
13584
|
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
13585
|
+
exports.PersistScheduleAdapter = PersistScheduleAdapter;
|
|
10427
13586
|
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
10428
13587
|
exports.PositionSize = PositionSize;
|
|
10429
13588
|
exports.Schedule = Schedule;
|
|
10430
13589
|
exports.Walker = Walker;
|
|
10431
13590
|
exports.addExchange = addExchange;
|
|
10432
13591
|
exports.addFrame = addFrame;
|
|
13592
|
+
exports.addOptimizer = addOptimizer;
|
|
10433
13593
|
exports.addRisk = addRisk;
|
|
10434
13594
|
exports.addSizing = addSizing;
|
|
10435
13595
|
exports.addStrategy = addStrategy;
|
|
@@ -10444,10 +13604,12 @@ exports.getMode = getMode;
|
|
|
10444
13604
|
exports.lib = backtest;
|
|
10445
13605
|
exports.listExchanges = listExchanges;
|
|
10446
13606
|
exports.listFrames = listFrames;
|
|
13607
|
+
exports.listOptimizers = listOptimizers;
|
|
10447
13608
|
exports.listRisks = listRisks;
|
|
10448
13609
|
exports.listSizings = listSizings;
|
|
10449
13610
|
exports.listStrategies = listStrategies;
|
|
10450
13611
|
exports.listWalkers = listWalkers;
|
|
13612
|
+
exports.listenBacktestProgress = listenBacktestProgress;
|
|
10451
13613
|
exports.listenDoneBacktest = listenDoneBacktest;
|
|
10452
13614
|
exports.listenDoneBacktestOnce = listenDoneBacktestOnce;
|
|
10453
13615
|
exports.listenDoneLive = listenDoneLive;
|
|
@@ -10455,8 +13617,11 @@ exports.listenDoneLiveOnce = listenDoneLiveOnce;
|
|
|
10455
13617
|
exports.listenDoneWalker = listenDoneWalker;
|
|
10456
13618
|
exports.listenDoneWalkerOnce = listenDoneWalkerOnce;
|
|
10457
13619
|
exports.listenError = listenError;
|
|
13620
|
+
exports.listenPartialLoss = listenPartialLoss;
|
|
13621
|
+
exports.listenPartialLossOnce = listenPartialLossOnce;
|
|
13622
|
+
exports.listenPartialProfit = listenPartialProfit;
|
|
13623
|
+
exports.listenPartialProfitOnce = listenPartialProfitOnce;
|
|
10458
13624
|
exports.listenPerformance = listenPerformance;
|
|
10459
|
-
exports.listenProgress = listenProgress;
|
|
10460
13625
|
exports.listenSignal = listenSignal;
|
|
10461
13626
|
exports.listenSignalBacktest = listenSignalBacktest;
|
|
10462
13627
|
exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|
|
@@ -10467,5 +13632,6 @@ exports.listenValidation = listenValidation;
|
|
|
10467
13632
|
exports.listenWalker = listenWalker;
|
|
10468
13633
|
exports.listenWalkerComplete = listenWalkerComplete;
|
|
10469
13634
|
exports.listenWalkerOnce = listenWalkerOnce;
|
|
13635
|
+
exports.listenWalkerProgress = listenWalkerProgress;
|
|
10470
13636
|
exports.setConfig = setConfig;
|
|
10471
13637
|
exports.setLogger = setLogger;
|