backtest-kit 1.1.8 → 1.1.9
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 +806 -970
- package/build/index.cjs +3588 -275
- package/build/index.mjs +3569 -275
- package/package.json +1 -1
- package/types.d.ts +2955 -520
package/build/index.cjs
CHANGED
|
@@ -47,11 +47,16 @@ const connectionServices$1 = {
|
|
|
47
47
|
exchangeConnectionService: Symbol('exchangeConnectionService'),
|
|
48
48
|
strategyConnectionService: Symbol('strategyConnectionService'),
|
|
49
49
|
frameConnectionService: Symbol('frameConnectionService'),
|
|
50
|
+
sizingConnectionService: Symbol('sizingConnectionService'),
|
|
51
|
+
riskConnectionService: Symbol('riskConnectionService'),
|
|
50
52
|
};
|
|
51
53
|
const schemaServices$1 = {
|
|
52
54
|
exchangeSchemaService: Symbol('exchangeSchemaService'),
|
|
53
55
|
strategySchemaService: Symbol('strategySchemaService'),
|
|
54
56
|
frameSchemaService: Symbol('frameSchemaService'),
|
|
57
|
+
walkerSchemaService: Symbol('walkerSchemaService'),
|
|
58
|
+
sizingSchemaService: Symbol('sizingSchemaService'),
|
|
59
|
+
riskSchemaService: Symbol('riskSchemaService'),
|
|
55
60
|
};
|
|
56
61
|
const globalServices$1 = {
|
|
57
62
|
exchangeGlobalService: Symbol('exchangeGlobalService'),
|
|
@@ -59,24 +64,34 @@ const globalServices$1 = {
|
|
|
59
64
|
frameGlobalService: Symbol('frameGlobalService'),
|
|
60
65
|
liveGlobalService: Symbol('liveGlobalService'),
|
|
61
66
|
backtestGlobalService: Symbol('backtestGlobalService'),
|
|
67
|
+
walkerGlobalService: Symbol('walkerGlobalService'),
|
|
68
|
+
sizingGlobalService: Symbol('sizingGlobalService'),
|
|
69
|
+
riskGlobalService: Symbol('riskGlobalService'),
|
|
62
70
|
};
|
|
63
71
|
const logicPrivateServices$1 = {
|
|
64
72
|
backtestLogicPrivateService: Symbol('backtestLogicPrivateService'),
|
|
65
73
|
liveLogicPrivateService: Symbol('liveLogicPrivateService'),
|
|
74
|
+
walkerLogicPrivateService: Symbol('walkerLogicPrivateService'),
|
|
66
75
|
};
|
|
67
76
|
const logicPublicServices$1 = {
|
|
68
77
|
backtestLogicPublicService: Symbol('backtestLogicPublicService'),
|
|
69
78
|
liveLogicPublicService: Symbol('liveLogicPublicService'),
|
|
79
|
+
walkerLogicPublicService: Symbol('walkerLogicPublicService'),
|
|
70
80
|
};
|
|
71
81
|
const markdownServices$1 = {
|
|
72
82
|
backtestMarkdownService: Symbol('backtestMarkdownService'),
|
|
73
83
|
liveMarkdownService: Symbol('liveMarkdownService'),
|
|
74
84
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
85
|
+
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
86
|
+
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
75
87
|
};
|
|
76
88
|
const validationServices$1 = {
|
|
77
89
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
78
90
|
strategyValidationService: Symbol('strategyValidationService'),
|
|
79
91
|
frameValidationService: Symbol('frameValidationService'),
|
|
92
|
+
walkerValidationService: Symbol('walkerValidationService'),
|
|
93
|
+
sizingValidationService: Symbol('sizingValidationService'),
|
|
94
|
+
riskValidationService: Symbol('riskValidationService'),
|
|
80
95
|
};
|
|
81
96
|
const TYPES = {
|
|
82
97
|
...baseServices$1,
|
|
@@ -1103,6 +1118,102 @@ class PersistSignalUtils {
|
|
|
1103
1118
|
* ```
|
|
1104
1119
|
*/
|
|
1105
1120
|
const PersistSignalAdaper = new PersistSignalUtils();
|
|
1121
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1122
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1123
|
+
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
1124
|
+
/**
|
|
1125
|
+
* Utility class for managing risk active positions persistence.
|
|
1126
|
+
*
|
|
1127
|
+
* Features:
|
|
1128
|
+
* - Memoized storage instances per risk profile
|
|
1129
|
+
* - Custom adapter support
|
|
1130
|
+
* - Atomic read/write operations for RiskData
|
|
1131
|
+
* - Crash-safe position state management
|
|
1132
|
+
*
|
|
1133
|
+
* Used by ClientRisk for live mode persistence of active positions.
|
|
1134
|
+
*/
|
|
1135
|
+
class PersistRiskUtils {
|
|
1136
|
+
constructor() {
|
|
1137
|
+
this.PersistRiskFactory = PersistBase;
|
|
1138
|
+
this.getRiskStorage = functoolsKit.memoize(([riskName]) => `${riskName}`, (riskName) => Reflect.construct(this.PersistRiskFactory, [
|
|
1139
|
+
riskName,
|
|
1140
|
+
`./logs/data/risk/`,
|
|
1141
|
+
]));
|
|
1142
|
+
/**
|
|
1143
|
+
* Reads persisted active positions for a risk profile.
|
|
1144
|
+
*
|
|
1145
|
+
* Called by ClientRisk.waitForInit() to restore state.
|
|
1146
|
+
* Returns empty Map if no positions exist.
|
|
1147
|
+
*
|
|
1148
|
+
* @param riskName - Risk profile identifier
|
|
1149
|
+
* @returns Promise resolving to Map of active positions
|
|
1150
|
+
*/
|
|
1151
|
+
this.readPositionData = async (riskName) => {
|
|
1152
|
+
backtest$1.loggerService.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
|
|
1153
|
+
const isInitial = !this.getRiskStorage.has(riskName);
|
|
1154
|
+
const stateStorage = this.getRiskStorage(riskName);
|
|
1155
|
+
await stateStorage.waitForInit(isInitial);
|
|
1156
|
+
const RISK_STORAGE_KEY = "positions";
|
|
1157
|
+
if (await stateStorage.hasValue(RISK_STORAGE_KEY)) {
|
|
1158
|
+
return await stateStorage.readValue(RISK_STORAGE_KEY);
|
|
1159
|
+
}
|
|
1160
|
+
return [];
|
|
1161
|
+
};
|
|
1162
|
+
/**
|
|
1163
|
+
* Writes active positions to disk with atomic file writes.
|
|
1164
|
+
*
|
|
1165
|
+
* Called by ClientRisk after addSignal/removeSignal to persist state.
|
|
1166
|
+
* Uses atomic writes to prevent corruption on crashes.
|
|
1167
|
+
*
|
|
1168
|
+
* @param positions - Map of active positions
|
|
1169
|
+
* @param riskName - Risk profile identifier
|
|
1170
|
+
* @returns Promise that resolves when write is complete
|
|
1171
|
+
*/
|
|
1172
|
+
this.writePositionData = async (riskRow, riskName) => {
|
|
1173
|
+
backtest$1.loggerService.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
|
|
1174
|
+
const isInitial = !this.getRiskStorage.has(riskName);
|
|
1175
|
+
const stateStorage = this.getRiskStorage(riskName);
|
|
1176
|
+
await stateStorage.waitForInit(isInitial);
|
|
1177
|
+
const RISK_STORAGE_KEY = "positions";
|
|
1178
|
+
await stateStorage.writeValue(RISK_STORAGE_KEY, riskRow);
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Registers a custom persistence adapter.
|
|
1183
|
+
*
|
|
1184
|
+
* @param Ctor - Custom PersistBase constructor
|
|
1185
|
+
*
|
|
1186
|
+
* @example
|
|
1187
|
+
* ```typescript
|
|
1188
|
+
* class RedisPersist extends PersistBase {
|
|
1189
|
+
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1190
|
+
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1191
|
+
* }
|
|
1192
|
+
* PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
|
|
1193
|
+
* ```
|
|
1194
|
+
*/
|
|
1195
|
+
usePersistRiskAdapter(Ctor) {
|
|
1196
|
+
backtest$1.loggerService.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
|
|
1197
|
+
this.PersistRiskFactory = Ctor;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Global singleton instance of PersistRiskUtils.
|
|
1202
|
+
* Used by ClientRisk for active positions persistence.
|
|
1203
|
+
*
|
|
1204
|
+
* @example
|
|
1205
|
+
* ```typescript
|
|
1206
|
+
* // Custom adapter
|
|
1207
|
+
* PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
|
|
1208
|
+
*
|
|
1209
|
+
* // Read positions
|
|
1210
|
+
* const positions = await PersistRiskAdapter.readPositionData("my-risk");
|
|
1211
|
+
*
|
|
1212
|
+
* // Write positions
|
|
1213
|
+
* await PersistRiskAdapter.writePositionData(positionsMap, "my-risk");
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
1216
|
+
const PersistRiskAdapter = new PersistRiskUtils();
|
|
1106
1217
|
|
|
1107
1218
|
/**
|
|
1108
1219
|
* Global signal emitter for all trading events (live + backtest).
|
|
@@ -1125,10 +1236,20 @@ const signalBacktestEmitter = new functoolsKit.Subject();
|
|
|
1125
1236
|
*/
|
|
1126
1237
|
const errorEmitter = new functoolsKit.Subject();
|
|
1127
1238
|
/**
|
|
1128
|
-
* Done emitter for background execution completion.
|
|
1129
|
-
* Emits when background tasks complete (Live.background
|
|
1239
|
+
* Done emitter for live background execution completion.
|
|
1240
|
+
* Emits when live background tasks complete (Live.background).
|
|
1241
|
+
*/
|
|
1242
|
+
const doneLiveSubject = new functoolsKit.Subject();
|
|
1243
|
+
/**
|
|
1244
|
+
* Done emitter for backtest background execution completion.
|
|
1245
|
+
* Emits when backtest background tasks complete (Backtest.background).
|
|
1246
|
+
*/
|
|
1247
|
+
const doneBacktestSubject = new functoolsKit.Subject();
|
|
1248
|
+
/**
|
|
1249
|
+
* Done emitter for walker background execution completion.
|
|
1250
|
+
* Emits when walker background tasks complete (Walker.background).
|
|
1130
1251
|
*/
|
|
1131
|
-
const
|
|
1252
|
+
const doneWalkerSubject = new functoolsKit.Subject();
|
|
1132
1253
|
/**
|
|
1133
1254
|
* Progress emitter for backtest execution progress.
|
|
1134
1255
|
* Emits progress updates during backtest execution.
|
|
@@ -1139,6 +1260,37 @@ const progressEmitter = new functoolsKit.Subject();
|
|
|
1139
1260
|
* Emits performance metrics for profiling and bottleneck detection.
|
|
1140
1261
|
*/
|
|
1141
1262
|
const performanceEmitter = new functoolsKit.Subject();
|
|
1263
|
+
/**
|
|
1264
|
+
* Walker emitter for strategy comparison progress.
|
|
1265
|
+
* Emits progress updates during walker execution (each strategy completion).
|
|
1266
|
+
*/
|
|
1267
|
+
const walkerEmitter = new functoolsKit.Subject();
|
|
1268
|
+
/**
|
|
1269
|
+
* Walker complete emitter for strategy comparison completion.
|
|
1270
|
+
* Emits when all strategies have been tested and final results are available.
|
|
1271
|
+
*/
|
|
1272
|
+
const walkerCompleteSubject = new functoolsKit.Subject();
|
|
1273
|
+
/**
|
|
1274
|
+
* Validation emitter for risk validation errors.
|
|
1275
|
+
* Emits when risk validation functions throw errors during signal checking.
|
|
1276
|
+
*/
|
|
1277
|
+
const validationSubject = new functoolsKit.Subject();
|
|
1278
|
+
|
|
1279
|
+
var emitters = /*#__PURE__*/Object.freeze({
|
|
1280
|
+
__proto__: null,
|
|
1281
|
+
doneBacktestSubject: doneBacktestSubject,
|
|
1282
|
+
doneLiveSubject: doneLiveSubject,
|
|
1283
|
+
doneWalkerSubject: doneWalkerSubject,
|
|
1284
|
+
errorEmitter: errorEmitter,
|
|
1285
|
+
performanceEmitter: performanceEmitter,
|
|
1286
|
+
progressEmitter: progressEmitter,
|
|
1287
|
+
signalBacktestEmitter: signalBacktestEmitter,
|
|
1288
|
+
signalEmitter: signalEmitter,
|
|
1289
|
+
signalLiveEmitter: signalLiveEmitter,
|
|
1290
|
+
validationSubject: validationSubject,
|
|
1291
|
+
walkerCompleteSubject: walkerCompleteSubject,
|
|
1292
|
+
walkerEmitter: walkerEmitter
|
|
1293
|
+
});
|
|
1142
1294
|
|
|
1143
1295
|
const INTERVAL_MINUTES$1 = {
|
|
1144
1296
|
"1m": 1,
|
|
@@ -1205,13 +1357,23 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1205
1357
|
}
|
|
1206
1358
|
self._lastSignalTimestamp = currentTime;
|
|
1207
1359
|
}
|
|
1360
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1361
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1362
|
+
symbol: self.params.execution.context.symbol,
|
|
1363
|
+
strategyName: self.params.method.context.strategyName,
|
|
1364
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1365
|
+
currentPrice,
|
|
1366
|
+
timestamp: currentTime,
|
|
1367
|
+
}))) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1208
1370
|
const signal = await self.params.getSignal(self.params.execution.context.symbol);
|
|
1209
1371
|
if (!signal) {
|
|
1210
1372
|
return null;
|
|
1211
1373
|
}
|
|
1212
1374
|
const signalRow = {
|
|
1213
1375
|
id: functoolsKit.randomString(),
|
|
1214
|
-
priceOpen:
|
|
1376
|
+
priceOpen: currentPrice,
|
|
1215
1377
|
...signal,
|
|
1216
1378
|
symbol: self.params.execution.context.symbol,
|
|
1217
1379
|
exchangeName: self.params.method.context.exchangeName,
|
|
@@ -1241,7 +1403,7 @@ const GET_AVG_PRICE_FN = (candles) => {
|
|
|
1241
1403
|
? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
|
|
1242
1404
|
: sumPriceVolume / totalVolume;
|
|
1243
1405
|
};
|
|
1244
|
-
const WAIT_FOR_INIT_FN = async (self) => {
|
|
1406
|
+
const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
1245
1407
|
self.params.logger.debug("ClientStrategy waitForInit");
|
|
1246
1408
|
if (self.params.execution.context.backtest) {
|
|
1247
1409
|
return;
|
|
@@ -1300,7 +1462,7 @@ class ClientStrategy {
|
|
|
1300
1462
|
*
|
|
1301
1463
|
* @returns Promise that resolves when initialization is complete
|
|
1302
1464
|
*/
|
|
1303
|
-
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN(this));
|
|
1465
|
+
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
|
|
1304
1466
|
}
|
|
1305
1467
|
/**
|
|
1306
1468
|
* Updates pending signal and persists to disk in live mode.
|
|
@@ -1350,6 +1512,11 @@ class ClientStrategy {
|
|
|
1350
1512
|
const pendingSignal = await GET_SIGNAL_FN(this);
|
|
1351
1513
|
await this.setPendingSignal(pendingSignal);
|
|
1352
1514
|
if (this._pendingSignal) {
|
|
1515
|
+
// Register signal with risk management
|
|
1516
|
+
await this.params.risk.addSignal(this.params.execution.context.symbol, {
|
|
1517
|
+
strategyName: this.params.method.context.strategyName,
|
|
1518
|
+
riskName: this.params.riskName,
|
|
1519
|
+
});
|
|
1353
1520
|
if (this.params.callbacks?.onOpen) {
|
|
1354
1521
|
this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
|
|
1355
1522
|
}
|
|
@@ -1358,6 +1525,7 @@ class ClientStrategy {
|
|
|
1358
1525
|
signal: this._pendingSignal,
|
|
1359
1526
|
strategyName: this.params.method.context.strategyName,
|
|
1360
1527
|
exchangeName: this.params.method.context.exchangeName,
|
|
1528
|
+
symbol: this.params.execution.context.symbol,
|
|
1361
1529
|
currentPrice: this._pendingSignal.priceOpen,
|
|
1362
1530
|
};
|
|
1363
1531
|
if (this.params.callbacks?.onTick) {
|
|
@@ -1374,6 +1542,7 @@ class ClientStrategy {
|
|
|
1374
1542
|
signal: null,
|
|
1375
1543
|
strategyName: this.params.method.context.strategyName,
|
|
1376
1544
|
exchangeName: this.params.method.context.exchangeName,
|
|
1545
|
+
symbol: this.params.execution.context.symbol,
|
|
1377
1546
|
currentPrice,
|
|
1378
1547
|
};
|
|
1379
1548
|
if (this.params.callbacks?.onTick) {
|
|
@@ -1444,6 +1613,11 @@ class ClientStrategy {
|
|
|
1444
1613
|
if (this.params.callbacks?.onClose) {
|
|
1445
1614
|
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1446
1615
|
}
|
|
1616
|
+
// Remove signal from risk management
|
|
1617
|
+
await this.params.risk.removeSignal(this.params.execution.context.symbol, {
|
|
1618
|
+
strategyName: this.params.method.context.strategyName,
|
|
1619
|
+
riskName: this.params.riskName,
|
|
1620
|
+
});
|
|
1447
1621
|
await this.setPendingSignal(null);
|
|
1448
1622
|
const result = {
|
|
1449
1623
|
action: "closed",
|
|
@@ -1454,6 +1628,7 @@ class ClientStrategy {
|
|
|
1454
1628
|
pnl: pnl,
|
|
1455
1629
|
strategyName: this.params.method.context.strategyName,
|
|
1456
1630
|
exchangeName: this.params.method.context.exchangeName,
|
|
1631
|
+
symbol: this.params.execution.context.symbol,
|
|
1457
1632
|
};
|
|
1458
1633
|
if (this.params.callbacks?.onTick) {
|
|
1459
1634
|
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
@@ -1469,6 +1644,7 @@ class ClientStrategy {
|
|
|
1469
1644
|
currentPrice: averagePrice,
|
|
1470
1645
|
strategyName: this.params.method.context.strategyName,
|
|
1471
1646
|
exchangeName: this.params.method.context.exchangeName,
|
|
1647
|
+
symbol: this.params.execution.context.symbol,
|
|
1472
1648
|
};
|
|
1473
1649
|
if (this.params.callbacks?.onTick) {
|
|
1474
1650
|
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
@@ -1559,6 +1735,11 @@ class ClientStrategy {
|
|
|
1559
1735
|
if (this.params.callbacks?.onClose) {
|
|
1560
1736
|
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1561
1737
|
}
|
|
1738
|
+
// Remove signal from risk management
|
|
1739
|
+
await this.params.risk.removeSignal(this.params.execution.context.symbol, {
|
|
1740
|
+
strategyName: this.params.method.context.strategyName,
|
|
1741
|
+
riskName: this.params.riskName,
|
|
1742
|
+
});
|
|
1562
1743
|
await this.setPendingSignal(null);
|
|
1563
1744
|
const result = {
|
|
1564
1745
|
action: "closed",
|
|
@@ -1569,6 +1750,7 @@ class ClientStrategy {
|
|
|
1569
1750
|
pnl: pnl,
|
|
1570
1751
|
strategyName: this.params.method.context.strategyName,
|
|
1571
1752
|
exchangeName: this.params.method.context.exchangeName,
|
|
1753
|
+
symbol: this.params.execution.context.symbol,
|
|
1572
1754
|
};
|
|
1573
1755
|
if (this.params.callbacks?.onTick) {
|
|
1574
1756
|
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
@@ -1595,6 +1777,11 @@ class ClientStrategy {
|
|
|
1595
1777
|
if (this.params.callbacks?.onClose) {
|
|
1596
1778
|
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, lastPrice, this.params.execution.context.backtest);
|
|
1597
1779
|
}
|
|
1780
|
+
// Remove signal from risk management
|
|
1781
|
+
await this.params.risk.removeSignal(this.params.execution.context.symbol, {
|
|
1782
|
+
strategyName: this.params.method.context.strategyName,
|
|
1783
|
+
riskName: this.params.riskName,
|
|
1784
|
+
});
|
|
1598
1785
|
await this.setPendingSignal(null);
|
|
1599
1786
|
const result = {
|
|
1600
1787
|
action: "closed",
|
|
@@ -1605,6 +1792,7 @@ class ClientStrategy {
|
|
|
1605
1792
|
pnl: pnl,
|
|
1606
1793
|
strategyName: this.params.method.context.strategyName,
|
|
1607
1794
|
exchangeName: this.params.method.context.exchangeName,
|
|
1795
|
+
symbol: this.params.execution.context.symbol,
|
|
1608
1796
|
};
|
|
1609
1797
|
if (this.params.callbacks?.onTick) {
|
|
1610
1798
|
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
@@ -1637,6 +1825,11 @@ class ClientStrategy {
|
|
|
1637
1825
|
}
|
|
1638
1826
|
}
|
|
1639
1827
|
|
|
1828
|
+
const NOOP_RISK = {
|
|
1829
|
+
checkSignal: () => Promise.resolve(true),
|
|
1830
|
+
addSignal: () => Promise.resolve(),
|
|
1831
|
+
removeSignal: () => Promise.resolve(),
|
|
1832
|
+
};
|
|
1640
1833
|
/**
|
|
1641
1834
|
* Connection service routing strategy operations to correct ClientStrategy instance.
|
|
1642
1835
|
*
|
|
@@ -1663,6 +1856,7 @@ class StrategyConnectionService {
|
|
|
1663
1856
|
this.loggerService = inject(TYPES.loggerService);
|
|
1664
1857
|
this.executionContextService = inject(TYPES.executionContextService);
|
|
1665
1858
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
1859
|
+
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
1666
1860
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
1667
1861
|
this.methodContextService = inject(TYPES.methodContextService);
|
|
1668
1862
|
/**
|
|
@@ -1675,13 +1869,15 @@ class StrategyConnectionService {
|
|
|
1675
1869
|
* @returns Configured ClientStrategy instance
|
|
1676
1870
|
*/
|
|
1677
1871
|
this.getStrategy = functoolsKit.memoize(([strategyName]) => `${strategyName}`, (strategyName) => {
|
|
1678
|
-
const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
|
|
1872
|
+
const { riskName, getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
|
|
1679
1873
|
return new ClientStrategy({
|
|
1680
1874
|
interval,
|
|
1681
1875
|
execution: this.executionContextService,
|
|
1682
1876
|
method: this.methodContextService,
|
|
1683
1877
|
logger: this.loggerService,
|
|
1684
1878
|
exchange: this.exchangeConnectionService,
|
|
1879
|
+
risk: riskName ? this.riskConnectionService.getRisk(riskName) : NOOP_RISK,
|
|
1880
|
+
riskName,
|
|
1685
1881
|
strategyName,
|
|
1686
1882
|
getSignal,
|
|
1687
1883
|
callbacks,
|
|
@@ -1908,104 +2104,581 @@ class FrameConnectionService {
|
|
|
1908
2104
|
}
|
|
1909
2105
|
|
|
1910
2106
|
/**
|
|
1911
|
-
*
|
|
2107
|
+
* Calculates position size using fixed percentage risk method.
|
|
2108
|
+
* Risk amount = accountBalance * riskPercentage
|
|
2109
|
+
* Position size = riskAmount / |priceOpen - priceStopLoss|
|
|
1912
2110
|
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
2111
|
+
* @param params - Calculation parameters
|
|
2112
|
+
* @param schema - Fixed percentage schema
|
|
2113
|
+
* @returns Calculated position size
|
|
2114
|
+
*/
|
|
2115
|
+
const calculateFixedPercentage = (params, schema) => {
|
|
2116
|
+
const { accountBalance, priceOpen, priceStopLoss } = params;
|
|
2117
|
+
const { riskPercentage } = schema;
|
|
2118
|
+
const riskAmount = accountBalance * (riskPercentage / 100);
|
|
2119
|
+
const stopDistance = Math.abs(priceOpen - priceStopLoss);
|
|
2120
|
+
if (stopDistance === 0) {
|
|
2121
|
+
throw new Error("Stop-loss distance cannot be zero");
|
|
2122
|
+
}
|
|
2123
|
+
return riskAmount / stopDistance;
|
|
2124
|
+
};
|
|
2125
|
+
/**
|
|
2126
|
+
* Calculates position size using Kelly Criterion.
|
|
2127
|
+
* Kelly % = (winRate * winLossRatio - (1 - winRate)) / winLossRatio
|
|
2128
|
+
* Position size = accountBalance * kellyPercentage * kellyMultiplier / priceOpen
|
|
1915
2129
|
*
|
|
1916
|
-
*
|
|
2130
|
+
* @param params - Calculation parameters
|
|
2131
|
+
* @param schema - Kelly schema
|
|
2132
|
+
* @returns Calculated position size
|
|
1917
2133
|
*/
|
|
1918
|
-
|
|
2134
|
+
const calculateKellyCriterion = (params, schema) => {
|
|
2135
|
+
const { accountBalance, priceOpen, winRate, winLossRatio } = params;
|
|
2136
|
+
const { kellyMultiplier = 0.25 } = schema;
|
|
2137
|
+
if (winRate <= 0 || winRate >= 1) {
|
|
2138
|
+
throw new Error("winRate must be between 0 and 1");
|
|
2139
|
+
}
|
|
2140
|
+
if (winLossRatio <= 0) {
|
|
2141
|
+
throw new Error("winLossRatio must be positive");
|
|
2142
|
+
}
|
|
2143
|
+
// Kelly formula: (W * R - L) / R
|
|
2144
|
+
// W = win rate, L = loss rate (1 - W), R = win/loss ratio
|
|
2145
|
+
const kellyPercentage = (winRate * winLossRatio - (1 - winRate)) / winLossRatio;
|
|
2146
|
+
// Kelly can be negative (edge is negative) or very large
|
|
2147
|
+
// Apply multiplier to reduce risk (common practice: 0.25 for quarter Kelly)
|
|
2148
|
+
const adjustedKelly = Math.max(0, kellyPercentage) * kellyMultiplier;
|
|
2149
|
+
return (accountBalance * adjustedKelly) / priceOpen;
|
|
2150
|
+
};
|
|
2151
|
+
/**
|
|
2152
|
+
* Calculates position size using ATR-based method.
|
|
2153
|
+
* Risk amount = accountBalance * riskPercentage
|
|
2154
|
+
* Position size = riskAmount / (ATR * atrMultiplier)
|
|
2155
|
+
*
|
|
2156
|
+
* @param params - Calculation parameters
|
|
2157
|
+
* @param schema - ATR schema
|
|
2158
|
+
* @returns Calculated position size
|
|
2159
|
+
*/
|
|
2160
|
+
const calculateATRBased = (params, schema) => {
|
|
2161
|
+
const { accountBalance, atr } = params;
|
|
2162
|
+
const { riskPercentage, atrMultiplier = 2 } = schema;
|
|
2163
|
+
if (atr <= 0) {
|
|
2164
|
+
throw new Error("ATR must be positive");
|
|
2165
|
+
}
|
|
2166
|
+
const riskAmount = accountBalance * (riskPercentage / 100);
|
|
2167
|
+
const stopDistance = atr * atrMultiplier;
|
|
2168
|
+
return riskAmount / stopDistance;
|
|
2169
|
+
};
|
|
2170
|
+
/**
|
|
2171
|
+
* Main calculation function routing to specific sizing method.
|
|
2172
|
+
* Applies min/max constraints after calculation.
|
|
2173
|
+
*
|
|
2174
|
+
* @param params - Calculation parameters
|
|
2175
|
+
* @param self - ClientSizing instance reference
|
|
2176
|
+
* @returns Calculated and constrained position size
|
|
2177
|
+
*/
|
|
2178
|
+
const CALCULATE_FN = async (params, self) => {
|
|
2179
|
+
self.params.logger.debug("ClientSizing calculate", {
|
|
2180
|
+
symbol: params.symbol,
|
|
2181
|
+
method: params.method,
|
|
2182
|
+
});
|
|
2183
|
+
const schema = self.params;
|
|
2184
|
+
let quantity;
|
|
2185
|
+
// Type-safe routing based on discriminated union using schema.method
|
|
2186
|
+
if (schema.method === "fixed-percentage") {
|
|
2187
|
+
if (params.method !== "fixed-percentage") {
|
|
2188
|
+
throw new Error(`Params method mismatch: expected fixed-percentage, got ${params.method}`);
|
|
2189
|
+
}
|
|
2190
|
+
quantity = calculateFixedPercentage(params, schema);
|
|
2191
|
+
}
|
|
2192
|
+
else if (schema.method === "kelly-criterion") {
|
|
2193
|
+
if (params.method !== "kelly-criterion") {
|
|
2194
|
+
throw new Error(`Params method mismatch: expected kelly-criterion, got ${params.method}`);
|
|
2195
|
+
}
|
|
2196
|
+
quantity = calculateKellyCriterion(params, schema);
|
|
2197
|
+
}
|
|
2198
|
+
else if (schema.method === "atr-based") {
|
|
2199
|
+
if (params.method !== "atr-based") {
|
|
2200
|
+
throw new Error(`Params method mismatch: expected atr-based, got ${params.method}`);
|
|
2201
|
+
}
|
|
2202
|
+
quantity = calculateATRBased(params, schema);
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
const _exhaustiveCheck = schema;
|
|
2206
|
+
throw new Error(`ClientSizing calculate: unknown method ${_exhaustiveCheck.method}`);
|
|
2207
|
+
}
|
|
2208
|
+
// Apply max position percentage constraint
|
|
2209
|
+
if (schema.maxPositionPercentage !== undefined) {
|
|
2210
|
+
const maxByPercentage = (params.accountBalance * schema.maxPositionPercentage) /
|
|
2211
|
+
100 /
|
|
2212
|
+
params.priceOpen;
|
|
2213
|
+
quantity = Math.min(quantity, maxByPercentage);
|
|
2214
|
+
}
|
|
2215
|
+
// Apply min/max absolute constraints
|
|
2216
|
+
if (schema.minPositionSize !== undefined) {
|
|
2217
|
+
quantity = Math.max(quantity, schema.minPositionSize);
|
|
2218
|
+
}
|
|
2219
|
+
if (schema.maxPositionSize !== undefined) {
|
|
2220
|
+
quantity = Math.min(quantity, schema.maxPositionSize);
|
|
2221
|
+
}
|
|
2222
|
+
// Trigger callback if defined
|
|
2223
|
+
if (schema.callbacks?.onCalculate) {
|
|
2224
|
+
schema.callbacks.onCalculate(quantity, params);
|
|
2225
|
+
}
|
|
2226
|
+
return quantity;
|
|
2227
|
+
};
|
|
2228
|
+
/**
|
|
2229
|
+
* Client implementation for position sizing calculation.
|
|
2230
|
+
*
|
|
2231
|
+
* Features:
|
|
2232
|
+
* - Multiple sizing methods (fixed %, Kelly, ATR)
|
|
2233
|
+
* - Min/max position constraints
|
|
2234
|
+
* - Max position percentage limit
|
|
2235
|
+
* - Callback support for validation and logging
|
|
2236
|
+
*
|
|
2237
|
+
* Used by strategy execution to determine optimal position sizes.
|
|
2238
|
+
*/
|
|
2239
|
+
class ClientSizing {
|
|
2240
|
+
constructor(params) {
|
|
2241
|
+
this.params = params;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Calculates position size based on configured method and constraints.
|
|
2245
|
+
*
|
|
2246
|
+
* @param params - Calculation parameters (symbol, balance, prices, etc.)
|
|
2247
|
+
* @returns Promise resolving to calculated position size
|
|
2248
|
+
* @throws Error if required parameters are missing or invalid
|
|
2249
|
+
*/
|
|
2250
|
+
async calculate(params) {
|
|
2251
|
+
return await CALCULATE_FN(params, this);
|
|
2252
|
+
}
|
|
2253
|
+
;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Connection service routing sizing operations to correct ClientSizing instance.
|
|
2258
|
+
*
|
|
2259
|
+
* Routes sizing method calls to the appropriate sizing implementation
|
|
2260
|
+
* based on the provided sizingName parameter. Uses memoization to cache
|
|
2261
|
+
* ClientSizing instances for performance.
|
|
2262
|
+
*
|
|
2263
|
+
* Key features:
|
|
2264
|
+
* - Explicit sizing routing via sizingName parameter
|
|
2265
|
+
* - Memoized ClientSizing instances by sizingName
|
|
2266
|
+
* - Position size calculation with risk management
|
|
2267
|
+
*
|
|
2268
|
+
* Note: sizingName is empty string for strategies without sizing configuration.
|
|
2269
|
+
*
|
|
2270
|
+
* @example
|
|
2271
|
+
* ```typescript
|
|
2272
|
+
* // Used internally by framework
|
|
2273
|
+
* const quantity = await sizingConnectionService.calculate(
|
|
2274
|
+
* {
|
|
2275
|
+
* symbol: "BTCUSDT",
|
|
2276
|
+
* accountBalance: 10000,
|
|
2277
|
+
* priceOpen: 50000,
|
|
2278
|
+
* priceStopLoss: 49000,
|
|
2279
|
+
* method: "fixed-percentage"
|
|
2280
|
+
* },
|
|
2281
|
+
* { sizingName: "conservative" }
|
|
2282
|
+
* );
|
|
2283
|
+
* ```
|
|
2284
|
+
*/
|
|
2285
|
+
class SizingConnectionService {
|
|
1919
2286
|
constructor() {
|
|
1920
2287
|
this.loggerService = inject(TYPES.loggerService);
|
|
1921
|
-
this.
|
|
2288
|
+
this.sizingSchemaService = inject(TYPES.sizingSchemaService);
|
|
1922
2289
|
/**
|
|
1923
|
-
*
|
|
2290
|
+
* Retrieves memoized ClientSizing instance for given sizing name.
|
|
1924
2291
|
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
* @param
|
|
1929
|
-
* @
|
|
1930
|
-
* @returns Promise resolving to array of candles
|
|
2292
|
+
* Creates ClientSizing on first call, returns cached instance on subsequent calls.
|
|
2293
|
+
* Cache key is sizingName string.
|
|
2294
|
+
*
|
|
2295
|
+
* @param sizingName - Name of registered sizing schema
|
|
2296
|
+
* @returns Configured ClientSizing instance
|
|
1931
2297
|
*/
|
|
1932
|
-
this.
|
|
1933
|
-
this.
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
when,
|
|
1938
|
-
backtest,
|
|
1939
|
-
});
|
|
1940
|
-
return await ExecutionContextService.runInContext(async () => {
|
|
1941
|
-
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
1942
|
-
}, {
|
|
1943
|
-
symbol,
|
|
1944
|
-
when,
|
|
1945
|
-
backtest,
|
|
2298
|
+
this.getSizing = functoolsKit.memoize(([sizingName]) => `${sizingName}`, (sizingName) => {
|
|
2299
|
+
const schema = this.sizingSchemaService.get(sizingName);
|
|
2300
|
+
return new ClientSizing({
|
|
2301
|
+
...schema,
|
|
2302
|
+
logger: this.loggerService,
|
|
1946
2303
|
});
|
|
1947
|
-
};
|
|
2304
|
+
});
|
|
1948
2305
|
/**
|
|
1949
|
-
*
|
|
2306
|
+
* Calculates position size based on risk parameters and configured method.
|
|
1950
2307
|
*
|
|
1951
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
* @param
|
|
1955
|
-
* @param
|
|
1956
|
-
* @returns Promise resolving to
|
|
2308
|
+
* Routes to appropriate ClientSizing instance based on provided context.
|
|
2309
|
+
* Supports multiple sizing methods: fixed-percentage, kelly-criterion, atr-based.
|
|
2310
|
+
*
|
|
2311
|
+
* @param params - Calculation parameters (symbol, balance, prices, method-specific data)
|
|
2312
|
+
* @param context - Execution context with sizing name
|
|
2313
|
+
* @returns Promise resolving to calculated position size
|
|
1957
2314
|
*/
|
|
1958
|
-
this.
|
|
1959
|
-
this.loggerService.log("
|
|
1960
|
-
symbol,
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
when,
|
|
1964
|
-
backtest,
|
|
1965
|
-
});
|
|
1966
|
-
return await ExecutionContextService.runInContext(async () => {
|
|
1967
|
-
return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
1968
|
-
}, {
|
|
1969
|
-
symbol,
|
|
1970
|
-
when,
|
|
1971
|
-
backtest,
|
|
2315
|
+
this.calculate = async (params, context) => {
|
|
2316
|
+
this.loggerService.log("sizingConnectionService calculate", {
|
|
2317
|
+
symbol: params.symbol,
|
|
2318
|
+
method: params.method,
|
|
2319
|
+
context,
|
|
1972
2320
|
});
|
|
2321
|
+
return await this.getSizing(context.sizingName).calculate(params);
|
|
1973
2322
|
};
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/** Symbol indicating that positions need to be fetched from persistence */
|
|
2327
|
+
const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
|
|
2328
|
+
/** Key generator for active position map */
|
|
2329
|
+
const GET_KEY_FN = (strategyName, symbol) => `${strategyName}:${symbol}`;
|
|
2330
|
+
/** Wrapper to execute risk validation function with error handling */
|
|
2331
|
+
const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
|
|
2332
|
+
await validation(params);
|
|
2333
|
+
return true;
|
|
2334
|
+
}, {
|
|
2335
|
+
defaultValue: false,
|
|
2336
|
+
fallback: (error) => {
|
|
2337
|
+
backtest$1.loggerService.warn("ClientRisk exception thrown", {
|
|
2338
|
+
error: functoolsKit.errorData(error),
|
|
2339
|
+
message: functoolsKit.getErrorMessage(error),
|
|
2340
|
+
});
|
|
2341
|
+
validationSubject.next(error);
|
|
2342
|
+
},
|
|
2343
|
+
});
|
|
2344
|
+
/**
|
|
2345
|
+
* Initializes active positions by reading from persistence.
|
|
2346
|
+
* Uses singleshot pattern to ensure it only runs once.
|
|
2347
|
+
* This function is exported for use in tests or other modules.
|
|
2348
|
+
*/
|
|
2349
|
+
const WAIT_FOR_INIT_FN = async (self) => {
|
|
2350
|
+
self.params.logger.debug("ClientRisk waitForInit");
|
|
2351
|
+
const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName);
|
|
2352
|
+
self._activePositions = new Map(persistedPositions);
|
|
2353
|
+
};
|
|
2354
|
+
/**
|
|
2355
|
+
* ClientRisk implementation for portfolio-level risk management.
|
|
2356
|
+
*
|
|
2357
|
+
* Provides risk checking logic to prevent signals that violate configured limits:
|
|
2358
|
+
* - Maximum concurrent positions (tracks across all strategies)
|
|
2359
|
+
* - Custom validations with access to all active positions
|
|
2360
|
+
*
|
|
2361
|
+
* Multiple ClientStrategy instances share the same ClientRisk instance,
|
|
2362
|
+
* allowing cross-strategy risk analysis.
|
|
2363
|
+
*
|
|
2364
|
+
* Used internally by strategy execution to validate signals before opening positions.
|
|
2365
|
+
*/
|
|
2366
|
+
class ClientRisk {
|
|
2367
|
+
constructor(params) {
|
|
2368
|
+
this.params = params;
|
|
1974
2369
|
/**
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
* @param when - Timestamp for context
|
|
1979
|
-
* @param backtest - Whether running in backtest mode
|
|
1980
|
-
* @returns Promise resolving to VWAP price
|
|
2370
|
+
* Map of active positions tracked across all strategies.
|
|
2371
|
+
* Key: `${strategyName}:${exchangeName}:${symbol}`
|
|
2372
|
+
* Starts as POSITION_NEED_FETCH symbol, gets initialized on first use.
|
|
1981
2373
|
*/
|
|
1982
|
-
this.
|
|
1983
|
-
this.loggerService.log("exchangeGlobalService getAveragePrice", {
|
|
1984
|
-
symbol,
|
|
1985
|
-
when,
|
|
1986
|
-
backtest,
|
|
1987
|
-
});
|
|
1988
|
-
return await ExecutionContextService.runInContext(async () => {
|
|
1989
|
-
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
1990
|
-
}, {
|
|
1991
|
-
symbol,
|
|
1992
|
-
when,
|
|
1993
|
-
backtest,
|
|
1994
|
-
});
|
|
1995
|
-
};
|
|
2374
|
+
this._activePositions = POSITION_NEED_FETCH;
|
|
1996
2375
|
/**
|
|
1997
|
-
*
|
|
2376
|
+
* Initializes active positions by loading from persistence.
|
|
2377
|
+
* Uses singleshot pattern to ensure initialization happens exactly once.
|
|
2378
|
+
* Skips persistence in backtest mode.
|
|
2379
|
+
*/
|
|
2380
|
+
this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN(this));
|
|
2381
|
+
/**
|
|
2382
|
+
* Checks if a signal should be allowed based on risk limits.
|
|
1998
2383
|
*
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2384
|
+
* Executes custom validations with access to:
|
|
2385
|
+
* - Passthrough params from ClientStrategy (symbol, strategyName, exchangeName, currentPrice, timestamp)
|
|
2386
|
+
* - Active positions via this.activePositions getter
|
|
2387
|
+
*
|
|
2388
|
+
* Returns false immediately if any validation throws error.
|
|
2389
|
+
* Triggers callbacks (onRejected, onAllowed) based on result.
|
|
2390
|
+
*
|
|
2391
|
+
* @param params - Risk check arguments (passthrough from ClientStrategy)
|
|
2392
|
+
* @returns Promise resolving to true if allowed, false if rejected
|
|
2004
2393
|
*/
|
|
2005
|
-
this.
|
|
2006
|
-
this.
|
|
2007
|
-
symbol,
|
|
2008
|
-
|
|
2394
|
+
this.checkSignal = async (params) => {
|
|
2395
|
+
this.params.logger.debug("ClientRisk checkSignal", {
|
|
2396
|
+
symbol: params.symbol,
|
|
2397
|
+
strategyName: params.strategyName,
|
|
2398
|
+
});
|
|
2399
|
+
if (this._activePositions === POSITION_NEED_FETCH) {
|
|
2400
|
+
await this.waitForInit();
|
|
2401
|
+
}
|
|
2402
|
+
const riskMap = this._activePositions;
|
|
2403
|
+
const payload = {
|
|
2404
|
+
...params,
|
|
2405
|
+
activePositionCount: riskMap.size,
|
|
2406
|
+
activePositions: Array.from(riskMap.values()),
|
|
2407
|
+
};
|
|
2408
|
+
// Execute custom validations
|
|
2409
|
+
let isValid = true;
|
|
2410
|
+
if (this.params.validations) {
|
|
2411
|
+
for (const validation of this.params.validations) {
|
|
2412
|
+
if (functoolsKit.not(await DO_VALIDATION_FN(typeof validation === "function"
|
|
2413
|
+
? validation
|
|
2414
|
+
: validation.validate, payload))) {
|
|
2415
|
+
isValid = false;
|
|
2416
|
+
break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (!isValid) {
|
|
2421
|
+
if (this.params.callbacks?.onRejected) {
|
|
2422
|
+
this.params.callbacks.onRejected(params.symbol, params);
|
|
2423
|
+
}
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
// All checks passed
|
|
2427
|
+
if (this.params.callbacks?.onAllowed) {
|
|
2428
|
+
this.params.callbacks.onAllowed(params.symbol, params);
|
|
2429
|
+
}
|
|
2430
|
+
return true;
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Persists current active positions to disk.
|
|
2435
|
+
*/
|
|
2436
|
+
async _updatePositions() {
|
|
2437
|
+
if (this._activePositions === POSITION_NEED_FETCH) {
|
|
2438
|
+
await this.waitForInit();
|
|
2439
|
+
}
|
|
2440
|
+
await PersistRiskAdapter.writePositionData(Array.from(this._activePositions), this.params.riskName);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Registers a new opened signal.
|
|
2444
|
+
* Called by StrategyConnectionService after signal is opened.
|
|
2445
|
+
*/
|
|
2446
|
+
async addSignal(symbol, context) {
|
|
2447
|
+
this.params.logger.debug("ClientRisk addSignal", {
|
|
2448
|
+
symbol,
|
|
2449
|
+
context,
|
|
2450
|
+
});
|
|
2451
|
+
if (this._activePositions === POSITION_NEED_FETCH) {
|
|
2452
|
+
await this.waitForInit();
|
|
2453
|
+
}
|
|
2454
|
+
const key = GET_KEY_FN(context.strategyName, symbol);
|
|
2455
|
+
const riskMap = this._activePositions;
|
|
2456
|
+
riskMap.set(key, {
|
|
2457
|
+
signal: null, // Signal details not needed for position tracking
|
|
2458
|
+
strategyName: context.strategyName,
|
|
2459
|
+
exchangeName: "",
|
|
2460
|
+
openTimestamp: Date.now(),
|
|
2461
|
+
});
|
|
2462
|
+
await this._updatePositions();
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Removes a closed signal.
|
|
2466
|
+
* Called by StrategyConnectionService when signal is closed.
|
|
2467
|
+
*/
|
|
2468
|
+
async removeSignal(symbol, context) {
|
|
2469
|
+
this.params.logger.debug("ClientRisk removeSignal", {
|
|
2470
|
+
symbol,
|
|
2471
|
+
context,
|
|
2472
|
+
});
|
|
2473
|
+
if (this._activePositions === POSITION_NEED_FETCH) {
|
|
2474
|
+
await this.waitForInit();
|
|
2475
|
+
}
|
|
2476
|
+
const key = GET_KEY_FN(context.strategyName, symbol);
|
|
2477
|
+
const riskMap = this._activePositions;
|
|
2478
|
+
riskMap.delete(key);
|
|
2479
|
+
await this._updatePositions();
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* Connection service routing risk operations to correct ClientRisk instance.
|
|
2485
|
+
*
|
|
2486
|
+
* Routes risk checking calls to the appropriate risk implementation
|
|
2487
|
+
* based on the provided riskName parameter. Uses memoization to cache
|
|
2488
|
+
* ClientRisk instances for performance.
|
|
2489
|
+
*
|
|
2490
|
+
* Key features:
|
|
2491
|
+
* - Explicit risk routing via riskName parameter
|
|
2492
|
+
* - Memoized ClientRisk instances by riskName
|
|
2493
|
+
* - Risk limit validation for signals
|
|
2494
|
+
*
|
|
2495
|
+
* Note: riskName is empty string for strategies without risk configuration.
|
|
2496
|
+
*
|
|
2497
|
+
* @example
|
|
2498
|
+
* ```typescript
|
|
2499
|
+
* // Used internally by framework
|
|
2500
|
+
* const result = await riskConnectionService.checkSignal(
|
|
2501
|
+
* {
|
|
2502
|
+
* symbol: "BTCUSDT",
|
|
2503
|
+
* positionSize: 0.5,
|
|
2504
|
+
* currentPrice: 50000,
|
|
2505
|
+
* portfolioBalance: 100000,
|
|
2506
|
+
* currentDrawdown: 5,
|
|
2507
|
+
* currentPositions: 3,
|
|
2508
|
+
* dailyPnl: -2,
|
|
2509
|
+
* currentSymbolExposure: 8
|
|
2510
|
+
* },
|
|
2511
|
+
* { riskName: "conservative" }
|
|
2512
|
+
* );
|
|
2513
|
+
* ```
|
|
2514
|
+
*/
|
|
2515
|
+
class RiskConnectionService {
|
|
2516
|
+
constructor() {
|
|
2517
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
2518
|
+
this.riskSchemaService = inject(TYPES.riskSchemaService);
|
|
2519
|
+
/**
|
|
2520
|
+
* Retrieves memoized ClientRisk instance for given risk name.
|
|
2521
|
+
*
|
|
2522
|
+
* Creates ClientRisk on first call, returns cached instance on subsequent calls.
|
|
2523
|
+
* Cache key is riskName string.
|
|
2524
|
+
*
|
|
2525
|
+
* @param riskName - Name of registered risk schema
|
|
2526
|
+
* @returns Configured ClientRisk instance
|
|
2527
|
+
*/
|
|
2528
|
+
this.getRisk = functoolsKit.memoize(([riskName]) => `${riskName}`, (riskName) => {
|
|
2529
|
+
const schema = this.riskSchemaService.get(riskName);
|
|
2530
|
+
return new ClientRisk({
|
|
2531
|
+
...schema,
|
|
2532
|
+
logger: this.loggerService,
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
2535
|
+
/**
|
|
2536
|
+
* Checks if a signal should be allowed based on risk limits.
|
|
2537
|
+
*
|
|
2538
|
+
* Routes to appropriate ClientRisk instance based on provided context.
|
|
2539
|
+
* Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
|
|
2540
|
+
*
|
|
2541
|
+
* @param params - Risk check arguments (portfolio state, position details)
|
|
2542
|
+
* @param context - Execution context with risk name
|
|
2543
|
+
* @returns Promise resolving to risk check result
|
|
2544
|
+
*/
|
|
2545
|
+
this.checkSignal = async (params, context) => {
|
|
2546
|
+
this.loggerService.log("riskConnectionService checkSignal", {
|
|
2547
|
+
symbol: params.symbol,
|
|
2548
|
+
context,
|
|
2549
|
+
});
|
|
2550
|
+
return await this.getRisk(context.riskName).checkSignal(params);
|
|
2551
|
+
};
|
|
2552
|
+
/**
|
|
2553
|
+
* Registers an opened signal with the risk management system.
|
|
2554
|
+
* Routes to appropriate ClientRisk instance.
|
|
2555
|
+
*
|
|
2556
|
+
* @param symbol - Trading pair symbol
|
|
2557
|
+
* @param context - Context information (strategyName, riskName)
|
|
2558
|
+
*/
|
|
2559
|
+
this.addSignal = async (symbol, context) => {
|
|
2560
|
+
this.loggerService.log("riskConnectionService addSignal", {
|
|
2561
|
+
symbol,
|
|
2562
|
+
context,
|
|
2563
|
+
});
|
|
2564
|
+
await this.getRisk(context.riskName).addSignal(symbol, context);
|
|
2565
|
+
};
|
|
2566
|
+
/**
|
|
2567
|
+
* Removes a closed signal from the risk management system.
|
|
2568
|
+
* Routes to appropriate ClientRisk instance.
|
|
2569
|
+
*
|
|
2570
|
+
* @param symbol - Trading pair symbol
|
|
2571
|
+
* @param context - Context information (strategyName, riskName)
|
|
2572
|
+
*/
|
|
2573
|
+
this.removeSignal = async (symbol, context) => {
|
|
2574
|
+
this.loggerService.log("riskConnectionService removeSignal", {
|
|
2575
|
+
symbol,
|
|
2576
|
+
context,
|
|
2577
|
+
});
|
|
2578
|
+
await this.getRisk(context.riskName).removeSignal(symbol, context);
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* Global service for exchange operations with execution context injection.
|
|
2585
|
+
*
|
|
2586
|
+
* Wraps ExchangeConnectionService with ExecutionContextService to inject
|
|
2587
|
+
* symbol, when, and backtest parameters into the execution context.
|
|
2588
|
+
*
|
|
2589
|
+
* Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
|
|
2590
|
+
*/
|
|
2591
|
+
class ExchangeGlobalService {
|
|
2592
|
+
constructor() {
|
|
2593
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
2594
|
+
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
2595
|
+
/**
|
|
2596
|
+
* Fetches historical candles with execution context.
|
|
2597
|
+
*
|
|
2598
|
+
* @param symbol - Trading pair symbol
|
|
2599
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
2600
|
+
* @param limit - Maximum number of candles to fetch
|
|
2601
|
+
* @param when - Timestamp for context (used in backtest mode)
|
|
2602
|
+
* @param backtest - Whether running in backtest mode
|
|
2603
|
+
* @returns Promise resolving to array of candles
|
|
2604
|
+
*/
|
|
2605
|
+
this.getCandles = async (symbol, interval, limit, when, backtest) => {
|
|
2606
|
+
this.loggerService.log("exchangeGlobalService getCandles", {
|
|
2607
|
+
symbol,
|
|
2608
|
+
interval,
|
|
2609
|
+
limit,
|
|
2610
|
+
when,
|
|
2611
|
+
backtest,
|
|
2612
|
+
});
|
|
2613
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
2614
|
+
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
2615
|
+
}, {
|
|
2616
|
+
symbol,
|
|
2617
|
+
when,
|
|
2618
|
+
backtest,
|
|
2619
|
+
});
|
|
2620
|
+
};
|
|
2621
|
+
/**
|
|
2622
|
+
* Fetches future candles (backtest mode only) with execution context.
|
|
2623
|
+
*
|
|
2624
|
+
* @param symbol - Trading pair symbol
|
|
2625
|
+
* @param interval - Candle interval
|
|
2626
|
+
* @param limit - Maximum number of candles to fetch
|
|
2627
|
+
* @param when - Timestamp for context
|
|
2628
|
+
* @param backtest - Whether running in backtest mode (must be true)
|
|
2629
|
+
* @returns Promise resolving to array of future candles
|
|
2630
|
+
*/
|
|
2631
|
+
this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
|
|
2632
|
+
this.loggerService.log("exchangeGlobalService getNextCandles", {
|
|
2633
|
+
symbol,
|
|
2634
|
+
interval,
|
|
2635
|
+
limit,
|
|
2636
|
+
when,
|
|
2637
|
+
backtest,
|
|
2638
|
+
});
|
|
2639
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
2640
|
+
return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
2641
|
+
}, {
|
|
2642
|
+
symbol,
|
|
2643
|
+
when,
|
|
2644
|
+
backtest,
|
|
2645
|
+
});
|
|
2646
|
+
};
|
|
2647
|
+
/**
|
|
2648
|
+
* Calculates VWAP with execution context.
|
|
2649
|
+
*
|
|
2650
|
+
* @param symbol - Trading pair symbol
|
|
2651
|
+
* @param when - Timestamp for context
|
|
2652
|
+
* @param backtest - Whether running in backtest mode
|
|
2653
|
+
* @returns Promise resolving to VWAP price
|
|
2654
|
+
*/
|
|
2655
|
+
this.getAveragePrice = async (symbol, when, backtest) => {
|
|
2656
|
+
this.loggerService.log("exchangeGlobalService getAveragePrice", {
|
|
2657
|
+
symbol,
|
|
2658
|
+
when,
|
|
2659
|
+
backtest,
|
|
2660
|
+
});
|
|
2661
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
2662
|
+
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
2663
|
+
}, {
|
|
2664
|
+
symbol,
|
|
2665
|
+
when,
|
|
2666
|
+
backtest,
|
|
2667
|
+
});
|
|
2668
|
+
};
|
|
2669
|
+
/**
|
|
2670
|
+
* Formats price with execution context.
|
|
2671
|
+
*
|
|
2672
|
+
* @param symbol - Trading pair symbol
|
|
2673
|
+
* @param price - Price to format
|
|
2674
|
+
* @param when - Timestamp for context
|
|
2675
|
+
* @param backtest - Whether running in backtest mode
|
|
2676
|
+
* @returns Promise resolving to formatted price string
|
|
2677
|
+
*/
|
|
2678
|
+
this.formatPrice = async (symbol, price, when, backtest) => {
|
|
2679
|
+
this.loggerService.log("exchangeGlobalService formatPrice", {
|
|
2680
|
+
symbol,
|
|
2681
|
+
price,
|
|
2009
2682
|
when,
|
|
2010
2683
|
backtest,
|
|
2011
2684
|
});
|
|
@@ -2165,6 +2838,87 @@ class FrameGlobalService {
|
|
|
2165
2838
|
}
|
|
2166
2839
|
}
|
|
2167
2840
|
|
|
2841
|
+
/**
|
|
2842
|
+
* Global service for sizing operations.
|
|
2843
|
+
*
|
|
2844
|
+
* Wraps SizingConnectionService for position size calculation.
|
|
2845
|
+
* Used internally by strategy execution and public API.
|
|
2846
|
+
*/
|
|
2847
|
+
class SizingGlobalService {
|
|
2848
|
+
constructor() {
|
|
2849
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
2850
|
+
this.sizingConnectionService = inject(TYPES.sizingConnectionService);
|
|
2851
|
+
/**
|
|
2852
|
+
* Calculates position size based on risk parameters.
|
|
2853
|
+
*
|
|
2854
|
+
* @param params - Calculation parameters (symbol, balance, prices, method-specific data)
|
|
2855
|
+
* @param context - Execution context with sizing name
|
|
2856
|
+
* @returns Promise resolving to calculated position size
|
|
2857
|
+
*/
|
|
2858
|
+
this.calculate = async (params, context) => {
|
|
2859
|
+
this.loggerService.log("sizingGlobalService calculate", {
|
|
2860
|
+
symbol: params.symbol,
|
|
2861
|
+
method: params.method,
|
|
2862
|
+
context,
|
|
2863
|
+
});
|
|
2864
|
+
return await this.sizingConnectionService.calculate(params, context);
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
/**
|
|
2870
|
+
* Global service for risk operations.
|
|
2871
|
+
*
|
|
2872
|
+
* Wraps RiskConnectionService for risk limit validation.
|
|
2873
|
+
* Used internally by strategy execution and public API.
|
|
2874
|
+
*/
|
|
2875
|
+
class RiskGlobalService {
|
|
2876
|
+
constructor() {
|
|
2877
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
2878
|
+
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
2879
|
+
/**
|
|
2880
|
+
* Checks if a signal should be allowed based on risk limits.
|
|
2881
|
+
*
|
|
2882
|
+
* @param params - Risk check arguments (portfolio state, position details)
|
|
2883
|
+
* @param context - Execution context with risk name
|
|
2884
|
+
* @returns Promise resolving to risk check result
|
|
2885
|
+
*/
|
|
2886
|
+
this.checkSignal = async (params, context) => {
|
|
2887
|
+
this.loggerService.log("riskGlobalService checkSignal", {
|
|
2888
|
+
symbol: params.symbol,
|
|
2889
|
+
context,
|
|
2890
|
+
});
|
|
2891
|
+
return await this.riskConnectionService.checkSignal(params, context);
|
|
2892
|
+
};
|
|
2893
|
+
/**
|
|
2894
|
+
* Registers an opened signal with the risk management system.
|
|
2895
|
+
*
|
|
2896
|
+
* @param symbol - Trading pair symbol
|
|
2897
|
+
* @param context - Context information (strategyName, riskName)
|
|
2898
|
+
*/
|
|
2899
|
+
this.addSignal = async (symbol, context) => {
|
|
2900
|
+
this.loggerService.log("riskGlobalService addSignal", {
|
|
2901
|
+
symbol,
|
|
2902
|
+
context,
|
|
2903
|
+
});
|
|
2904
|
+
await this.riskConnectionService.addSignal(symbol, context);
|
|
2905
|
+
};
|
|
2906
|
+
/**
|
|
2907
|
+
* Removes a closed signal from the risk management system.
|
|
2908
|
+
*
|
|
2909
|
+
* @param symbol - Trading pair symbol
|
|
2910
|
+
* @param context - Context information (strategyName, riskName)
|
|
2911
|
+
*/
|
|
2912
|
+
this.removeSignal = async (symbol, context) => {
|
|
2913
|
+
this.loggerService.log("riskGlobalService removeSignal", {
|
|
2914
|
+
symbol,
|
|
2915
|
+
context,
|
|
2916
|
+
});
|
|
2917
|
+
await this.riskConnectionService.removeSignal(symbol, context);
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2168
2922
|
/**
|
|
2169
2923
|
* Service for managing exchange schema registry.
|
|
2170
2924
|
*
|
|
@@ -2395,28 +3149,266 @@ class FrameSchemaService {
|
|
|
2395
3149
|
}
|
|
2396
3150
|
|
|
2397
3151
|
/**
|
|
2398
|
-
*
|
|
2399
|
-
*
|
|
2400
|
-
* Flow:
|
|
2401
|
-
* 1. Get timeframes from frame service
|
|
2402
|
-
* 2. Iterate through timeframes calling tick()
|
|
2403
|
-
* 3. When signal opens: fetch candles and call backtest()
|
|
2404
|
-
* 4. Skip timeframes until signal closes
|
|
2405
|
-
* 5. Yield closed result and continue
|
|
3152
|
+
* Service for managing sizing schema registry.
|
|
2406
3153
|
*
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
3154
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
3155
|
+
* Sizing schemas are registered via addSizing() and retrieved by name.
|
|
2409
3156
|
*/
|
|
2410
|
-
class
|
|
3157
|
+
class SizingSchemaService {
|
|
2411
3158
|
constructor() {
|
|
2412
3159
|
this.loggerService = inject(TYPES.loggerService);
|
|
2413
|
-
this.
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
3160
|
+
this._registry = new functoolsKit.ToolRegistry("sizingSchema");
|
|
3161
|
+
/**
|
|
3162
|
+
* Validates sizing schema structure for required properties.
|
|
3163
|
+
*
|
|
3164
|
+
* Performs shallow validation to ensure all required properties exist
|
|
3165
|
+
* and have correct types before registration in the registry.
|
|
3166
|
+
*
|
|
3167
|
+
* @param sizingSchema - Sizing schema to validate
|
|
3168
|
+
* @throws Error if sizingName is missing or not a string
|
|
3169
|
+
* @throws Error if method is missing or not a valid sizing method
|
|
3170
|
+
* @throws Error if required method-specific fields are missing
|
|
3171
|
+
*/
|
|
3172
|
+
this.validateShallow = (sizingSchema) => {
|
|
3173
|
+
this.loggerService.log(`sizingSchemaService validateShallow`, {
|
|
3174
|
+
sizingSchema,
|
|
3175
|
+
});
|
|
3176
|
+
const sizingName = sizingSchema.sizingName;
|
|
3177
|
+
const method = sizingSchema.method;
|
|
3178
|
+
if (typeof sizingName !== "string") {
|
|
3179
|
+
throw new Error(`sizing schema validation failed: missing sizingName`);
|
|
3180
|
+
}
|
|
3181
|
+
if (typeof method !== "string") {
|
|
3182
|
+
throw new Error(`sizing schema validation failed: missing method for sizingName=${sizingName}`);
|
|
3183
|
+
}
|
|
3184
|
+
// Method-specific validation
|
|
3185
|
+
if (sizingSchema.method === "fixed-percentage") {
|
|
3186
|
+
if (typeof sizingSchema.riskPercentage !== "number") {
|
|
3187
|
+
throw new Error(`sizing schema validation failed: missing riskPercentage for fixed-percentage sizing (sizingName=${sizingName})`);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
if (sizingSchema.method === "atr-based") {
|
|
3191
|
+
if (typeof sizingSchema.riskPercentage !== "number") {
|
|
3192
|
+
throw new Error(`sizing schema validation failed: missing riskPercentage for atr-based sizing (sizingName=${sizingName})`);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
2417
3196
|
}
|
|
2418
3197
|
/**
|
|
2419
|
-
*
|
|
3198
|
+
* Registers a new sizing schema.
|
|
3199
|
+
*
|
|
3200
|
+
* @param key - Unique sizing name
|
|
3201
|
+
* @param value - Sizing schema configuration
|
|
3202
|
+
* @throws Error if sizing name already exists
|
|
3203
|
+
*/
|
|
3204
|
+
register(key, value) {
|
|
3205
|
+
this.loggerService.log(`sizingSchemaService register`, { key });
|
|
3206
|
+
this.validateShallow(value);
|
|
3207
|
+
this._registry = this._registry.register(key, value);
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Overrides an existing sizing schema with partial updates.
|
|
3211
|
+
*
|
|
3212
|
+
* @param key - Sizing name to override
|
|
3213
|
+
* @param value - Partial schema updates
|
|
3214
|
+
* @throws Error if sizing name doesn't exist
|
|
3215
|
+
*/
|
|
3216
|
+
override(key, value) {
|
|
3217
|
+
this.loggerService.log(`sizingSchemaService override`, { key });
|
|
3218
|
+
this._registry = this._registry.override(key, value);
|
|
3219
|
+
return this._registry.get(key);
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Retrieves a sizing schema by name.
|
|
3223
|
+
*
|
|
3224
|
+
* @param key - Sizing name
|
|
3225
|
+
* @returns Sizing schema configuration
|
|
3226
|
+
* @throws Error if sizing name doesn't exist
|
|
3227
|
+
*/
|
|
3228
|
+
get(key) {
|
|
3229
|
+
this.loggerService.log(`sizingSchemaService get`, { key });
|
|
3230
|
+
return this._registry.get(key);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
/**
|
|
3235
|
+
* Service for managing risk schema registry.
|
|
3236
|
+
*
|
|
3237
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
3238
|
+
* Risk profiles are registered via addRisk() and retrieved by name.
|
|
3239
|
+
*/
|
|
3240
|
+
class RiskSchemaService {
|
|
3241
|
+
constructor() {
|
|
3242
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3243
|
+
this._registry = new functoolsKit.ToolRegistry("riskSchema");
|
|
3244
|
+
/**
|
|
3245
|
+
* Registers a new risk schema.
|
|
3246
|
+
*
|
|
3247
|
+
* @param key - Unique risk profile name
|
|
3248
|
+
* @param value - Risk schema configuration
|
|
3249
|
+
* @throws Error if risk name already exists
|
|
3250
|
+
*/
|
|
3251
|
+
this.register = (key, value) => {
|
|
3252
|
+
this.loggerService.log(`riskSchemaService register`, { key });
|
|
3253
|
+
this.validateShallow(value);
|
|
3254
|
+
this._registry = this._registry.register(key, value);
|
|
3255
|
+
};
|
|
3256
|
+
/**
|
|
3257
|
+
* Validates risk schema structure for required properties.
|
|
3258
|
+
*
|
|
3259
|
+
* Performs shallow validation to ensure all required properties exist
|
|
3260
|
+
* and have correct types before registration in the registry.
|
|
3261
|
+
*
|
|
3262
|
+
* @param riskSchema - Risk schema to validate
|
|
3263
|
+
* @throws Error if riskName is missing or not a string
|
|
3264
|
+
*/
|
|
3265
|
+
this.validateShallow = (riskSchema) => {
|
|
3266
|
+
this.loggerService.log(`riskSchemaService validateShallow`, {
|
|
3267
|
+
riskSchema,
|
|
3268
|
+
});
|
|
3269
|
+
if (typeof riskSchema.riskName !== "string") {
|
|
3270
|
+
throw new Error(`risk schema validation failed: missing riskName`);
|
|
3271
|
+
}
|
|
3272
|
+
if (riskSchema.validations && !Array.isArray(riskSchema.validations)) {
|
|
3273
|
+
throw new Error(`risk schema validation failed: validations is not an array for riskName=${riskSchema.riskName}`);
|
|
3274
|
+
}
|
|
3275
|
+
if (riskSchema.validations &&
|
|
3276
|
+
riskSchema.validations?.some((validation) => typeof validation !== "function" && !functoolsKit.isObject(validation))) {
|
|
3277
|
+
throw new Error(`risk schema validation failed: invalid validations for riskName=${riskSchema.riskName}`);
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
/**
|
|
3281
|
+
* Overrides an existing risk schema with partial updates.
|
|
3282
|
+
*
|
|
3283
|
+
* @param key - Risk name to override
|
|
3284
|
+
* @param value - Partial schema updates
|
|
3285
|
+
* @returns Updated risk schema
|
|
3286
|
+
* @throws Error if risk name doesn't exist
|
|
3287
|
+
*/
|
|
3288
|
+
this.override = (key, value) => {
|
|
3289
|
+
this.loggerService.log(`riskSchemaService override`, { key });
|
|
3290
|
+
this._registry = this._registry.override(key, value);
|
|
3291
|
+
return this._registry.get(key);
|
|
3292
|
+
};
|
|
3293
|
+
/**
|
|
3294
|
+
* Retrieves a risk schema by name.
|
|
3295
|
+
*
|
|
3296
|
+
* @param key - Risk name
|
|
3297
|
+
* @returns Risk schema configuration
|
|
3298
|
+
* @throws Error if risk name doesn't exist
|
|
3299
|
+
*/
|
|
3300
|
+
this.get = (key) => {
|
|
3301
|
+
this.loggerService.log(`riskSchemaService get`, { key });
|
|
3302
|
+
return this._registry.get(key);
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
/**
|
|
3308
|
+
* Service for managing walker schema registry.
|
|
3309
|
+
*
|
|
3310
|
+
* Uses ToolRegistry from functools-kit for type-safe schema storage.
|
|
3311
|
+
* Walkers are registered via addWalker() and retrieved by name.
|
|
3312
|
+
*/
|
|
3313
|
+
class WalkerSchemaService {
|
|
3314
|
+
constructor() {
|
|
3315
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3316
|
+
this._registry = new functoolsKit.ToolRegistry("walkerSchema");
|
|
3317
|
+
/**
|
|
3318
|
+
* Registers a new walker schema.
|
|
3319
|
+
*
|
|
3320
|
+
* @param key - Unique walker name
|
|
3321
|
+
* @param value - Walker schema configuration
|
|
3322
|
+
* @throws Error if walker name already exists
|
|
3323
|
+
*/
|
|
3324
|
+
this.register = (key, value) => {
|
|
3325
|
+
this.loggerService.log(`walkerSchemaService register`, { key });
|
|
3326
|
+
this.validateShallow(value);
|
|
3327
|
+
this._registry = this._registry.register(key, value);
|
|
3328
|
+
};
|
|
3329
|
+
/**
|
|
3330
|
+
* Validates walker schema structure for required properties.
|
|
3331
|
+
*
|
|
3332
|
+
* Performs shallow validation to ensure all required properties exist
|
|
3333
|
+
* and have correct types before registration in the registry.
|
|
3334
|
+
*
|
|
3335
|
+
* @param walkerSchema - Walker schema to validate
|
|
3336
|
+
* @throws Error if walkerName is missing or not a string
|
|
3337
|
+
* @throws Error if exchangeName is missing or not a string
|
|
3338
|
+
* @throws Error if frameName is missing or not a string
|
|
3339
|
+
* @throws Error if strategies is missing or not an array
|
|
3340
|
+
* @throws Error if strategies array is empty
|
|
3341
|
+
*/
|
|
3342
|
+
this.validateShallow = (walkerSchema) => {
|
|
3343
|
+
this.loggerService.log(`walkerSchemaService validateShallow`, {
|
|
3344
|
+
walkerSchema,
|
|
3345
|
+
});
|
|
3346
|
+
if (typeof walkerSchema.walkerName !== "string") {
|
|
3347
|
+
throw new Error(`walker schema validation failed: missing walkerName`);
|
|
3348
|
+
}
|
|
3349
|
+
if (typeof walkerSchema.exchangeName !== "string") {
|
|
3350
|
+
throw new Error(`walker schema validation failed: missing exchangeName for walkerName=${walkerSchema.walkerName}`);
|
|
3351
|
+
}
|
|
3352
|
+
if (typeof walkerSchema.frameName !== "string") {
|
|
3353
|
+
throw new Error(`walker schema validation failed: missing frameName for walkerName=${walkerSchema.walkerName}`);
|
|
3354
|
+
}
|
|
3355
|
+
if (!Array.isArray(walkerSchema.strategies)) {
|
|
3356
|
+
throw new Error(`walker schema validation failed: strategies must be an array for walkerName=${walkerSchema.walkerName}`);
|
|
3357
|
+
}
|
|
3358
|
+
if (walkerSchema.strategies.length === 0) {
|
|
3359
|
+
throw new Error(`walker schema validation failed: strategies array cannot be empty for walkerName=${walkerSchema.walkerName}`);
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
/**
|
|
3363
|
+
* Overrides an existing walker schema with partial updates.
|
|
3364
|
+
*
|
|
3365
|
+
* @param key - Walker name to override
|
|
3366
|
+
* @param value - Partial schema updates
|
|
3367
|
+
* @returns Updated walker schema
|
|
3368
|
+
* @throws Error if walker name doesn't exist
|
|
3369
|
+
*/
|
|
3370
|
+
this.override = (key, value) => {
|
|
3371
|
+
this.loggerService.log(`walkerSchemaService override`, { key });
|
|
3372
|
+
this._registry = this._registry.override(key, value);
|
|
3373
|
+
return this._registry.get(key);
|
|
3374
|
+
};
|
|
3375
|
+
/**
|
|
3376
|
+
* Retrieves a walker schema by name.
|
|
3377
|
+
*
|
|
3378
|
+
* @param key - Walker name
|
|
3379
|
+
* @returns Walker schema configuration
|
|
3380
|
+
* @throws Error if walker name doesn't exist
|
|
3381
|
+
*/
|
|
3382
|
+
this.get = (key) => {
|
|
3383
|
+
this.loggerService.log(`walkerSchemaService get`, { key });
|
|
3384
|
+
return this._registry.get(key);
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
/**
|
|
3390
|
+
* Private service for backtest orchestration using async generators.
|
|
3391
|
+
*
|
|
3392
|
+
* Flow:
|
|
3393
|
+
* 1. Get timeframes from frame service
|
|
3394
|
+
* 2. Iterate through timeframes calling tick()
|
|
3395
|
+
* 3. When signal opens: fetch candles and call backtest()
|
|
3396
|
+
* 4. Skip timeframes until signal closes
|
|
3397
|
+
* 5. Yield closed result and continue
|
|
3398
|
+
*
|
|
3399
|
+
* Memory efficient: streams results without array accumulation.
|
|
3400
|
+
* Supports early termination via break in consumer.
|
|
3401
|
+
*/
|
|
3402
|
+
class BacktestLogicPrivateService {
|
|
3403
|
+
constructor() {
|
|
3404
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3405
|
+
this.strategyGlobalService = inject(TYPES.strategyGlobalService);
|
|
3406
|
+
this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
|
|
3407
|
+
this.frameGlobalService = inject(TYPES.frameGlobalService);
|
|
3408
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Runs backtest for a symbol, streaming closed signals as async generator.
|
|
2420
3412
|
*
|
|
2421
3413
|
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2422
3414
|
* @yields Closed signal results with PNL
|
|
@@ -2437,6 +3429,7 @@ class BacktestLogicPrivateService {
|
|
|
2437
3429
|
const timeframes = await this.frameGlobalService.getTimeframe(symbol);
|
|
2438
3430
|
const totalFrames = timeframes.length;
|
|
2439
3431
|
let i = 0;
|
|
3432
|
+
let previousEventTimestamp = null;
|
|
2440
3433
|
while (i < timeframes.length) {
|
|
2441
3434
|
const timeframeStartTime = performance.now();
|
|
2442
3435
|
const when = timeframes[i];
|
|
@@ -2481,8 +3474,10 @@ class BacktestLogicPrivateService {
|
|
|
2481
3474
|
});
|
|
2482
3475
|
// Track signal processing duration
|
|
2483
3476
|
const signalEndTime = performance.now();
|
|
3477
|
+
const currentTimestamp = Date.now();
|
|
2484
3478
|
await performanceEmitter.next({
|
|
2485
|
-
timestamp:
|
|
3479
|
+
timestamp: currentTimestamp,
|
|
3480
|
+
previousTimestamp: previousEventTimestamp,
|
|
2486
3481
|
metricType: "backtest_signal",
|
|
2487
3482
|
duration: signalEndTime - signalStartTime,
|
|
2488
3483
|
strategyName: this.methodContextService.context.strategyName,
|
|
@@ -2490,6 +3485,7 @@ class BacktestLogicPrivateService {
|
|
|
2490
3485
|
symbol,
|
|
2491
3486
|
backtest: true,
|
|
2492
3487
|
});
|
|
3488
|
+
previousEventTimestamp = currentTimestamp;
|
|
2493
3489
|
// Пропускаем timeframes до closeTimestamp
|
|
2494
3490
|
while (i < timeframes.length &&
|
|
2495
3491
|
timeframes[i].getTime() < backtestResult.closeTimestamp) {
|
|
@@ -2499,8 +3495,10 @@ class BacktestLogicPrivateService {
|
|
|
2499
3495
|
}
|
|
2500
3496
|
// Track timeframe processing duration
|
|
2501
3497
|
const timeframeEndTime = performance.now();
|
|
3498
|
+
const currentTimestamp = Date.now();
|
|
2502
3499
|
await performanceEmitter.next({
|
|
2503
|
-
timestamp:
|
|
3500
|
+
timestamp: currentTimestamp,
|
|
3501
|
+
previousTimestamp: previousEventTimestamp,
|
|
2504
3502
|
metricType: "backtest_timeframe",
|
|
2505
3503
|
duration: timeframeEndTime - timeframeStartTime,
|
|
2506
3504
|
strategyName: this.methodContextService.context.strategyName,
|
|
@@ -2508,6 +3506,7 @@ class BacktestLogicPrivateService {
|
|
|
2508
3506
|
symbol,
|
|
2509
3507
|
backtest: true,
|
|
2510
3508
|
});
|
|
3509
|
+
previousEventTimestamp = currentTimestamp;
|
|
2511
3510
|
i++;
|
|
2512
3511
|
}
|
|
2513
3512
|
// Emit final progress event (100%)
|
|
@@ -2523,8 +3522,10 @@ class BacktestLogicPrivateService {
|
|
|
2523
3522
|
}
|
|
2524
3523
|
// Track total backtest duration
|
|
2525
3524
|
const backtestEndTime = performance.now();
|
|
3525
|
+
const currentTimestamp = Date.now();
|
|
2526
3526
|
await performanceEmitter.next({
|
|
2527
|
-
timestamp:
|
|
3527
|
+
timestamp: currentTimestamp,
|
|
3528
|
+
previousTimestamp: previousEventTimestamp,
|
|
2528
3529
|
metricType: "backtest_total",
|
|
2529
3530
|
duration: backtestEndTime - backtestStartTime,
|
|
2530
3531
|
strategyName: this.methodContextService.context.strategyName,
|
|
@@ -2532,6 +3533,7 @@ class BacktestLogicPrivateService {
|
|
|
2532
3533
|
symbol,
|
|
2533
3534
|
backtest: true,
|
|
2534
3535
|
});
|
|
3536
|
+
previousEventTimestamp = currentTimestamp;
|
|
2535
3537
|
}
|
|
2536
3538
|
}
|
|
2537
3539
|
|
|
@@ -2584,6 +3586,7 @@ class LiveLogicPrivateService {
|
|
|
2584
3586
|
this.loggerService.log("liveLogicPrivateService run", {
|
|
2585
3587
|
symbol,
|
|
2586
3588
|
});
|
|
3589
|
+
let previousEventTimestamp = null;
|
|
2587
3590
|
while (true) {
|
|
2588
3591
|
const tickStartTime = performance.now();
|
|
2589
3592
|
const when = new Date();
|
|
@@ -2594,8 +3597,10 @@ class LiveLogicPrivateService {
|
|
|
2594
3597
|
});
|
|
2595
3598
|
// Track tick duration
|
|
2596
3599
|
const tickEndTime = performance.now();
|
|
3600
|
+
const currentTimestamp = Date.now();
|
|
2597
3601
|
await performanceEmitter.next({
|
|
2598
|
-
timestamp:
|
|
3602
|
+
timestamp: currentTimestamp,
|
|
3603
|
+
previousTimestamp: previousEventTimestamp,
|
|
2599
3604
|
metricType: "live_tick",
|
|
2600
3605
|
duration: tickEndTime - tickStartTime,
|
|
2601
3606
|
strategyName: this.methodContextService.context.strategyName,
|
|
@@ -2603,6 +3608,7 @@ class LiveLogicPrivateService {
|
|
|
2603
3608
|
symbol,
|
|
2604
3609
|
backtest: false,
|
|
2605
3610
|
});
|
|
3611
|
+
previousEventTimestamp = currentTimestamp;
|
|
2606
3612
|
if (result.action === "active") {
|
|
2607
3613
|
await functoolsKit.sleep(TICK_TTL);
|
|
2608
3614
|
continue;
|
|
@@ -2617,6 +3623,144 @@ class LiveLogicPrivateService {
|
|
|
2617
3623
|
}
|
|
2618
3624
|
}
|
|
2619
3625
|
|
|
3626
|
+
/**
|
|
3627
|
+
* Private service for walker orchestration (strategy comparison).
|
|
3628
|
+
*
|
|
3629
|
+
* Flow:
|
|
3630
|
+
* 1. Yields progress updates as each strategy completes
|
|
3631
|
+
* 2. Tracks best metric in real-time
|
|
3632
|
+
* 3. Returns final results with all strategies ranked
|
|
3633
|
+
*
|
|
3634
|
+
* Uses BacktestLogicPublicService internally for each strategy.
|
|
3635
|
+
*/
|
|
3636
|
+
class WalkerLogicPrivateService {
|
|
3637
|
+
constructor() {
|
|
3638
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3639
|
+
this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
3640
|
+
this.backtestMarkdownService = inject(TYPES.backtestMarkdownService);
|
|
3641
|
+
this.walkerSchemaService = inject(TYPES.walkerSchemaService);
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Runs walker comparison for a symbol.
|
|
3645
|
+
*
|
|
3646
|
+
* Executes backtest for each strategy sequentially.
|
|
3647
|
+
* Yields WalkerContract after each strategy completes.
|
|
3648
|
+
*
|
|
3649
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3650
|
+
* @param strategies - List of strategy names to compare
|
|
3651
|
+
* @param metric - Metric to use for comparison
|
|
3652
|
+
* @param context - Walker context with exchangeName, frameName, walkerName
|
|
3653
|
+
* @yields WalkerContract with progress after each strategy
|
|
3654
|
+
*
|
|
3655
|
+
* @example
|
|
3656
|
+
* ```typescript
|
|
3657
|
+
* for await (const progress of walkerLogic.run(
|
|
3658
|
+
* "BTCUSDT",
|
|
3659
|
+
* ["strategy-v1", "strategy-v2"],
|
|
3660
|
+
* "sharpeRatio",
|
|
3661
|
+
* {
|
|
3662
|
+
* exchangeName: "binance",
|
|
3663
|
+
* frameName: "1d-backtest",
|
|
3664
|
+
* walkerName: "my-optimizer"
|
|
3665
|
+
* }
|
|
3666
|
+
* )) {
|
|
3667
|
+
* console.log("Progress:", progress.strategiesTested, "/", progress.totalStrategies);
|
|
3668
|
+
* }
|
|
3669
|
+
* ```
|
|
3670
|
+
*/
|
|
3671
|
+
async *run(symbol, strategies, metric, context) {
|
|
3672
|
+
this.loggerService.log("walkerLogicPrivateService run", {
|
|
3673
|
+
symbol,
|
|
3674
|
+
strategies,
|
|
3675
|
+
metric,
|
|
3676
|
+
context,
|
|
3677
|
+
});
|
|
3678
|
+
// Get walker schema for callbacks
|
|
3679
|
+
const walkerSchema = this.walkerSchemaService.get(context.walkerName);
|
|
3680
|
+
let strategiesTested = 0;
|
|
3681
|
+
let bestMetric = null;
|
|
3682
|
+
let bestStrategy = null;
|
|
3683
|
+
// Run backtest for each strategy
|
|
3684
|
+
for (const strategyName of strategies) {
|
|
3685
|
+
// Call onStrategyStart callback if provided
|
|
3686
|
+
if (walkerSchema.callbacks?.onStrategyStart) {
|
|
3687
|
+
walkerSchema.callbacks.onStrategyStart(strategyName, symbol);
|
|
3688
|
+
}
|
|
3689
|
+
this.loggerService.info("walkerLogicPrivateService testing strategy", {
|
|
3690
|
+
strategyName,
|
|
3691
|
+
symbol,
|
|
3692
|
+
});
|
|
3693
|
+
const iterator = this.backtestLogicPublicService.run(symbol, {
|
|
3694
|
+
strategyName,
|
|
3695
|
+
exchangeName: context.exchangeName,
|
|
3696
|
+
frameName: context.frameName,
|
|
3697
|
+
});
|
|
3698
|
+
await functoolsKit.resolveDocuments(iterator);
|
|
3699
|
+
this.loggerService.info("walkerLogicPrivateService backtest complete", {
|
|
3700
|
+
strategyName,
|
|
3701
|
+
symbol,
|
|
3702
|
+
});
|
|
3703
|
+
// Get statistics from BacktestMarkdownService
|
|
3704
|
+
const stats = await this.backtestMarkdownService.getData(strategyName);
|
|
3705
|
+
// Extract metric value
|
|
3706
|
+
const value = stats[metric];
|
|
3707
|
+
const metricValue = value !== null &&
|
|
3708
|
+
value !== undefined &&
|
|
3709
|
+
typeof value === "number" &&
|
|
3710
|
+
!isNaN(value) &&
|
|
3711
|
+
isFinite(value)
|
|
3712
|
+
? value
|
|
3713
|
+
: null;
|
|
3714
|
+
// Update best strategy if needed
|
|
3715
|
+
const isBetter = bestMetric === null ||
|
|
3716
|
+
(metricValue !== null && metricValue > bestMetric);
|
|
3717
|
+
if (isBetter && metricValue !== null) {
|
|
3718
|
+
bestMetric = metricValue;
|
|
3719
|
+
bestStrategy = strategyName;
|
|
3720
|
+
}
|
|
3721
|
+
strategiesTested++;
|
|
3722
|
+
const walkerContract = {
|
|
3723
|
+
walkerName: context.walkerName,
|
|
3724
|
+
exchangeName: context.exchangeName,
|
|
3725
|
+
frameName: context.frameName,
|
|
3726
|
+
symbol,
|
|
3727
|
+
strategyName,
|
|
3728
|
+
stats,
|
|
3729
|
+
metricValue,
|
|
3730
|
+
metric,
|
|
3731
|
+
bestMetric,
|
|
3732
|
+
bestStrategy,
|
|
3733
|
+
strategiesTested,
|
|
3734
|
+
totalStrategies: strategies.length,
|
|
3735
|
+
};
|
|
3736
|
+
// Call onStrategyComplete callback if provided
|
|
3737
|
+
if (walkerSchema.callbacks?.onStrategyComplete) {
|
|
3738
|
+
walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
|
|
3739
|
+
}
|
|
3740
|
+
await walkerEmitter.next(walkerContract);
|
|
3741
|
+
yield walkerContract;
|
|
3742
|
+
}
|
|
3743
|
+
const finalResults = {
|
|
3744
|
+
walkerName: context.walkerName,
|
|
3745
|
+
symbol,
|
|
3746
|
+
exchangeName: context.exchangeName,
|
|
3747
|
+
frameName: context.frameName,
|
|
3748
|
+
metric,
|
|
3749
|
+
totalStrategies: strategies.length,
|
|
3750
|
+
bestStrategy,
|
|
3751
|
+
bestMetric,
|
|
3752
|
+
bestStats: bestStrategy !== null
|
|
3753
|
+
? await this.backtestMarkdownService.getData(bestStrategy)
|
|
3754
|
+
: null,
|
|
3755
|
+
};
|
|
3756
|
+
// Call onComplete callback if provided with final best results
|
|
3757
|
+
if (walkerSchema.callbacks?.onComplete) {
|
|
3758
|
+
walkerSchema.callbacks.onComplete(finalResults);
|
|
3759
|
+
}
|
|
3760
|
+
await walkerCompleteSubject.next(finalResults);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
|
|
2620
3764
|
/**
|
|
2621
3765
|
* Public service for backtest orchestration with context management.
|
|
2622
3766
|
*
|
|
@@ -2729,7 +3873,58 @@ class LiveLogicPublicService {
|
|
|
2729
3873
|
}
|
|
2730
3874
|
}
|
|
2731
3875
|
|
|
2732
|
-
|
|
3876
|
+
/**
|
|
3877
|
+
* Public service for walker orchestration with context management.
|
|
3878
|
+
*
|
|
3879
|
+
* Wraps WalkerLogicPrivateService with MethodContextService to provide
|
|
3880
|
+
* implicit context propagation for strategyName, exchangeName, frameName, and walkerName.
|
|
3881
|
+
*
|
|
3882
|
+
* @example
|
|
3883
|
+
* ```typescript
|
|
3884
|
+
* const walkerLogicPublicService = inject(TYPES.walkerLogicPublicService);
|
|
3885
|
+
*
|
|
3886
|
+
* const results = await walkerLogicPublicService.run("BTCUSDT", {
|
|
3887
|
+
* walkerName: "my-optimizer",
|
|
3888
|
+
* exchangeName: "binance",
|
|
3889
|
+
* frameName: "1d-backtest",
|
|
3890
|
+
* strategies: ["strategy-v1", "strategy-v2"],
|
|
3891
|
+
* metric: "sharpeRatio",
|
|
3892
|
+
* });
|
|
3893
|
+
*
|
|
3894
|
+
* console.log("Best strategy:", results.bestStrategy);
|
|
3895
|
+
* ```
|
|
3896
|
+
*/
|
|
3897
|
+
class WalkerLogicPublicService {
|
|
3898
|
+
constructor() {
|
|
3899
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
3900
|
+
this.walkerLogicPrivateService = inject(TYPES.walkerLogicPrivateService);
|
|
3901
|
+
this.walkerSchemaService = inject(TYPES.walkerSchemaService);
|
|
3902
|
+
/**
|
|
3903
|
+
* Runs walker comparison for a symbol with context propagation.
|
|
3904
|
+
*
|
|
3905
|
+
* Executes backtests for all strategies.
|
|
3906
|
+
*
|
|
3907
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
3908
|
+
* @param context - Walker context with strategies and metric
|
|
3909
|
+
*/
|
|
3910
|
+
this.run = (symbol, context) => {
|
|
3911
|
+
this.loggerService.log("walkerLogicPublicService run", {
|
|
3912
|
+
symbol,
|
|
3913
|
+
context,
|
|
3914
|
+
});
|
|
3915
|
+
// Get walker schema
|
|
3916
|
+
const walkerSchema = this.walkerSchemaService.get(context.walkerName);
|
|
3917
|
+
// Run walker private service with strategies and metric from schema
|
|
3918
|
+
return this.walkerLogicPrivateService.run(symbol, walkerSchema.strategies, walkerSchema.metric || "sharpeRatio", {
|
|
3919
|
+
exchangeName: context.exchangeName,
|
|
3920
|
+
frameName: context.frameName,
|
|
3921
|
+
walkerName: context.walkerName,
|
|
3922
|
+
});
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
const METHOD_NAME_RUN$2 = "liveGlobalService run";
|
|
2733
3928
|
/**
|
|
2734
3929
|
* Global service providing access to live trading functionality.
|
|
2735
3930
|
*
|
|
@@ -2752,18 +3947,18 @@ class LiveGlobalService {
|
|
|
2752
3947
|
* @returns Infinite async generator yielding opened and closed signals
|
|
2753
3948
|
*/
|
|
2754
3949
|
this.run = (symbol, context) => {
|
|
2755
|
-
this.loggerService.log(METHOD_NAME_RUN$
|
|
3950
|
+
this.loggerService.log(METHOD_NAME_RUN$2, {
|
|
2756
3951
|
symbol,
|
|
2757
3952
|
context,
|
|
2758
3953
|
});
|
|
2759
|
-
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$
|
|
2760
|
-
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$
|
|
3954
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
|
|
3955
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
|
|
2761
3956
|
return this.liveLogicPublicService.run(symbol, context);
|
|
2762
3957
|
};
|
|
2763
3958
|
}
|
|
2764
3959
|
}
|
|
2765
3960
|
|
|
2766
|
-
const METHOD_NAME_RUN = "backtestGlobalService run";
|
|
3961
|
+
const METHOD_NAME_RUN$1 = "backtestGlobalService run";
|
|
2767
3962
|
/**
|
|
2768
3963
|
* Global service providing access to backtest functionality.
|
|
2769
3964
|
*
|
|
@@ -2785,25 +3980,52 @@ class BacktestGlobalService {
|
|
|
2785
3980
|
* @returns Async generator yielding closed signals with PNL
|
|
2786
3981
|
*/
|
|
2787
3982
|
this.run = (symbol, context) => {
|
|
2788
|
-
this.loggerService.log(METHOD_NAME_RUN, {
|
|
3983
|
+
this.loggerService.log(METHOD_NAME_RUN$1, {
|
|
2789
3984
|
symbol,
|
|
2790
3985
|
context,
|
|
2791
3986
|
});
|
|
2792
|
-
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN);
|
|
2793
|
-
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
|
|
2794
|
-
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
|
|
3987
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
|
|
3988
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
|
|
3989
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
|
|
2795
3990
|
return this.backtestLogicPublicService.run(symbol, context);
|
|
2796
3991
|
};
|
|
2797
3992
|
}
|
|
2798
3993
|
}
|
|
2799
3994
|
|
|
3995
|
+
const METHOD_NAME_RUN = "walkerGlobalService run";
|
|
3996
|
+
/**
|
|
3997
|
+
* Global service providing access to walker functionality.
|
|
3998
|
+
*
|
|
3999
|
+
* Simple wrapper around WalkerLogicPublicService for dependency injection.
|
|
4000
|
+
* Used by public API exports.
|
|
4001
|
+
*/
|
|
4002
|
+
class WalkerGlobalService {
|
|
4003
|
+
constructor() {
|
|
4004
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
4005
|
+
this.walkerLogicPublicService = inject(TYPES.walkerLogicPublicService);
|
|
4006
|
+
/**
|
|
4007
|
+
* Runs walker comparison for a symbol with context propagation.
|
|
4008
|
+
*
|
|
4009
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
4010
|
+
* @param context - Walker context with strategies and metric
|
|
4011
|
+
*/
|
|
4012
|
+
this.run = (symbol, context) => {
|
|
4013
|
+
this.loggerService.log(METHOD_NAME_RUN, {
|
|
4014
|
+
symbol,
|
|
4015
|
+
context,
|
|
4016
|
+
});
|
|
4017
|
+
return this.walkerLogicPublicService.run(symbol, context);
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
2800
4022
|
/**
|
|
2801
4023
|
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
2802
4024
|
*
|
|
2803
4025
|
* @param value - Value to check
|
|
2804
4026
|
* @returns true if value is unsafe, false otherwise
|
|
2805
4027
|
*/
|
|
2806
|
-
function isUnsafe$
|
|
4028
|
+
function isUnsafe$3(value) {
|
|
2807
4029
|
if (typeof value !== "number") {
|
|
2808
4030
|
return true;
|
|
2809
4031
|
}
|
|
@@ -2815,7 +4037,7 @@ function isUnsafe$1(value) {
|
|
|
2815
4037
|
}
|
|
2816
4038
|
return false;
|
|
2817
4039
|
}
|
|
2818
|
-
const columns$
|
|
4040
|
+
const columns$2 = [
|
|
2819
4041
|
{
|
|
2820
4042
|
key: "signalId",
|
|
2821
4043
|
label: "Signal ID",
|
|
@@ -2893,7 +4115,7 @@ const columns$1 = [
|
|
|
2893
4115
|
* Storage class for accumulating closed signals per strategy.
|
|
2894
4116
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
2895
4117
|
*/
|
|
2896
|
-
let ReportStorage$
|
|
4118
|
+
let ReportStorage$2 = class ReportStorage {
|
|
2897
4119
|
constructor() {
|
|
2898
4120
|
/** Internal list of all closed signals for this strategy */
|
|
2899
4121
|
this._signalList = [];
|
|
@@ -2962,14 +4184,14 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
2962
4184
|
totalSignals,
|
|
2963
4185
|
winCount,
|
|
2964
4186
|
lossCount,
|
|
2965
|
-
winRate: isUnsafe$
|
|
2966
|
-
avgPnl: isUnsafe$
|
|
2967
|
-
totalPnl: isUnsafe$
|
|
2968
|
-
stdDev: isUnsafe$
|
|
2969
|
-
sharpeRatio: isUnsafe$
|
|
2970
|
-
annualizedSharpeRatio: isUnsafe$
|
|
2971
|
-
certaintyRatio: isUnsafe$
|
|
2972
|
-
expectedYearlyReturns: isUnsafe$
|
|
4187
|
+
winRate: isUnsafe$3(winRate) ? null : winRate,
|
|
4188
|
+
avgPnl: isUnsafe$3(avgPnl) ? null : avgPnl,
|
|
4189
|
+
totalPnl: isUnsafe$3(totalPnl) ? null : totalPnl,
|
|
4190
|
+
stdDev: isUnsafe$3(stdDev) ? null : stdDev,
|
|
4191
|
+
sharpeRatio: isUnsafe$3(sharpeRatio) ? null : sharpeRatio,
|
|
4192
|
+
annualizedSharpeRatio: isUnsafe$3(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
4193
|
+
certaintyRatio: isUnsafe$3(certaintyRatio) ? null : certaintyRatio,
|
|
4194
|
+
expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
2973
4195
|
};
|
|
2974
4196
|
}
|
|
2975
4197
|
/**
|
|
@@ -2983,9 +4205,9 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
2983
4205
|
if (stats.totalSignals === 0) {
|
|
2984
4206
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
2985
4207
|
}
|
|
2986
|
-
const header = columns$
|
|
2987
|
-
const separator = columns$
|
|
2988
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
4208
|
+
const header = columns$2.map((col) => col.label);
|
|
4209
|
+
const separator = columns$2.map(() => "---");
|
|
4210
|
+
const rows = this._signalList.map((closedSignal) => columns$2.map((col) => col.format(closedSignal)));
|
|
2989
4211
|
const tableData = [header, separator, ...rows];
|
|
2990
4212
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
2991
4213
|
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)`}`);
|
|
@@ -3046,7 +4268,7 @@ class BacktestMarkdownService {
|
|
|
3046
4268
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
3047
4269
|
* Each strategy gets its own isolated storage instance.
|
|
3048
4270
|
*/
|
|
3049
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
4271
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
3050
4272
|
/**
|
|
3051
4273
|
* Processes tick events and accumulates closed signals.
|
|
3052
4274
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -3193,7 +4415,7 @@ class BacktestMarkdownService {
|
|
|
3193
4415
|
* @param value - Value to check
|
|
3194
4416
|
* @returns true if value is unsafe, false otherwise
|
|
3195
4417
|
*/
|
|
3196
|
-
function isUnsafe(value) {
|
|
4418
|
+
function isUnsafe$2(value) {
|
|
3197
4419
|
if (typeof value !== "number") {
|
|
3198
4420
|
return true;
|
|
3199
4421
|
}
|
|
@@ -3205,7 +4427,7 @@ function isUnsafe(value) {
|
|
|
3205
4427
|
}
|
|
3206
4428
|
return false;
|
|
3207
4429
|
}
|
|
3208
|
-
const columns = [
|
|
4430
|
+
const columns$1 = [
|
|
3209
4431
|
{
|
|
3210
4432
|
key: "timestamp",
|
|
3211
4433
|
label: "Timestamp",
|
|
@@ -3284,7 +4506,7 @@ const MAX_EVENTS$1 = 250;
|
|
|
3284
4506
|
* Storage class for accumulating all tick events per strategy.
|
|
3285
4507
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
3286
4508
|
*/
|
|
3287
|
-
class ReportStorage {
|
|
4509
|
+
let ReportStorage$1 = class ReportStorage {
|
|
3288
4510
|
constructor() {
|
|
3289
4511
|
/** Internal list of all tick events for this strategy */
|
|
3290
4512
|
this._eventList = [];
|
|
@@ -3482,14 +4704,14 @@ class ReportStorage {
|
|
|
3482
4704
|
totalClosed,
|
|
3483
4705
|
winCount,
|
|
3484
4706
|
lossCount,
|
|
3485
|
-
winRate: isUnsafe(winRate) ? null : winRate,
|
|
3486
|
-
avgPnl: isUnsafe(avgPnl) ? null : avgPnl,
|
|
3487
|
-
totalPnl: isUnsafe(totalPnl) ? null : totalPnl,
|
|
3488
|
-
stdDev: isUnsafe(stdDev) ? null : stdDev,
|
|
3489
|
-
sharpeRatio: isUnsafe(sharpeRatio) ? null : sharpeRatio,
|
|
3490
|
-
annualizedSharpeRatio: isUnsafe(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
3491
|
-
certaintyRatio: isUnsafe(certaintyRatio) ? null : certaintyRatio,
|
|
3492
|
-
expectedYearlyReturns: isUnsafe(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
4707
|
+
winRate: isUnsafe$2(winRate) ? null : winRate,
|
|
4708
|
+
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
4709
|
+
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
4710
|
+
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
4711
|
+
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
4712
|
+
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
4713
|
+
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
4714
|
+
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
3493
4715
|
};
|
|
3494
4716
|
}
|
|
3495
4717
|
/**
|
|
@@ -3503,9 +4725,9 @@ class ReportStorage {
|
|
|
3503
4725
|
if (stats.totalEvents === 0) {
|
|
3504
4726
|
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
3505
4727
|
}
|
|
3506
|
-
const header = columns.map((col) => col.label);
|
|
3507
|
-
const separator = columns.map(() => "---");
|
|
3508
|
-
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
4728
|
+
const header = columns$1.map((col) => col.label);
|
|
4729
|
+
const separator = columns$1.map(() => "---");
|
|
4730
|
+
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
3509
4731
|
const tableData = [header, separator, ...rows];
|
|
3510
4732
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
3511
4733
|
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)`}`);
|
|
@@ -3530,7 +4752,7 @@ class ReportStorage {
|
|
|
3530
4752
|
console.error(`Failed to save markdown report:`, error);
|
|
3531
4753
|
}
|
|
3532
4754
|
}
|
|
3533
|
-
}
|
|
4755
|
+
};
|
|
3534
4756
|
/**
|
|
3535
4757
|
* Service for generating and saving live trading markdown reports.
|
|
3536
4758
|
*
|
|
@@ -3569,7 +4791,7 @@ class LiveMarkdownService {
|
|
|
3569
4791
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
3570
4792
|
* Each strategy gets its own isolated storage instance.
|
|
3571
4793
|
*/
|
|
3572
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage());
|
|
4794
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
|
|
3573
4795
|
/**
|
|
3574
4796
|
* Processes tick events and accumulates all event types.
|
|
3575
4797
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -3785,9 +5007,26 @@ class PerformanceStorage {
|
|
|
3785
5007
|
const variance = durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
|
|
3786
5008
|
durations.length;
|
|
3787
5009
|
const stdDev = Math.sqrt(variance);
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
5010
|
+
// Calculate wait times between events
|
|
5011
|
+
const waitTimes = [];
|
|
5012
|
+
for (let i = 0; i < events.length; i++) {
|
|
5013
|
+
if (events[i].previousTimestamp !== null) {
|
|
5014
|
+
const waitTime = events[i].timestamp - events[i].previousTimestamp;
|
|
5015
|
+
waitTimes.push(waitTime);
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
const sortedWaitTimes = waitTimes.sort((a, b) => a - b);
|
|
5019
|
+
const avgWaitTime = sortedWaitTimes.length > 0
|
|
5020
|
+
? sortedWaitTimes.reduce((sum, w) => sum + w, 0) /
|
|
5021
|
+
sortedWaitTimes.length
|
|
5022
|
+
: 0;
|
|
5023
|
+
const minWaitTime = sortedWaitTimes.length > 0 ? sortedWaitTimes[0] : 0;
|
|
5024
|
+
const maxWaitTime = sortedWaitTimes.length > 0
|
|
5025
|
+
? sortedWaitTimes[sortedWaitTimes.length - 1]
|
|
5026
|
+
: 0;
|
|
5027
|
+
metricStats[metricType] = {
|
|
5028
|
+
metricType,
|
|
5029
|
+
count: events.length,
|
|
3791
5030
|
totalDuration,
|
|
3792
5031
|
avgDuration,
|
|
3793
5032
|
minDuration: durations[0],
|
|
@@ -3796,6 +5035,9 @@ class PerformanceStorage {
|
|
|
3796
5035
|
median: percentile(durations, 50),
|
|
3797
5036
|
p95: percentile(durations, 95),
|
|
3798
5037
|
p99: percentile(durations, 99),
|
|
5038
|
+
avgWaitTime,
|
|
5039
|
+
minWaitTime,
|
|
5040
|
+
maxWaitTime,
|
|
3799
5041
|
};
|
|
3800
5042
|
}
|
|
3801
5043
|
const totalDuration = this._events.reduce((sum, e) => sum + e.duration, 0);
|
|
@@ -3832,6 +5074,9 @@ class PerformanceStorage {
|
|
|
3832
5074
|
"Median (ms)",
|
|
3833
5075
|
"P95 (ms)",
|
|
3834
5076
|
"P99 (ms)",
|
|
5077
|
+
"Avg Wait (ms)",
|
|
5078
|
+
"Min Wait (ms)",
|
|
5079
|
+
"Max Wait (ms)",
|
|
3835
5080
|
];
|
|
3836
5081
|
const summarySeparator = summaryHeader.map(() => "---");
|
|
3837
5082
|
const summaryRows = sortedMetrics.map((metric) => [
|
|
@@ -3845,6 +5090,9 @@ class PerformanceStorage {
|
|
|
3845
5090
|
metric.median.toFixed(2),
|
|
3846
5091
|
metric.p95.toFixed(2),
|
|
3847
5092
|
metric.p99.toFixed(2),
|
|
5093
|
+
metric.avgWaitTime.toFixed(2),
|
|
5094
|
+
metric.minWaitTime.toFixed(2),
|
|
5095
|
+
metric.maxWaitTime.toFixed(2),
|
|
3848
5096
|
]);
|
|
3849
5097
|
const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
|
|
3850
5098
|
const summaryTable = functoolsKit.str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
|
|
@@ -3853,7 +5101,7 @@ class PerformanceStorage {
|
|
|
3853
5101
|
const pct = (metric.totalDuration / stats.totalDuration) * 100;
|
|
3854
5102
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
3855
5103
|
});
|
|
3856
|
-
return functoolsKit.str.newline(`# Performance Report: ${strategyName}`, "", `**Total events:** ${stats.totalEvents}`, `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`, `**Number of metric types:** ${Object.keys(stats.metricStats).length}`, "", "## Time Distribution", "", functoolsKit.str.newline(percentages), "", "## Detailed Metrics", "", summaryTable, "", "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times.");
|
|
5104
|
+
return functoolsKit.str.newline(`# Performance Report: ${strategyName}`, "", `**Total events:** ${stats.totalEvents}`, `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`, `**Number of metric types:** ${Object.keys(stats.metricStats).length}`, "", "## Time Distribution", "", functoolsKit.str.newline(percentages), "", "## Detailed Metrics", "", summaryTable, "", "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type.");
|
|
3857
5105
|
}
|
|
3858
5106
|
/**
|
|
3859
5107
|
* Saves performance report to disk.
|
|
@@ -3888,101 +5136,896 @@ class PerformanceStorage {
|
|
|
3888
5136
|
*
|
|
3889
5137
|
* @example
|
|
3890
5138
|
* ```typescript
|
|
3891
|
-
* import { listenPerformance } from "backtest-kit";
|
|
3892
|
-
*
|
|
3893
|
-
* // Subscribe to performance events
|
|
3894
|
-
* listenPerformance((event) => {
|
|
3895
|
-
* console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
|
|
3896
|
-
* });
|
|
5139
|
+
* import { listenPerformance } from "backtest-kit";
|
|
5140
|
+
*
|
|
5141
|
+
* // Subscribe to performance events
|
|
5142
|
+
* listenPerformance((event) => {
|
|
5143
|
+
* console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
|
|
5144
|
+
* });
|
|
5145
|
+
*
|
|
5146
|
+
* // After execution, generate report
|
|
5147
|
+
* const stats = await Performance.getData("my-strategy");
|
|
5148
|
+
* console.log("Bottlenecks:", stats.metricStats);
|
|
5149
|
+
*
|
|
5150
|
+
* // Save report to disk
|
|
5151
|
+
* await Performance.dump("my-strategy");
|
|
5152
|
+
* ```
|
|
5153
|
+
*/
|
|
5154
|
+
class PerformanceMarkdownService {
|
|
5155
|
+
constructor() {
|
|
5156
|
+
/** Logger service for debug output */
|
|
5157
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
5158
|
+
/**
|
|
5159
|
+
* Memoized function to get or create PerformanceStorage for a strategy.
|
|
5160
|
+
* Each strategy gets its own isolated storage instance.
|
|
5161
|
+
*/
|
|
5162
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new PerformanceStorage());
|
|
5163
|
+
/**
|
|
5164
|
+
* Processes performance events and accumulates metrics.
|
|
5165
|
+
* Should be called from performance tracking code.
|
|
5166
|
+
*
|
|
5167
|
+
* @param event - Performance event with timing data
|
|
5168
|
+
*/
|
|
5169
|
+
this.track = async (event) => {
|
|
5170
|
+
this.loggerService.log("performanceMarkdownService track", {
|
|
5171
|
+
event,
|
|
5172
|
+
});
|
|
5173
|
+
const strategyName = event.strategyName || "global";
|
|
5174
|
+
const storage = this.getStorage(strategyName);
|
|
5175
|
+
storage.addEvent(event);
|
|
5176
|
+
};
|
|
5177
|
+
/**
|
|
5178
|
+
* Gets aggregated performance statistics for a strategy.
|
|
5179
|
+
*
|
|
5180
|
+
* @param strategyName - Strategy name to get data for
|
|
5181
|
+
* @returns Performance statistics with aggregated metrics
|
|
5182
|
+
*
|
|
5183
|
+
* @example
|
|
5184
|
+
* ```typescript
|
|
5185
|
+
* const stats = await performanceService.getData("my-strategy");
|
|
5186
|
+
* console.log("Total time:", stats.totalDuration);
|
|
5187
|
+
* console.log("Slowest operation:", Object.values(stats.metricStats)
|
|
5188
|
+
* .sort((a, b) => b.avgDuration - a.avgDuration)[0]);
|
|
5189
|
+
* ```
|
|
5190
|
+
*/
|
|
5191
|
+
this.getData = async (strategyName) => {
|
|
5192
|
+
this.loggerService.log("performanceMarkdownService getData", {
|
|
5193
|
+
strategyName,
|
|
5194
|
+
});
|
|
5195
|
+
const storage = this.getStorage(strategyName);
|
|
5196
|
+
return storage.getData(strategyName);
|
|
5197
|
+
};
|
|
5198
|
+
/**
|
|
5199
|
+
* Generates markdown report with performance analysis.
|
|
5200
|
+
*
|
|
5201
|
+
* @param strategyName - Strategy name to generate report for
|
|
5202
|
+
* @returns Markdown formatted report string
|
|
5203
|
+
*
|
|
5204
|
+
* @example
|
|
5205
|
+
* ```typescript
|
|
5206
|
+
* const markdown = await performanceService.getReport("my-strategy");
|
|
5207
|
+
* console.log(markdown);
|
|
5208
|
+
* ```
|
|
5209
|
+
*/
|
|
5210
|
+
this.getReport = async (strategyName) => {
|
|
5211
|
+
this.loggerService.log("performanceMarkdownService getReport", {
|
|
5212
|
+
strategyName,
|
|
5213
|
+
});
|
|
5214
|
+
const storage = this.getStorage(strategyName);
|
|
5215
|
+
return storage.getReport(strategyName);
|
|
5216
|
+
};
|
|
5217
|
+
/**
|
|
5218
|
+
* Saves performance report to disk.
|
|
5219
|
+
*
|
|
5220
|
+
* @param strategyName - Strategy name to save report for
|
|
5221
|
+
* @param path - Directory path to save report
|
|
5222
|
+
*
|
|
5223
|
+
* @example
|
|
5224
|
+
* ```typescript
|
|
5225
|
+
* // Save to default path: ./logs/performance/my-strategy.md
|
|
5226
|
+
* await performanceService.dump("my-strategy");
|
|
5227
|
+
*
|
|
5228
|
+
* // Save to custom path
|
|
5229
|
+
* await performanceService.dump("my-strategy", "./custom/path");
|
|
5230
|
+
* ```
|
|
5231
|
+
*/
|
|
5232
|
+
this.dump = async (strategyName, path = "./logs/performance") => {
|
|
5233
|
+
this.loggerService.log("performanceMarkdownService dump", {
|
|
5234
|
+
strategyName,
|
|
5235
|
+
path,
|
|
5236
|
+
});
|
|
5237
|
+
const storage = this.getStorage(strategyName);
|
|
5238
|
+
await storage.dump(strategyName, path);
|
|
5239
|
+
};
|
|
5240
|
+
/**
|
|
5241
|
+
* Clears accumulated performance data from storage.
|
|
5242
|
+
*
|
|
5243
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
5244
|
+
*/
|
|
5245
|
+
this.clear = async (strategyName) => {
|
|
5246
|
+
this.loggerService.log("performanceMarkdownService clear", {
|
|
5247
|
+
strategyName,
|
|
5248
|
+
});
|
|
5249
|
+
this.getStorage.clear(strategyName);
|
|
5250
|
+
};
|
|
5251
|
+
/**
|
|
5252
|
+
* Initializes the service by subscribing to performance events.
|
|
5253
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
5254
|
+
*/
|
|
5255
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
5256
|
+
this.loggerService.log("performanceMarkdownService init");
|
|
5257
|
+
performanceEmitter.subscribe(this.track);
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
/**
|
|
5263
|
+
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
5264
|
+
*/
|
|
5265
|
+
function isUnsafe$1(value) {
|
|
5266
|
+
if (value === null) {
|
|
5267
|
+
return true;
|
|
5268
|
+
}
|
|
5269
|
+
if (typeof value !== "number") {
|
|
5270
|
+
return true;
|
|
5271
|
+
}
|
|
5272
|
+
if (isNaN(value)) {
|
|
5273
|
+
return true;
|
|
5274
|
+
}
|
|
5275
|
+
if (!isFinite(value)) {
|
|
5276
|
+
return true;
|
|
5277
|
+
}
|
|
5278
|
+
return false;
|
|
5279
|
+
}
|
|
5280
|
+
/**
|
|
5281
|
+
* Formats a metric value for display.
|
|
5282
|
+
* Returns "N/A" for unsafe values, otherwise formats with 2 decimal places.
|
|
5283
|
+
*/
|
|
5284
|
+
function formatMetric(value) {
|
|
5285
|
+
if (isUnsafe$1(value)) {
|
|
5286
|
+
return "N/A";
|
|
5287
|
+
}
|
|
5288
|
+
return value.toFixed(2);
|
|
5289
|
+
}
|
|
5290
|
+
/**
|
|
5291
|
+
* Storage class for accumulating walker results.
|
|
5292
|
+
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
5293
|
+
*/
|
|
5294
|
+
class ReportStorage {
|
|
5295
|
+
constructor(walkerName) {
|
|
5296
|
+
this.walkerName = walkerName;
|
|
5297
|
+
/** Walker metadata (set from first addResult call) */
|
|
5298
|
+
this._totalStrategies = null;
|
|
5299
|
+
this._bestStats = null;
|
|
5300
|
+
this._bestMetric = null;
|
|
5301
|
+
this._bestStrategy = null;
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Adds a strategy result to the storage.
|
|
5305
|
+
*
|
|
5306
|
+
* @param data - Walker contract with strategy result
|
|
5307
|
+
*/
|
|
5308
|
+
addResult(data) {
|
|
5309
|
+
{
|
|
5310
|
+
this._bestMetric = data.bestMetric;
|
|
5311
|
+
this._bestStrategy = data.bestStrategy;
|
|
5312
|
+
this._totalStrategies = data.totalStrategies;
|
|
5313
|
+
}
|
|
5314
|
+
// Update best stats only if this strategy is the current best
|
|
5315
|
+
if (data.strategyName === data.bestStrategy) {
|
|
5316
|
+
this._bestStats = data.stats;
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
/**
|
|
5320
|
+
* Calculates walker results from strategy results.
|
|
5321
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5322
|
+
*
|
|
5323
|
+
* @param symbol - Trading symbol
|
|
5324
|
+
* @param metric - Metric being optimized
|
|
5325
|
+
* @param context - Context with exchangeName and frameName
|
|
5326
|
+
* @returns Walker results data
|
|
5327
|
+
*/
|
|
5328
|
+
async getData(symbol, metric, context) {
|
|
5329
|
+
if (this._totalStrategies === null) {
|
|
5330
|
+
throw new Error("No walker data available - no results added yet");
|
|
5331
|
+
}
|
|
5332
|
+
return {
|
|
5333
|
+
walkerName: this.walkerName,
|
|
5334
|
+
symbol,
|
|
5335
|
+
exchangeName: context.exchangeName,
|
|
5336
|
+
frameName: context.frameName,
|
|
5337
|
+
metric,
|
|
5338
|
+
totalStrategies: this._totalStrategies,
|
|
5339
|
+
bestStrategy: this._bestStrategy,
|
|
5340
|
+
bestMetric: this._bestMetric,
|
|
5341
|
+
bestStats: this._bestStats,
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Generates markdown report with all strategy results (View).
|
|
5346
|
+
*
|
|
5347
|
+
* @param symbol - Trading symbol
|
|
5348
|
+
* @param metric - Metric being optimized
|
|
5349
|
+
* @param context - Context with exchangeName and frameName
|
|
5350
|
+
* @returns Markdown formatted report with all results
|
|
5351
|
+
*/
|
|
5352
|
+
async getReport(symbol, metric, context) {
|
|
5353
|
+
const results = await this.getData(symbol, metric, context);
|
|
5354
|
+
return functoolsKit.str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
|
|
5355
|
+
}
|
|
5356
|
+
/**
|
|
5357
|
+
* Saves walker report to disk.
|
|
5358
|
+
*
|
|
5359
|
+
* @param symbol - Trading symbol
|
|
5360
|
+
* @param metric - Metric being optimized
|
|
5361
|
+
* @param context - Context with exchangeName and frameName
|
|
5362
|
+
* @param path - Directory path to save report (default: "./logs/walker")
|
|
5363
|
+
*/
|
|
5364
|
+
async dump(symbol, metric, context, path$1 = "./logs/walker") {
|
|
5365
|
+
const markdown = await this.getReport(symbol, metric, context);
|
|
5366
|
+
try {
|
|
5367
|
+
const dir = path.join(process.cwd(), path$1);
|
|
5368
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5369
|
+
const filename = `${this.walkerName}.md`;
|
|
5370
|
+
const filepath = path.join(dir, filename);
|
|
5371
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
5372
|
+
console.log(`Walker report saved: ${filepath}`);
|
|
5373
|
+
}
|
|
5374
|
+
catch (error) {
|
|
5375
|
+
console.error(`Failed to save walker report:`, error);
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
/**
|
|
5380
|
+
* Service for generating and saving walker markdown reports.
|
|
5381
|
+
*
|
|
5382
|
+
* Features:
|
|
5383
|
+
* - Listens to walker events via tick callback
|
|
5384
|
+
* - Accumulates strategy results per walker using memoized storage
|
|
5385
|
+
* - Generates markdown tables with detailed strategy comparison
|
|
5386
|
+
* - Saves reports to disk in logs/walker/{walkerName}.md
|
|
5387
|
+
*
|
|
5388
|
+
* @example
|
|
5389
|
+
* ```typescript
|
|
5390
|
+
* const service = new WalkerMarkdownService();
|
|
5391
|
+
* const results = await service.getData("my-walker");
|
|
5392
|
+
* await service.dump("my-walker");
|
|
5393
|
+
* ```
|
|
5394
|
+
*/
|
|
5395
|
+
class WalkerMarkdownService {
|
|
5396
|
+
constructor() {
|
|
5397
|
+
/** Logger service for debug output */
|
|
5398
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
5399
|
+
/**
|
|
5400
|
+
* Memoized function to get or create ReportStorage for a walker.
|
|
5401
|
+
* Each walker gets its own isolated storage instance.
|
|
5402
|
+
*/
|
|
5403
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage(walkerName));
|
|
5404
|
+
/**
|
|
5405
|
+
* Processes walker progress events and accumulates strategy results.
|
|
5406
|
+
* Should be called from walkerEmitter.
|
|
5407
|
+
*
|
|
5408
|
+
* @param data - Walker contract from walker execution
|
|
5409
|
+
*
|
|
5410
|
+
* @example
|
|
5411
|
+
* ```typescript
|
|
5412
|
+
* const service = new WalkerMarkdownService();
|
|
5413
|
+
* walkerEmitter.subscribe((data) => service.tick(data));
|
|
5414
|
+
* ```
|
|
5415
|
+
*/
|
|
5416
|
+
this.tick = async (data) => {
|
|
5417
|
+
this.loggerService.log("walkerMarkdownService tick", {
|
|
5418
|
+
data,
|
|
5419
|
+
});
|
|
5420
|
+
const storage = this.getStorage(data.walkerName);
|
|
5421
|
+
storage.addResult(data);
|
|
5422
|
+
};
|
|
5423
|
+
/**
|
|
5424
|
+
* Gets walker results data from all strategy results.
|
|
5425
|
+
* Delegates to ReportStorage.getData().
|
|
5426
|
+
*
|
|
5427
|
+
* @param walkerName - Walker name to get data for
|
|
5428
|
+
* @param symbol - Trading symbol
|
|
5429
|
+
* @param metric - Metric being optimized
|
|
5430
|
+
* @param context - Context with exchangeName and frameName
|
|
5431
|
+
* @returns Walker results data object with all metrics
|
|
5432
|
+
*
|
|
5433
|
+
* @example
|
|
5434
|
+
* ```typescript
|
|
5435
|
+
* const service = new WalkerMarkdownService();
|
|
5436
|
+
* const results = await service.getData("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
|
|
5437
|
+
* console.log(results.bestStrategy, results.bestMetric);
|
|
5438
|
+
* ```
|
|
5439
|
+
*/
|
|
5440
|
+
this.getData = async (walkerName, symbol, metric, context) => {
|
|
5441
|
+
this.loggerService.log("walkerMarkdownService getData", {
|
|
5442
|
+
walkerName,
|
|
5443
|
+
symbol,
|
|
5444
|
+
metric,
|
|
5445
|
+
context,
|
|
5446
|
+
});
|
|
5447
|
+
const storage = this.getStorage(walkerName);
|
|
5448
|
+
return storage.getData(symbol, metric, context);
|
|
5449
|
+
};
|
|
5450
|
+
/**
|
|
5451
|
+
* Generates markdown report with all strategy results for a walker.
|
|
5452
|
+
* Delegates to ReportStorage.getReport().
|
|
5453
|
+
*
|
|
5454
|
+
* @param walkerName - Walker name to generate report for
|
|
5455
|
+
* @param symbol - Trading symbol
|
|
5456
|
+
* @param metric - Metric being optimized
|
|
5457
|
+
* @param context - Context with exchangeName and frameName
|
|
5458
|
+
* @returns Markdown formatted report string
|
|
5459
|
+
*
|
|
5460
|
+
* @example
|
|
5461
|
+
* ```typescript
|
|
5462
|
+
* const service = new WalkerMarkdownService();
|
|
5463
|
+
* const markdown = await service.getReport("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
|
|
5464
|
+
* console.log(markdown);
|
|
5465
|
+
* ```
|
|
5466
|
+
*/
|
|
5467
|
+
this.getReport = async (walkerName, symbol, metric, context) => {
|
|
5468
|
+
this.loggerService.log("walkerMarkdownService getReport", {
|
|
5469
|
+
walkerName,
|
|
5470
|
+
symbol,
|
|
5471
|
+
metric,
|
|
5472
|
+
context,
|
|
5473
|
+
});
|
|
5474
|
+
const storage = this.getStorage(walkerName);
|
|
5475
|
+
return storage.getReport(symbol, metric, context);
|
|
5476
|
+
};
|
|
5477
|
+
/**
|
|
5478
|
+
* Saves walker report to disk.
|
|
5479
|
+
* Creates directory if it doesn't exist.
|
|
5480
|
+
* Delegates to ReportStorage.dump().
|
|
5481
|
+
*
|
|
5482
|
+
* @param walkerName - Walker name to save report for
|
|
5483
|
+
* @param symbol - Trading symbol
|
|
5484
|
+
* @param metric - Metric being optimized
|
|
5485
|
+
* @param context - Context with exchangeName and frameName
|
|
5486
|
+
* @param path - Directory path to save report (default: "./logs/walker")
|
|
5487
|
+
*
|
|
5488
|
+
* @example
|
|
5489
|
+
* ```typescript
|
|
5490
|
+
* const service = new WalkerMarkdownService();
|
|
5491
|
+
*
|
|
5492
|
+
* // Save to default path: ./logs/walker/my-walker.md
|
|
5493
|
+
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
|
|
5494
|
+
*
|
|
5495
|
+
* // Save to custom path: ./custom/path/my-walker.md
|
|
5496
|
+
* await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" }, "./custom/path");
|
|
5497
|
+
* ```
|
|
5498
|
+
*/
|
|
5499
|
+
this.dump = async (walkerName, symbol, metric, context, path = "./logs/walker") => {
|
|
5500
|
+
this.loggerService.log("walkerMarkdownService dump", {
|
|
5501
|
+
walkerName,
|
|
5502
|
+
symbol,
|
|
5503
|
+
metric,
|
|
5504
|
+
context,
|
|
5505
|
+
path,
|
|
5506
|
+
});
|
|
5507
|
+
const storage = this.getStorage(walkerName);
|
|
5508
|
+
await storage.dump(symbol, metric, context, path);
|
|
5509
|
+
};
|
|
5510
|
+
/**
|
|
5511
|
+
* Clears accumulated result data from storage.
|
|
5512
|
+
* If walkerName is provided, clears only that walker's data.
|
|
5513
|
+
* If walkerName is omitted, clears all walkers' data.
|
|
5514
|
+
*
|
|
5515
|
+
* @param walkerName - Optional walker name to clear specific walker data
|
|
5516
|
+
*
|
|
5517
|
+
* @example
|
|
5518
|
+
* ```typescript
|
|
5519
|
+
* const service = new WalkerMarkdownService();
|
|
5520
|
+
*
|
|
5521
|
+
* // Clear specific walker data
|
|
5522
|
+
* await service.clear("my-walker");
|
|
5523
|
+
*
|
|
5524
|
+
* // Clear all walkers' data
|
|
5525
|
+
* await service.clear();
|
|
5526
|
+
* ```
|
|
5527
|
+
*/
|
|
5528
|
+
this.clear = async (walkerName) => {
|
|
5529
|
+
this.loggerService.log("walkerMarkdownService clear", {
|
|
5530
|
+
walkerName,
|
|
5531
|
+
});
|
|
5532
|
+
this.getStorage.clear(walkerName);
|
|
5533
|
+
};
|
|
5534
|
+
/**
|
|
5535
|
+
* Initializes the service by subscribing to walker events.
|
|
5536
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
5537
|
+
* Automatically called on first use.
|
|
5538
|
+
*
|
|
5539
|
+
* @example
|
|
5540
|
+
* ```typescript
|
|
5541
|
+
* const service = new WalkerMarkdownService();
|
|
5542
|
+
* await service.init(); // Subscribe to walker events
|
|
5543
|
+
* ```
|
|
5544
|
+
*/
|
|
5545
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
5546
|
+
this.loggerService.log("walkerMarkdownService init");
|
|
5547
|
+
walkerEmitter.subscribe(this.tick);
|
|
5548
|
+
});
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
const HEATMAP_METHOD_NAME_GET_DATA = "HeatMarkdownService.getData";
|
|
5553
|
+
const HEATMAP_METHOD_NAME_GET_REPORT = "HeatMarkdownService.getReport";
|
|
5554
|
+
const HEATMAP_METHOD_NAME_DUMP = "HeatMarkdownService.dump";
|
|
5555
|
+
const HEATMAP_METHOD_NAME_CLEAR = "HeatMarkdownService.clear";
|
|
5556
|
+
/**
|
|
5557
|
+
* Checks if a value is unsafe for display (not a number, NaN, or Infinity).
|
|
5558
|
+
*
|
|
5559
|
+
* @param value - Value to check
|
|
5560
|
+
* @returns true if value is unsafe, false otherwise
|
|
5561
|
+
*/
|
|
5562
|
+
function isUnsafe(value) {
|
|
5563
|
+
if (typeof value !== "number") {
|
|
5564
|
+
return true;
|
|
5565
|
+
}
|
|
5566
|
+
if (isNaN(value)) {
|
|
5567
|
+
return true;
|
|
5568
|
+
}
|
|
5569
|
+
if (!isFinite(value)) {
|
|
5570
|
+
return true;
|
|
5571
|
+
}
|
|
5572
|
+
return false;
|
|
5573
|
+
}
|
|
5574
|
+
const columns = [
|
|
5575
|
+
{
|
|
5576
|
+
key: "symbol",
|
|
5577
|
+
label: "Symbol",
|
|
5578
|
+
format: (data) => data.symbol,
|
|
5579
|
+
},
|
|
5580
|
+
{
|
|
5581
|
+
key: "totalPnl",
|
|
5582
|
+
label: "Total PNL",
|
|
5583
|
+
format: (data) => data.totalPnl !== null ? functoolsKit.str(data.totalPnl, "%+.2f%%") : "N/A",
|
|
5584
|
+
},
|
|
5585
|
+
{
|
|
5586
|
+
key: "sharpeRatio",
|
|
5587
|
+
label: "Sharpe",
|
|
5588
|
+
format: (data) => data.sharpeRatio !== null ? functoolsKit.str(data.sharpeRatio, "%.2f") : "N/A",
|
|
5589
|
+
},
|
|
5590
|
+
{
|
|
5591
|
+
key: "profitFactor",
|
|
5592
|
+
label: "PF",
|
|
5593
|
+
format: (data) => data.profitFactor !== null ? functoolsKit.str(data.profitFactor, "%.2f") : "N/A",
|
|
5594
|
+
},
|
|
5595
|
+
{
|
|
5596
|
+
key: "expectancy",
|
|
5597
|
+
label: "Expect",
|
|
5598
|
+
format: (data) => data.expectancy !== null ? functoolsKit.str(data.expectancy, "%+.2f%%") : "N/A",
|
|
5599
|
+
},
|
|
5600
|
+
{
|
|
5601
|
+
key: "winRate",
|
|
5602
|
+
label: "WR",
|
|
5603
|
+
format: (data) => data.winRate !== null ? functoolsKit.str(data.winRate, "%.1f%%") : "N/A",
|
|
5604
|
+
},
|
|
5605
|
+
{
|
|
5606
|
+
key: "avgWin",
|
|
5607
|
+
label: "Avg Win",
|
|
5608
|
+
format: (data) => data.avgWin !== null ? functoolsKit.str(data.avgWin, "%+.2f%%") : "N/A",
|
|
5609
|
+
},
|
|
5610
|
+
{
|
|
5611
|
+
key: "avgLoss",
|
|
5612
|
+
label: "Avg Loss",
|
|
5613
|
+
format: (data) => data.avgLoss !== null ? functoolsKit.str(data.avgLoss, "%+.2f%%") : "N/A",
|
|
5614
|
+
},
|
|
5615
|
+
{
|
|
5616
|
+
key: "maxDrawdown",
|
|
5617
|
+
label: "Max DD",
|
|
5618
|
+
format: (data) => data.maxDrawdown !== null ? functoolsKit.str(-data.maxDrawdown, "%.2f%%") : "N/A",
|
|
5619
|
+
},
|
|
5620
|
+
{
|
|
5621
|
+
key: "maxWinStreak",
|
|
5622
|
+
label: "W Streak",
|
|
5623
|
+
format: (data) => data.maxWinStreak.toString(),
|
|
5624
|
+
},
|
|
5625
|
+
{
|
|
5626
|
+
key: "maxLossStreak",
|
|
5627
|
+
label: "L Streak",
|
|
5628
|
+
format: (data) => data.maxLossStreak.toString(),
|
|
5629
|
+
},
|
|
5630
|
+
{
|
|
5631
|
+
key: "totalTrades",
|
|
5632
|
+
label: "Trades",
|
|
5633
|
+
format: (data) => data.totalTrades.toString(),
|
|
5634
|
+
},
|
|
5635
|
+
];
|
|
5636
|
+
/**
|
|
5637
|
+
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
5638
|
+
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
5639
|
+
*/
|
|
5640
|
+
class HeatmapStorage {
|
|
5641
|
+
constructor() {
|
|
5642
|
+
/** Internal storage of closed signals per symbol */
|
|
5643
|
+
this.symbolData = new Map();
|
|
5644
|
+
}
|
|
5645
|
+
/**
|
|
5646
|
+
* Adds a closed signal to the storage.
|
|
5647
|
+
*
|
|
5648
|
+
* @param data - Closed signal data with PNL and symbol
|
|
5649
|
+
*/
|
|
5650
|
+
addSignal(data) {
|
|
5651
|
+
const { symbol } = data;
|
|
5652
|
+
if (!this.symbolData.has(symbol)) {
|
|
5653
|
+
this.symbolData.set(symbol, []);
|
|
5654
|
+
}
|
|
5655
|
+
this.symbolData.get(symbol).push(data);
|
|
5656
|
+
}
|
|
5657
|
+
/**
|
|
5658
|
+
* Calculates statistics for a single symbol.
|
|
5659
|
+
*
|
|
5660
|
+
* @param symbol - Trading pair symbol
|
|
5661
|
+
* @param signals - Array of closed signals for this symbol
|
|
5662
|
+
* @returns Heatmap row with aggregated statistics
|
|
5663
|
+
*/
|
|
5664
|
+
calculateSymbolStats(symbol, signals) {
|
|
5665
|
+
const totalTrades = signals.length;
|
|
5666
|
+
const winCount = signals.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
5667
|
+
const lossCount = signals.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
5668
|
+
// Calculate win rate
|
|
5669
|
+
let winRate = null;
|
|
5670
|
+
if (totalTrades > 0) {
|
|
5671
|
+
winRate = (winCount / totalTrades) * 100;
|
|
5672
|
+
}
|
|
5673
|
+
// Calculate total PNL
|
|
5674
|
+
let totalPnl = null;
|
|
5675
|
+
if (signals.length > 0) {
|
|
5676
|
+
totalPnl = signals.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0);
|
|
5677
|
+
}
|
|
5678
|
+
// Calculate average PNL
|
|
5679
|
+
let avgPnl = null;
|
|
5680
|
+
if (signals.length > 0) {
|
|
5681
|
+
avgPnl = totalPnl / signals.length;
|
|
5682
|
+
}
|
|
5683
|
+
// Calculate standard deviation
|
|
5684
|
+
let stdDev = null;
|
|
5685
|
+
if (signals.length > 1 && avgPnl !== null) {
|
|
5686
|
+
const variance = signals.reduce((acc, s) => acc + Math.pow(s.pnl.pnlPercentage - avgPnl, 2), 0) / signals.length;
|
|
5687
|
+
stdDev = Math.sqrt(variance);
|
|
5688
|
+
}
|
|
5689
|
+
// Calculate Sharpe Ratio
|
|
5690
|
+
let sharpeRatio = null;
|
|
5691
|
+
if (avgPnl !== null && stdDev !== null && stdDev !== 0) {
|
|
5692
|
+
sharpeRatio = avgPnl / stdDev;
|
|
5693
|
+
}
|
|
5694
|
+
// Calculate Maximum Drawdown
|
|
5695
|
+
let maxDrawdown = null;
|
|
5696
|
+
if (signals.length > 0) {
|
|
5697
|
+
let peak = 0;
|
|
5698
|
+
let currentDrawdown = 0;
|
|
5699
|
+
let maxDD = 0;
|
|
5700
|
+
for (const signal of signals) {
|
|
5701
|
+
peak += signal.pnl.pnlPercentage;
|
|
5702
|
+
if (peak > 0) {
|
|
5703
|
+
currentDrawdown = 0;
|
|
5704
|
+
}
|
|
5705
|
+
else {
|
|
5706
|
+
currentDrawdown = Math.abs(peak);
|
|
5707
|
+
if (currentDrawdown > maxDD) {
|
|
5708
|
+
maxDD = currentDrawdown;
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
maxDrawdown = maxDD;
|
|
5713
|
+
}
|
|
5714
|
+
// Calculate Profit Factor
|
|
5715
|
+
let profitFactor = null;
|
|
5716
|
+
if (winCount > 0 && lossCount > 0) {
|
|
5717
|
+
const sumWins = signals
|
|
5718
|
+
.filter((s) => s.pnl.pnlPercentage > 0)
|
|
5719
|
+
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0);
|
|
5720
|
+
const sumLosses = Math.abs(signals
|
|
5721
|
+
.filter((s) => s.pnl.pnlPercentage < 0)
|
|
5722
|
+
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0));
|
|
5723
|
+
if (sumLosses > 0) {
|
|
5724
|
+
profitFactor = sumWins / sumLosses;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
// Calculate Average Win / Average Loss
|
|
5728
|
+
let avgWin = null;
|
|
5729
|
+
let avgLoss = null;
|
|
5730
|
+
if (winCount > 0) {
|
|
5731
|
+
avgWin =
|
|
5732
|
+
signals
|
|
5733
|
+
.filter((s) => s.pnl.pnlPercentage > 0)
|
|
5734
|
+
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0) / winCount;
|
|
5735
|
+
}
|
|
5736
|
+
if (lossCount > 0) {
|
|
5737
|
+
avgLoss =
|
|
5738
|
+
signals
|
|
5739
|
+
.filter((s) => s.pnl.pnlPercentage < 0)
|
|
5740
|
+
.reduce((acc, s) => acc + s.pnl.pnlPercentage, 0) / lossCount;
|
|
5741
|
+
}
|
|
5742
|
+
// Calculate Win/Loss Streaks
|
|
5743
|
+
let maxWinStreak = 0;
|
|
5744
|
+
let maxLossStreak = 0;
|
|
5745
|
+
let currentWinStreak = 0;
|
|
5746
|
+
let currentLossStreak = 0;
|
|
5747
|
+
for (const signal of signals) {
|
|
5748
|
+
if (signal.pnl.pnlPercentage > 0) {
|
|
5749
|
+
currentWinStreak++;
|
|
5750
|
+
currentLossStreak = 0;
|
|
5751
|
+
if (currentWinStreak > maxWinStreak) {
|
|
5752
|
+
maxWinStreak = currentWinStreak;
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
else if (signal.pnl.pnlPercentage < 0) {
|
|
5756
|
+
currentLossStreak++;
|
|
5757
|
+
currentWinStreak = 0;
|
|
5758
|
+
if (currentLossStreak > maxLossStreak) {
|
|
5759
|
+
maxLossStreak = currentLossStreak;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
// Calculate Expectancy
|
|
5764
|
+
let expectancy = null;
|
|
5765
|
+
if (winRate !== null && avgWin !== null && avgLoss !== null) {
|
|
5766
|
+
const lossRate = 100 - winRate;
|
|
5767
|
+
expectancy = (winRate / 100) * avgWin + (lossRate / 100) * avgLoss;
|
|
5768
|
+
}
|
|
5769
|
+
// Apply safe math checks
|
|
5770
|
+
if (isUnsafe(winRate))
|
|
5771
|
+
winRate = null;
|
|
5772
|
+
if (isUnsafe(totalPnl))
|
|
5773
|
+
totalPnl = null;
|
|
5774
|
+
if (isUnsafe(avgPnl))
|
|
5775
|
+
avgPnl = null;
|
|
5776
|
+
if (isUnsafe(stdDev))
|
|
5777
|
+
stdDev = null;
|
|
5778
|
+
if (isUnsafe(sharpeRatio))
|
|
5779
|
+
sharpeRatio = null;
|
|
5780
|
+
if (isUnsafe(maxDrawdown))
|
|
5781
|
+
maxDrawdown = null;
|
|
5782
|
+
if (isUnsafe(profitFactor))
|
|
5783
|
+
profitFactor = null;
|
|
5784
|
+
if (isUnsafe(avgWin))
|
|
5785
|
+
avgWin = null;
|
|
5786
|
+
if (isUnsafe(avgLoss))
|
|
5787
|
+
avgLoss = null;
|
|
5788
|
+
if (isUnsafe(expectancy))
|
|
5789
|
+
expectancy = null;
|
|
5790
|
+
return {
|
|
5791
|
+
symbol,
|
|
5792
|
+
totalPnl,
|
|
5793
|
+
sharpeRatio,
|
|
5794
|
+
maxDrawdown,
|
|
5795
|
+
totalTrades,
|
|
5796
|
+
winCount,
|
|
5797
|
+
lossCount,
|
|
5798
|
+
winRate,
|
|
5799
|
+
avgPnl,
|
|
5800
|
+
stdDev,
|
|
5801
|
+
profitFactor,
|
|
5802
|
+
avgWin,
|
|
5803
|
+
avgLoss,
|
|
5804
|
+
maxWinStreak,
|
|
5805
|
+
maxLossStreak,
|
|
5806
|
+
expectancy,
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
/**
|
|
5810
|
+
* Gets aggregated portfolio heatmap statistics (Controller).
|
|
5811
|
+
*
|
|
5812
|
+
* @returns Promise resolving to heatmap statistics with per-symbol and portfolio-wide metrics
|
|
5813
|
+
*/
|
|
5814
|
+
async getData() {
|
|
5815
|
+
const symbols = [];
|
|
5816
|
+
// Calculate per-symbol statistics
|
|
5817
|
+
for (const [symbol, signals] of this.symbolData.entries()) {
|
|
5818
|
+
const row = this.calculateSymbolStats(symbol, signals);
|
|
5819
|
+
symbols.push(row);
|
|
5820
|
+
}
|
|
5821
|
+
// Sort by Sharpe Ratio descending (best performers first, nulls last)
|
|
5822
|
+
symbols.sort((a, b) => {
|
|
5823
|
+
if (a.sharpeRatio === null && b.sharpeRatio === null)
|
|
5824
|
+
return 0;
|
|
5825
|
+
if (a.sharpeRatio === null)
|
|
5826
|
+
return 1;
|
|
5827
|
+
if (b.sharpeRatio === null)
|
|
5828
|
+
return -1;
|
|
5829
|
+
return b.sharpeRatio - a.sharpeRatio;
|
|
5830
|
+
});
|
|
5831
|
+
// Calculate portfolio-wide metrics
|
|
5832
|
+
const totalSymbols = symbols.length;
|
|
5833
|
+
let portfolioTotalPnl = null;
|
|
5834
|
+
let portfolioTotalTrades = 0;
|
|
5835
|
+
if (symbols.length > 0) {
|
|
5836
|
+
portfolioTotalPnl = symbols.reduce((acc, s) => acc + (s.totalPnl || 0), 0);
|
|
5837
|
+
portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
5838
|
+
}
|
|
5839
|
+
// Calculate portfolio Sharpe Ratio (weighted by number of trades)
|
|
5840
|
+
let portfolioSharpeRatio = null;
|
|
5841
|
+
const validSharpes = symbols.filter((s) => s.sharpeRatio !== null);
|
|
5842
|
+
if (validSharpes.length > 0 && portfolioTotalTrades > 0) {
|
|
5843
|
+
const weightedSum = validSharpes.reduce((acc, s) => acc + s.sharpeRatio * s.totalTrades, 0);
|
|
5844
|
+
portfolioSharpeRatio = weightedSum / portfolioTotalTrades;
|
|
5845
|
+
}
|
|
5846
|
+
// Apply safe math
|
|
5847
|
+
if (isUnsafe(portfolioTotalPnl))
|
|
5848
|
+
portfolioTotalPnl = null;
|
|
5849
|
+
if (isUnsafe(portfolioSharpeRatio))
|
|
5850
|
+
portfolioSharpeRatio = null;
|
|
5851
|
+
return {
|
|
5852
|
+
symbols,
|
|
5853
|
+
totalSymbols,
|
|
5854
|
+
portfolioTotalPnl,
|
|
5855
|
+
portfolioSharpeRatio,
|
|
5856
|
+
portfolioTotalTrades,
|
|
5857
|
+
};
|
|
5858
|
+
}
|
|
5859
|
+
/**
|
|
5860
|
+
* Generates markdown report with portfolio heatmap table (View).
|
|
5861
|
+
*
|
|
5862
|
+
* @param strategyName - Strategy name for report title
|
|
5863
|
+
* @returns Promise resolving to markdown formatted report string
|
|
5864
|
+
*/
|
|
5865
|
+
async getReport(strategyName) {
|
|
5866
|
+
const data = await this.getData();
|
|
5867
|
+
if (data.symbols.length === 0) {
|
|
5868
|
+
return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
|
|
5869
|
+
}
|
|
5870
|
+
const header = columns.map((col) => col.label);
|
|
5871
|
+
const separator = columns.map(() => "---");
|
|
5872
|
+
const rows = data.symbols.map((row) => columns.map((col) => col.format(row)));
|
|
5873
|
+
const tableData = [header, separator, ...rows];
|
|
5874
|
+
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
5875
|
+
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);
|
|
5876
|
+
}
|
|
5877
|
+
/**
|
|
5878
|
+
* Saves heatmap report to disk.
|
|
5879
|
+
*
|
|
5880
|
+
* @param strategyName - Strategy name for filename
|
|
5881
|
+
* @param path - Directory path to save report (default: "./logs/heatmap")
|
|
5882
|
+
*/
|
|
5883
|
+
async dump(strategyName, path$1 = "./logs/heatmap") {
|
|
5884
|
+
const markdown = await this.getReport(strategyName);
|
|
5885
|
+
try {
|
|
5886
|
+
const dir = path.join(process.cwd(), path$1);
|
|
5887
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5888
|
+
const filename = `${strategyName}.md`;
|
|
5889
|
+
const filepath = path.join(dir, filename);
|
|
5890
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
5891
|
+
console.log(`Heatmap report saved: ${filepath}`);
|
|
5892
|
+
}
|
|
5893
|
+
catch (error) {
|
|
5894
|
+
console.error(`Failed to save heatmap report:`, error);
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
/**
|
|
5899
|
+
* Portfolio Heatmap Markdown Service.
|
|
5900
|
+
*
|
|
5901
|
+
* Subscribes to signalEmitter and aggregates statistics across all symbols per strategy.
|
|
5902
|
+
* Provides portfolio-wide metrics and per-symbol breakdowns.
|
|
5903
|
+
*
|
|
5904
|
+
* Features:
|
|
5905
|
+
* - Real-time aggregation of closed signals
|
|
5906
|
+
* - Per-symbol statistics (Total PNL, Sharpe Ratio, Max Drawdown, Trades)
|
|
5907
|
+
* - Portfolio-wide aggregated metrics per strategy
|
|
5908
|
+
* - Markdown table report generation
|
|
5909
|
+
* - Safe math (handles NaN/Infinity gracefully)
|
|
5910
|
+
* - Strategy-based navigation using memoized storage
|
|
5911
|
+
*
|
|
5912
|
+
* @example
|
|
5913
|
+
* ```typescript
|
|
5914
|
+
* const service = new HeatMarkdownService();
|
|
3897
5915
|
*
|
|
3898
|
-
* //
|
|
3899
|
-
* const stats = await
|
|
3900
|
-
* console.log(
|
|
5916
|
+
* // Service automatically tracks all closed signals per strategy
|
|
5917
|
+
* const stats = await service.getData("my-strategy");
|
|
5918
|
+
* console.log(`Portfolio Total PNL: ${stats.portfolioTotalPnl}%`);
|
|
3901
5919
|
*
|
|
3902
|
-
* //
|
|
3903
|
-
* await
|
|
5920
|
+
* // Generate and save report
|
|
5921
|
+
* await service.dump("my-strategy", "./reports");
|
|
3904
5922
|
* ```
|
|
3905
5923
|
*/
|
|
3906
|
-
class
|
|
5924
|
+
class HeatMarkdownService {
|
|
3907
5925
|
constructor() {
|
|
3908
5926
|
/** Logger service for debug output */
|
|
3909
5927
|
this.loggerService = inject(TYPES.loggerService);
|
|
3910
5928
|
/**
|
|
3911
|
-
* Memoized function to get or create
|
|
3912
|
-
* Each strategy gets its own isolated storage instance.
|
|
5929
|
+
* Memoized function to get or create HeatmapStorage for a strategy.
|
|
5930
|
+
* Each strategy gets its own isolated heatmap storage instance.
|
|
3913
5931
|
*/
|
|
3914
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new
|
|
5932
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new HeatmapStorage());
|
|
3915
5933
|
/**
|
|
3916
|
-
* Processes
|
|
3917
|
-
* Should be called from
|
|
5934
|
+
* Processes tick events and accumulates closed signals.
|
|
5935
|
+
* Should be called from signal emitter subscription.
|
|
3918
5936
|
*
|
|
3919
|
-
*
|
|
5937
|
+
* Only processes closed signals - opened signals are ignored.
|
|
5938
|
+
*
|
|
5939
|
+
* @param data - Tick result from strategy execution (closed signals only)
|
|
3920
5940
|
*/
|
|
3921
|
-
this.
|
|
3922
|
-
this.loggerService.log("
|
|
3923
|
-
|
|
5941
|
+
this.tick = async (data) => {
|
|
5942
|
+
this.loggerService.log("heatMarkdownService tick", {
|
|
5943
|
+
data,
|
|
3924
5944
|
});
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
5945
|
+
if (data.action !== "closed") {
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5948
|
+
const storage = this.getStorage(data.strategyName);
|
|
5949
|
+
storage.addSignal(data);
|
|
3928
5950
|
};
|
|
3929
5951
|
/**
|
|
3930
|
-
* Gets aggregated
|
|
5952
|
+
* Gets aggregated portfolio heatmap statistics for a strategy.
|
|
3931
5953
|
*
|
|
3932
|
-
* @param strategyName - Strategy name to get data for
|
|
3933
|
-
* @returns
|
|
5954
|
+
* @param strategyName - Strategy name to get heatmap data for
|
|
5955
|
+
* @returns Promise resolving to heatmap statistics with per-symbol and portfolio-wide metrics
|
|
3934
5956
|
*
|
|
3935
5957
|
* @example
|
|
3936
5958
|
* ```typescript
|
|
3937
|
-
* const
|
|
3938
|
-
*
|
|
3939
|
-
*
|
|
3940
|
-
*
|
|
5959
|
+
* const service = new HeatMarkdownService();
|
|
5960
|
+
* const stats = await service.getData("my-strategy");
|
|
5961
|
+
*
|
|
5962
|
+
* console.log(`Total symbols: ${stats.totalSymbols}`);
|
|
5963
|
+
* console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
|
|
5964
|
+
*
|
|
5965
|
+
* stats.symbols.forEach(row => {
|
|
5966
|
+
* console.log(`${row.symbol}: ${row.totalPnl}% (${row.totalTrades} trades)`);
|
|
5967
|
+
* });
|
|
3941
5968
|
* ```
|
|
3942
5969
|
*/
|
|
3943
5970
|
this.getData = async (strategyName) => {
|
|
3944
|
-
this.loggerService.log(
|
|
5971
|
+
this.loggerService.log(HEATMAP_METHOD_NAME_GET_DATA, {
|
|
3945
5972
|
strategyName,
|
|
3946
5973
|
});
|
|
3947
5974
|
const storage = this.getStorage(strategyName);
|
|
3948
|
-
return storage.getData(
|
|
5975
|
+
return storage.getData();
|
|
3949
5976
|
};
|
|
3950
5977
|
/**
|
|
3951
|
-
* Generates markdown report with
|
|
5978
|
+
* Generates markdown report with portfolio heatmap table for a strategy.
|
|
3952
5979
|
*
|
|
3953
|
-
* @param strategyName - Strategy name to generate report for
|
|
3954
|
-
* @returns
|
|
5980
|
+
* @param strategyName - Strategy name to generate heatmap report for
|
|
5981
|
+
* @returns Promise resolving to markdown formatted report string
|
|
3955
5982
|
*
|
|
3956
5983
|
* @example
|
|
3957
5984
|
* ```typescript
|
|
3958
|
-
* const
|
|
5985
|
+
* const service = new HeatMarkdownService();
|
|
5986
|
+
* const markdown = await service.getReport("my-strategy");
|
|
3959
5987
|
* console.log(markdown);
|
|
5988
|
+
* // Output:
|
|
5989
|
+
* // # Portfolio Heatmap: my-strategy
|
|
5990
|
+
* //
|
|
5991
|
+
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
|
|
5992
|
+
* //
|
|
5993
|
+
* // | Symbol | Total PNL | Sharpe | Max DD | Trades |
|
|
5994
|
+
* // |--------|-----------|--------|--------|--------|
|
|
5995
|
+
* // | BTCUSDT | +15.5% | 2.10 | -2.5% | 45 |
|
|
5996
|
+
* // | ETHUSDT | +12.3% | 1.85 | -3.1% | 38 |
|
|
5997
|
+
* // ...
|
|
3960
5998
|
* ```
|
|
3961
5999
|
*/
|
|
3962
6000
|
this.getReport = async (strategyName) => {
|
|
3963
|
-
this.loggerService.log(
|
|
6001
|
+
this.loggerService.log(HEATMAP_METHOD_NAME_GET_REPORT, {
|
|
3964
6002
|
strategyName,
|
|
3965
6003
|
});
|
|
3966
6004
|
const storage = this.getStorage(strategyName);
|
|
3967
6005
|
return storage.getReport(strategyName);
|
|
3968
6006
|
};
|
|
3969
6007
|
/**
|
|
3970
|
-
* Saves
|
|
6008
|
+
* Saves heatmap report to disk for a strategy.
|
|
3971
6009
|
*
|
|
3972
|
-
*
|
|
3973
|
-
*
|
|
6010
|
+
* Creates directory if it doesn't exist.
|
|
6011
|
+
* Default filename: {strategyName}.md
|
|
6012
|
+
*
|
|
6013
|
+
* @param strategyName - Strategy name to save heatmap report for
|
|
6014
|
+
* @param path - Optional directory path to save report (default: "./logs/heatmap")
|
|
3974
6015
|
*
|
|
3975
6016
|
* @example
|
|
3976
6017
|
* ```typescript
|
|
3977
|
-
*
|
|
3978
|
-
* await performanceService.dump("my-strategy");
|
|
6018
|
+
* const service = new HeatMarkdownService();
|
|
3979
6019
|
*
|
|
3980
|
-
* // Save to
|
|
3981
|
-
* await
|
|
6020
|
+
* // Save to default path: ./logs/heatmap/my-strategy.md
|
|
6021
|
+
* await service.dump("my-strategy");
|
|
6022
|
+
*
|
|
6023
|
+
* // Save to custom path: ./reports/my-strategy.md
|
|
6024
|
+
* await service.dump("my-strategy", "./reports");
|
|
3982
6025
|
* ```
|
|
3983
6026
|
*/
|
|
3984
|
-
this.dump = async (strategyName, path = "./logs/
|
|
3985
|
-
this.loggerService.log(
|
|
6027
|
+
this.dump = async (strategyName, path = "./logs/heatmap") => {
|
|
6028
|
+
this.loggerService.log(HEATMAP_METHOD_NAME_DUMP, {
|
|
3986
6029
|
strategyName,
|
|
3987
6030
|
path,
|
|
3988
6031
|
});
|
|
@@ -3990,23 +6033,43 @@ class PerformanceMarkdownService {
|
|
|
3990
6033
|
await storage.dump(strategyName, path);
|
|
3991
6034
|
};
|
|
3992
6035
|
/**
|
|
3993
|
-
* Clears accumulated
|
|
6036
|
+
* Clears accumulated heatmap data from storage.
|
|
6037
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
6038
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
3994
6039
|
*
|
|
3995
6040
|
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
6041
|
+
*
|
|
6042
|
+
* @example
|
|
6043
|
+
* ```typescript
|
|
6044
|
+
* const service = new HeatMarkdownService();
|
|
6045
|
+
*
|
|
6046
|
+
* // Clear specific strategy data
|
|
6047
|
+
* await service.clear("my-strategy");
|
|
6048
|
+
*
|
|
6049
|
+
* // Clear all strategies' data
|
|
6050
|
+
* await service.clear();
|
|
6051
|
+
* ```
|
|
3996
6052
|
*/
|
|
3997
6053
|
this.clear = async (strategyName) => {
|
|
3998
|
-
this.loggerService.log(
|
|
6054
|
+
this.loggerService.log(HEATMAP_METHOD_NAME_CLEAR, {
|
|
3999
6055
|
strategyName,
|
|
4000
6056
|
});
|
|
4001
6057
|
this.getStorage.clear(strategyName);
|
|
4002
6058
|
};
|
|
4003
6059
|
/**
|
|
4004
|
-
* Initializes the service by subscribing to
|
|
6060
|
+
* Initializes the service by subscribing to signal events.
|
|
4005
6061
|
* Uses singleshot to ensure initialization happens only once.
|
|
6062
|
+
* Automatically called on first use.
|
|
6063
|
+
*
|
|
6064
|
+
* @example
|
|
6065
|
+
* ```typescript
|
|
6066
|
+
* const service = new HeatMarkdownService();
|
|
6067
|
+
* await service.init(); // Subscribe to signal events
|
|
6068
|
+
* ```
|
|
4006
6069
|
*/
|
|
4007
6070
|
this.init = functoolsKit.singleshot(async () => {
|
|
4008
|
-
this.loggerService.log("
|
|
4009
|
-
|
|
6071
|
+
this.loggerService.log("heatMarkdownService init");
|
|
6072
|
+
signalEmitter.subscribe(this.tick);
|
|
4010
6073
|
});
|
|
4011
6074
|
}
|
|
4012
6075
|
}
|
|
@@ -4084,6 +6147,12 @@ class StrategyValidationService {
|
|
|
4084
6147
|
* Injected logger service instance
|
|
4085
6148
|
*/
|
|
4086
6149
|
this.loggerService = inject(TYPES.loggerService);
|
|
6150
|
+
/**
|
|
6151
|
+
* @private
|
|
6152
|
+
* @readonly
|
|
6153
|
+
* Injected risk validation service instance
|
|
6154
|
+
*/
|
|
6155
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
4087
6156
|
/**
|
|
4088
6157
|
* @private
|
|
4089
6158
|
* Map storing strategy schemas by strategy name
|
|
@@ -4105,9 +6174,10 @@ class StrategyValidationService {
|
|
|
4105
6174
|
this._strategyMap.set(strategyName, strategySchema);
|
|
4106
6175
|
};
|
|
4107
6176
|
/**
|
|
4108
|
-
* Validates the existence of a strategy
|
|
6177
|
+
* Validates the existence of a strategy and its risk profile (if configured)
|
|
4109
6178
|
* @public
|
|
4110
6179
|
* @throws {Error} If strategyName is not found
|
|
6180
|
+
* @throws {Error} If riskName is configured but not found
|
|
4111
6181
|
* Memoized function to cache validation results
|
|
4112
6182
|
*/
|
|
4113
6183
|
this.validate = functoolsKit.memoize(([strategyName]) => strategyName, (strategyName, source) => {
|
|
@@ -4119,6 +6189,10 @@ class StrategyValidationService {
|
|
|
4119
6189
|
if (!strategy) {
|
|
4120
6190
|
throw new Error(`strategy ${strategyName} not found source=${source}`);
|
|
4121
6191
|
}
|
|
6192
|
+
// Validate risk profile if configured
|
|
6193
|
+
if (strategy.riskName) {
|
|
6194
|
+
this.riskValidationService.validate(strategy.riskName, source);
|
|
6195
|
+
}
|
|
4122
6196
|
return true;
|
|
4123
6197
|
});
|
|
4124
6198
|
/**
|
|
@@ -4194,6 +6268,194 @@ class FrameValidationService {
|
|
|
4194
6268
|
}
|
|
4195
6269
|
}
|
|
4196
6270
|
|
|
6271
|
+
/**
|
|
6272
|
+
* @class WalkerValidationService
|
|
6273
|
+
* Service for managing and validating walker configurations
|
|
6274
|
+
*/
|
|
6275
|
+
class WalkerValidationService {
|
|
6276
|
+
constructor() {
|
|
6277
|
+
/**
|
|
6278
|
+
* @private
|
|
6279
|
+
* @readonly
|
|
6280
|
+
* Injected logger service instance
|
|
6281
|
+
*/
|
|
6282
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
6283
|
+
/**
|
|
6284
|
+
* @private
|
|
6285
|
+
* Map storing walker schemas by walker name
|
|
6286
|
+
*/
|
|
6287
|
+
this._walkerMap = new Map();
|
|
6288
|
+
/**
|
|
6289
|
+
* Adds a walker schema to the validation service
|
|
6290
|
+
* @public
|
|
6291
|
+
* @throws {Error} If walkerName already exists
|
|
6292
|
+
*/
|
|
6293
|
+
this.addWalker = (walkerName, walkerSchema) => {
|
|
6294
|
+
this.loggerService.log("walkerValidationService addWalker", {
|
|
6295
|
+
walkerName,
|
|
6296
|
+
walkerSchema,
|
|
6297
|
+
});
|
|
6298
|
+
if (this._walkerMap.has(walkerName)) {
|
|
6299
|
+
throw new Error(`walker ${walkerName} already exist`);
|
|
6300
|
+
}
|
|
6301
|
+
this._walkerMap.set(walkerName, walkerSchema);
|
|
6302
|
+
};
|
|
6303
|
+
/**
|
|
6304
|
+
* Validates the existence of a walker
|
|
6305
|
+
* @public
|
|
6306
|
+
* @throws {Error} If walkerName is not found
|
|
6307
|
+
* Memoized function to cache validation results
|
|
6308
|
+
*/
|
|
6309
|
+
this.validate = functoolsKit.memoize(([walkerName]) => walkerName, (walkerName, source) => {
|
|
6310
|
+
this.loggerService.log("walkerValidationService validate", {
|
|
6311
|
+
walkerName,
|
|
6312
|
+
source,
|
|
6313
|
+
});
|
|
6314
|
+
const walker = this._walkerMap.get(walkerName);
|
|
6315
|
+
if (!walker) {
|
|
6316
|
+
throw new Error(`walker ${walkerName} not found source=${source}`);
|
|
6317
|
+
}
|
|
6318
|
+
return true;
|
|
6319
|
+
});
|
|
6320
|
+
/**
|
|
6321
|
+
* Returns a list of all registered walker schemas
|
|
6322
|
+
* @public
|
|
6323
|
+
* @returns Array of walker schemas with their configurations
|
|
6324
|
+
*/
|
|
6325
|
+
this.list = async () => {
|
|
6326
|
+
this.loggerService.log("walkerValidationService list");
|
|
6327
|
+
return Array.from(this._walkerMap.values());
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
/**
|
|
6333
|
+
* @class SizingValidationService
|
|
6334
|
+
* Service for managing and validating sizing configurations
|
|
6335
|
+
*/
|
|
6336
|
+
class SizingValidationService {
|
|
6337
|
+
constructor() {
|
|
6338
|
+
/**
|
|
6339
|
+
* @private
|
|
6340
|
+
* @readonly
|
|
6341
|
+
* Injected logger service instance
|
|
6342
|
+
*/
|
|
6343
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
6344
|
+
/**
|
|
6345
|
+
* @private
|
|
6346
|
+
* Map storing sizing schemas by sizing name
|
|
6347
|
+
*/
|
|
6348
|
+
this._sizingMap = new Map();
|
|
6349
|
+
/**
|
|
6350
|
+
* Adds a sizing schema to the validation service
|
|
6351
|
+
* @public
|
|
6352
|
+
* @throws {Error} If sizingName already exists
|
|
6353
|
+
*/
|
|
6354
|
+
this.addSizing = (sizingName, sizingSchema) => {
|
|
6355
|
+
this.loggerService.log("sizingValidationService addSizing", {
|
|
6356
|
+
sizingName,
|
|
6357
|
+
sizingSchema,
|
|
6358
|
+
});
|
|
6359
|
+
if (this._sizingMap.has(sizingName)) {
|
|
6360
|
+
throw new Error(`sizing ${sizingName} already exist`);
|
|
6361
|
+
}
|
|
6362
|
+
this._sizingMap.set(sizingName, sizingSchema);
|
|
6363
|
+
};
|
|
6364
|
+
/**
|
|
6365
|
+
* Validates the existence of a sizing and optionally its method
|
|
6366
|
+
* @public
|
|
6367
|
+
* @throws {Error} If sizingName is not found
|
|
6368
|
+
* @throws {Error} If method is provided and doesn't match sizing schema method
|
|
6369
|
+
* Memoized function to cache validation results
|
|
6370
|
+
*/
|
|
6371
|
+
this.validate = functoolsKit.memoize(([sizingName, source, method]) => `${sizingName}:${source}:${method || ""}`, (sizingName, source, method) => {
|
|
6372
|
+
this.loggerService.log("sizingValidationService validate", {
|
|
6373
|
+
sizingName,
|
|
6374
|
+
source,
|
|
6375
|
+
method,
|
|
6376
|
+
});
|
|
6377
|
+
const sizing = this._sizingMap.get(sizingName);
|
|
6378
|
+
if (!sizing) {
|
|
6379
|
+
throw new Error(`sizing ${sizingName} not found source=${source}`);
|
|
6380
|
+
}
|
|
6381
|
+
if (method !== undefined && sizing.method !== method) {
|
|
6382
|
+
throw new Error(`Sizing method mismatch: sizing "${sizingName}" is configured as "${sizing.method}" but "${method}" was requested at source=${source}`);
|
|
6383
|
+
}
|
|
6384
|
+
return true;
|
|
6385
|
+
});
|
|
6386
|
+
/**
|
|
6387
|
+
* Returns a list of all registered sizing schemas
|
|
6388
|
+
* @public
|
|
6389
|
+
* @returns Array of sizing schemas with their configurations
|
|
6390
|
+
*/
|
|
6391
|
+
this.list = async () => {
|
|
6392
|
+
this.loggerService.log("sizingValidationService list");
|
|
6393
|
+
return Array.from(this._sizingMap.values());
|
|
6394
|
+
};
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
|
|
6398
|
+
/**
|
|
6399
|
+
* @class RiskValidationService
|
|
6400
|
+
* Service for managing and validating risk configurations
|
|
6401
|
+
*/
|
|
6402
|
+
class RiskValidationService {
|
|
6403
|
+
constructor() {
|
|
6404
|
+
/**
|
|
6405
|
+
* @private
|
|
6406
|
+
* @readonly
|
|
6407
|
+
* Injected logger service instance
|
|
6408
|
+
*/
|
|
6409
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
6410
|
+
/**
|
|
6411
|
+
* @private
|
|
6412
|
+
* Map storing risk schemas by risk name
|
|
6413
|
+
*/
|
|
6414
|
+
this._riskMap = new Map();
|
|
6415
|
+
/**
|
|
6416
|
+
* Adds a risk schema to the validation service
|
|
6417
|
+
* @public
|
|
6418
|
+
* @throws {Error} If riskName already exists
|
|
6419
|
+
*/
|
|
6420
|
+
this.addRisk = (riskName, riskSchema) => {
|
|
6421
|
+
this.loggerService.log("riskValidationService addRisk", {
|
|
6422
|
+
riskName,
|
|
6423
|
+
riskSchema,
|
|
6424
|
+
});
|
|
6425
|
+
if (this._riskMap.has(riskName)) {
|
|
6426
|
+
throw new Error(`risk ${riskName} already exist`);
|
|
6427
|
+
}
|
|
6428
|
+
this._riskMap.set(riskName, riskSchema);
|
|
6429
|
+
};
|
|
6430
|
+
/**
|
|
6431
|
+
* Validates the existence of a risk profile
|
|
6432
|
+
* @public
|
|
6433
|
+
* @throws {Error} If riskName is not found
|
|
6434
|
+
* Memoized function to cache validation results
|
|
6435
|
+
*/
|
|
6436
|
+
this.validate = functoolsKit.memoize(([riskName, source]) => `${riskName}:${source}`, (riskName, source) => {
|
|
6437
|
+
this.loggerService.log("riskValidationService validate", {
|
|
6438
|
+
riskName,
|
|
6439
|
+
source,
|
|
6440
|
+
});
|
|
6441
|
+
const risk = this._riskMap.get(riskName);
|
|
6442
|
+
if (!risk) {
|
|
6443
|
+
throw new Error(`risk ${riskName} not found source=${source}`);
|
|
6444
|
+
}
|
|
6445
|
+
return true;
|
|
6446
|
+
});
|
|
6447
|
+
/**
|
|
6448
|
+
* Returns a list of all registered risk schemas
|
|
6449
|
+
* @public
|
|
6450
|
+
* @returns Array of risk schemas with their configurations
|
|
6451
|
+
*/
|
|
6452
|
+
this.list = async () => {
|
|
6453
|
+
this.loggerService.log("riskValidationService list");
|
|
6454
|
+
return Array.from(this._riskMap.values());
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
|
|
4197
6459
|
{
|
|
4198
6460
|
provide(TYPES.loggerService, () => new LoggerService());
|
|
4199
6461
|
}
|
|
@@ -4205,11 +6467,16 @@ class FrameValidationService {
|
|
|
4205
6467
|
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
4206
6468
|
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
4207
6469
|
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
6470
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
6471
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
4208
6472
|
}
|
|
4209
6473
|
{
|
|
4210
6474
|
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
4211
6475
|
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
4212
6476
|
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
6477
|
+
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
|
|
6478
|
+
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
|
|
6479
|
+
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
|
|
4213
6480
|
}
|
|
4214
6481
|
{
|
|
4215
6482
|
provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
|
|
@@ -4217,24 +6484,34 @@ class FrameValidationService {
|
|
|
4217
6484
|
provide(TYPES.frameGlobalService, () => new FrameGlobalService());
|
|
4218
6485
|
provide(TYPES.liveGlobalService, () => new LiveGlobalService());
|
|
4219
6486
|
provide(TYPES.backtestGlobalService, () => new BacktestGlobalService());
|
|
6487
|
+
provide(TYPES.walkerGlobalService, () => new WalkerGlobalService());
|
|
6488
|
+
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
6489
|
+
provide(TYPES.riskGlobalService, () => new RiskGlobalService());
|
|
4220
6490
|
}
|
|
4221
6491
|
{
|
|
4222
6492
|
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
|
|
4223
6493
|
provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
|
|
6494
|
+
provide(TYPES.walkerLogicPrivateService, () => new WalkerLogicPrivateService());
|
|
4224
6495
|
}
|
|
4225
6496
|
{
|
|
4226
6497
|
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
|
|
4227
6498
|
provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
|
|
6499
|
+
provide(TYPES.walkerLogicPublicService, () => new WalkerLogicPublicService());
|
|
4228
6500
|
}
|
|
4229
6501
|
{
|
|
4230
6502
|
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
|
|
4231
6503
|
provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
|
|
4232
6504
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
6505
|
+
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
6506
|
+
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
4233
6507
|
}
|
|
4234
6508
|
{
|
|
4235
6509
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
4236
6510
|
provide(TYPES.strategyValidationService, () => new StrategyValidationService());
|
|
4237
6511
|
provide(TYPES.frameValidationService, () => new FrameValidationService());
|
|
6512
|
+
provide(TYPES.walkerValidationService, () => new WalkerValidationService());
|
|
6513
|
+
provide(TYPES.sizingValidationService, () => new SizingValidationService());
|
|
6514
|
+
provide(TYPES.riskValidationService, () => new RiskValidationService());
|
|
4238
6515
|
}
|
|
4239
6516
|
|
|
4240
6517
|
const baseServices = {
|
|
@@ -4248,11 +6525,16 @@ const connectionServices = {
|
|
|
4248
6525
|
exchangeConnectionService: inject(TYPES.exchangeConnectionService),
|
|
4249
6526
|
strategyConnectionService: inject(TYPES.strategyConnectionService),
|
|
4250
6527
|
frameConnectionService: inject(TYPES.frameConnectionService),
|
|
6528
|
+
sizingConnectionService: inject(TYPES.sizingConnectionService),
|
|
6529
|
+
riskConnectionService: inject(TYPES.riskConnectionService),
|
|
4251
6530
|
};
|
|
4252
6531
|
const schemaServices = {
|
|
4253
6532
|
exchangeSchemaService: inject(TYPES.exchangeSchemaService),
|
|
4254
6533
|
strategySchemaService: inject(TYPES.strategySchemaService),
|
|
4255
6534
|
frameSchemaService: inject(TYPES.frameSchemaService),
|
|
6535
|
+
walkerSchemaService: inject(TYPES.walkerSchemaService),
|
|
6536
|
+
sizingSchemaService: inject(TYPES.sizingSchemaService),
|
|
6537
|
+
riskSchemaService: inject(TYPES.riskSchemaService),
|
|
4256
6538
|
};
|
|
4257
6539
|
const globalServices = {
|
|
4258
6540
|
exchangeGlobalService: inject(TYPES.exchangeGlobalService),
|
|
@@ -4260,24 +6542,34 @@ const globalServices = {
|
|
|
4260
6542
|
frameGlobalService: inject(TYPES.frameGlobalService),
|
|
4261
6543
|
liveGlobalService: inject(TYPES.liveGlobalService),
|
|
4262
6544
|
backtestGlobalService: inject(TYPES.backtestGlobalService),
|
|
6545
|
+
walkerGlobalService: inject(TYPES.walkerGlobalService),
|
|
6546
|
+
sizingGlobalService: inject(TYPES.sizingGlobalService),
|
|
6547
|
+
riskGlobalService: inject(TYPES.riskGlobalService),
|
|
4263
6548
|
};
|
|
4264
6549
|
const logicPrivateServices = {
|
|
4265
6550
|
backtestLogicPrivateService: inject(TYPES.backtestLogicPrivateService),
|
|
4266
6551
|
liveLogicPrivateService: inject(TYPES.liveLogicPrivateService),
|
|
6552
|
+
walkerLogicPrivateService: inject(TYPES.walkerLogicPrivateService),
|
|
4267
6553
|
};
|
|
4268
6554
|
const logicPublicServices = {
|
|
4269
6555
|
backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
|
|
4270
6556
|
liveLogicPublicService: inject(TYPES.liveLogicPublicService),
|
|
6557
|
+
walkerLogicPublicService: inject(TYPES.walkerLogicPublicService),
|
|
4271
6558
|
};
|
|
4272
6559
|
const markdownServices = {
|
|
4273
6560
|
backtestMarkdownService: inject(TYPES.backtestMarkdownService),
|
|
4274
6561
|
liveMarkdownService: inject(TYPES.liveMarkdownService),
|
|
4275
6562
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
6563
|
+
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
6564
|
+
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
4276
6565
|
};
|
|
4277
6566
|
const validationServices = {
|
|
4278
6567
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
4279
6568
|
strategyValidationService: inject(TYPES.strategyValidationService),
|
|
4280
6569
|
frameValidationService: inject(TYPES.frameValidationService),
|
|
6570
|
+
walkerValidationService: inject(TYPES.walkerValidationService),
|
|
6571
|
+
sizingValidationService: inject(TYPES.sizingValidationService),
|
|
6572
|
+
riskValidationService: inject(TYPES.riskValidationService),
|
|
4281
6573
|
};
|
|
4282
6574
|
const backtest = {
|
|
4283
6575
|
...baseServices,
|
|
@@ -4317,6 +6609,9 @@ async function setLogger(logger) {
|
|
|
4317
6609
|
const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
|
|
4318
6610
|
const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
|
|
4319
6611
|
const ADD_FRAME_METHOD_NAME = "add.addFrame";
|
|
6612
|
+
const ADD_WALKER_METHOD_NAME = "add.addWalker";
|
|
6613
|
+
const ADD_SIZING_METHOD_NAME = "add.addSizing";
|
|
6614
|
+
const ADD_RISK_METHOD_NAME = "add.addRisk";
|
|
4320
6615
|
/**
|
|
4321
6616
|
* Registers a trading strategy in the framework.
|
|
4322
6617
|
*
|
|
@@ -4423,24 +6718,198 @@ function addExchange(exchangeSchema) {
|
|
|
4423
6718
|
* startDate: new Date("2024-01-01T00:00:00Z"),
|
|
4424
6719
|
* endDate: new Date("2024-01-02T00:00:00Z"),
|
|
4425
6720
|
* callbacks: {
|
|
4426
|
-
* onTimeframe: (timeframe, startDate, endDate, interval) => {
|
|
4427
|
-
* console.log(`Generated ${timeframe.length} timeframes`);
|
|
6721
|
+
* onTimeframe: (timeframe, startDate, endDate, interval) => {
|
|
6722
|
+
* console.log(`Generated ${timeframe.length} timeframes`);
|
|
6723
|
+
* },
|
|
6724
|
+
* },
|
|
6725
|
+
* });
|
|
6726
|
+
* ```
|
|
6727
|
+
*/
|
|
6728
|
+
function addFrame(frameSchema) {
|
|
6729
|
+
backtest$1.loggerService.info(ADD_FRAME_METHOD_NAME, {
|
|
6730
|
+
frameSchema,
|
|
6731
|
+
});
|
|
6732
|
+
backtest$1.frameValidationService.addFrame(frameSchema.frameName, frameSchema);
|
|
6733
|
+
backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
|
|
6734
|
+
}
|
|
6735
|
+
/**
|
|
6736
|
+
* Registers a walker for strategy comparison.
|
|
6737
|
+
*
|
|
6738
|
+
* The walker executes backtests for multiple strategies on the same
|
|
6739
|
+
* historical data and compares their performance using a specified metric.
|
|
6740
|
+
*
|
|
6741
|
+
* @param walkerSchema - Walker configuration object
|
|
6742
|
+
* @param walkerSchema.walkerName - Unique walker identifier
|
|
6743
|
+
* @param walkerSchema.exchangeName - Exchange to use for all strategies
|
|
6744
|
+
* @param walkerSchema.frameName - Timeframe to use for all strategies
|
|
6745
|
+
* @param walkerSchema.strategies - Array of strategy names to compare
|
|
6746
|
+
* @param walkerSchema.metric - Metric to optimize (default: "sharpeRatio")
|
|
6747
|
+
* @param walkerSchema.callbacks - Optional lifecycle callbacks
|
|
6748
|
+
*
|
|
6749
|
+
* @example
|
|
6750
|
+
* ```typescript
|
|
6751
|
+
* addWalker({
|
|
6752
|
+
* walkerName: "llm-prompt-optimizer",
|
|
6753
|
+
* exchangeName: "binance",
|
|
6754
|
+
* frameName: "1d-backtest",
|
|
6755
|
+
* strategies: [
|
|
6756
|
+
* "my-strategy-v1",
|
|
6757
|
+
* "my-strategy-v2",
|
|
6758
|
+
* "my-strategy-v3"
|
|
6759
|
+
* ],
|
|
6760
|
+
* metric: "sharpeRatio",
|
|
6761
|
+
* callbacks: {
|
|
6762
|
+
* onStrategyComplete: (strategyName, symbol, stats, metric) => {
|
|
6763
|
+
* console.log(`${strategyName}: ${metric}`);
|
|
6764
|
+
* },
|
|
6765
|
+
* onComplete: (results) => {
|
|
6766
|
+
* console.log(`Best strategy: ${results.bestStrategy}`);
|
|
6767
|
+
* }
|
|
6768
|
+
* }
|
|
6769
|
+
* });
|
|
6770
|
+
* ```
|
|
6771
|
+
*/
|
|
6772
|
+
function addWalker(walkerSchema) {
|
|
6773
|
+
backtest$1.loggerService.info(ADD_WALKER_METHOD_NAME, {
|
|
6774
|
+
walkerSchema,
|
|
6775
|
+
});
|
|
6776
|
+
backtest$1.walkerValidationService.addWalker(walkerSchema.walkerName, walkerSchema);
|
|
6777
|
+
backtest$1.walkerSchemaService.register(walkerSchema.walkerName, walkerSchema);
|
|
6778
|
+
}
|
|
6779
|
+
/**
|
|
6780
|
+
* Registers a position sizing configuration in the framework.
|
|
6781
|
+
*
|
|
6782
|
+
* The sizing configuration defines:
|
|
6783
|
+
* - Position sizing method (fixed-percentage, kelly-criterion, atr-based)
|
|
6784
|
+
* - Risk parameters (risk percentage, Kelly multiplier, ATR multiplier)
|
|
6785
|
+
* - Position constraints (min/max size, max position percentage)
|
|
6786
|
+
* - Callback for calculation events
|
|
6787
|
+
*
|
|
6788
|
+
* @param sizingSchema - Sizing configuration object (discriminated union)
|
|
6789
|
+
* @param sizingSchema.sizingName - Unique sizing identifier
|
|
6790
|
+
* @param sizingSchema.method - Sizing method ("fixed-percentage" | "kelly-criterion" | "atr-based")
|
|
6791
|
+
* @param sizingSchema.riskPercentage - Risk percentage per trade (for fixed-percentage and atr-based)
|
|
6792
|
+
* @param sizingSchema.kellyMultiplier - Kelly multiplier (for kelly-criterion, default: 0.25)
|
|
6793
|
+
* @param sizingSchema.atrMultiplier - ATR multiplier (for atr-based, default: 2)
|
|
6794
|
+
* @param sizingSchema.maxPositionPercentage - Optional max position size as % of account
|
|
6795
|
+
* @param sizingSchema.minPositionSize - Optional minimum position size
|
|
6796
|
+
* @param sizingSchema.maxPositionSize - Optional maximum position size
|
|
6797
|
+
* @param sizingSchema.callbacks - Optional lifecycle callbacks
|
|
6798
|
+
*
|
|
6799
|
+
* @example
|
|
6800
|
+
* ```typescript
|
|
6801
|
+
* // Fixed percentage sizing
|
|
6802
|
+
* addSizing({
|
|
6803
|
+
* sizingName: "conservative",
|
|
6804
|
+
* method: "fixed-percentage",
|
|
6805
|
+
* riskPercentage: 1,
|
|
6806
|
+
* maxPositionPercentage: 10,
|
|
6807
|
+
* });
|
|
6808
|
+
*
|
|
6809
|
+
* // Kelly Criterion sizing
|
|
6810
|
+
* addSizing({
|
|
6811
|
+
* sizingName: "kelly",
|
|
6812
|
+
* method: "kelly-criterion",
|
|
6813
|
+
* kellyMultiplier: 0.25,
|
|
6814
|
+
* maxPositionPercentage: 20,
|
|
6815
|
+
* });
|
|
6816
|
+
*
|
|
6817
|
+
* // ATR-based sizing
|
|
6818
|
+
* addSizing({
|
|
6819
|
+
* sizingName: "atr-dynamic",
|
|
6820
|
+
* method: "atr-based",
|
|
6821
|
+
* riskPercentage: 2,
|
|
6822
|
+
* atrMultiplier: 2,
|
|
6823
|
+
* callbacks: {
|
|
6824
|
+
* onCalculate: (quantity, params) => {
|
|
6825
|
+
* console.log(`Calculated size: ${quantity} for ${params.symbol}`);
|
|
6826
|
+
* },
|
|
6827
|
+
* },
|
|
6828
|
+
* });
|
|
6829
|
+
* ```
|
|
6830
|
+
*/
|
|
6831
|
+
function addSizing(sizingSchema) {
|
|
6832
|
+
backtest$1.loggerService.info(ADD_SIZING_METHOD_NAME, {
|
|
6833
|
+
sizingSchema,
|
|
6834
|
+
});
|
|
6835
|
+
backtest$1.sizingValidationService.addSizing(sizingSchema.sizingName, sizingSchema);
|
|
6836
|
+
backtest$1.sizingSchemaService.register(sizingSchema.sizingName, sizingSchema);
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* Registers a risk management configuration in the framework.
|
|
6840
|
+
*
|
|
6841
|
+
* The risk configuration defines:
|
|
6842
|
+
* - Maximum concurrent positions across all strategies
|
|
6843
|
+
* - Custom validations for advanced risk logic (portfolio metrics, correlations, etc.)
|
|
6844
|
+
* - Callbacks for rejected/allowed signals
|
|
6845
|
+
*
|
|
6846
|
+
* Multiple ClientStrategy instances share the same ClientRisk instance,
|
|
6847
|
+
* enabling cross-strategy risk analysis. ClientRisk tracks all active positions
|
|
6848
|
+
* and provides access to them via validation functions.
|
|
6849
|
+
*
|
|
6850
|
+
* @param riskSchema - Risk configuration object
|
|
6851
|
+
* @param riskSchema.riskName - Unique risk profile identifier
|
|
6852
|
+
* @param riskSchema.maxConcurrentPositions - Optional max number of open positions across all strategies
|
|
6853
|
+
* @param riskSchema.validations - Optional custom validation functions with access to params and active positions
|
|
6854
|
+
* @param riskSchema.callbacks - Optional lifecycle callbacks (onRejected, onAllowed)
|
|
6855
|
+
*
|
|
6856
|
+
* @example
|
|
6857
|
+
* ```typescript
|
|
6858
|
+
* // Basic risk limit
|
|
6859
|
+
* addRisk({
|
|
6860
|
+
* riskName: "conservative",
|
|
6861
|
+
* maxConcurrentPositions: 5,
|
|
6862
|
+
* });
|
|
6863
|
+
*
|
|
6864
|
+
* // With custom validations (access to signal data and portfolio state)
|
|
6865
|
+
* addRisk({
|
|
6866
|
+
* riskName: "advanced",
|
|
6867
|
+
* maxConcurrentPositions: 10,
|
|
6868
|
+
* validations: [
|
|
6869
|
+
* {
|
|
6870
|
+
* validate: async ({ params }) => {
|
|
6871
|
+
* // params contains: symbol, strategyName, exchangeName, signal, currentPrice, timestamp
|
|
6872
|
+
* // Calculate portfolio metrics from external data source
|
|
6873
|
+
* const portfolio = await getPortfolioState();
|
|
6874
|
+
* if (portfolio.drawdown > 20) {
|
|
6875
|
+
* throw new Error("Portfolio drawdown exceeds 20%");
|
|
6876
|
+
* }
|
|
6877
|
+
* },
|
|
6878
|
+
* docDescription: "Prevents trading during high drawdown",
|
|
6879
|
+
* },
|
|
6880
|
+
* ({ params }) => {
|
|
6881
|
+
* // Access signal details
|
|
6882
|
+
* const positionValue = calculatePositionValue(params.signal, params.currentPrice);
|
|
6883
|
+
* if (positionValue > 10000) {
|
|
6884
|
+
* throw new Error("Position value exceeds $10,000 limit");
|
|
6885
|
+
* }
|
|
6886
|
+
* },
|
|
6887
|
+
* ],
|
|
6888
|
+
* callbacks: {
|
|
6889
|
+
* onRejected: (symbol, reason, limit, params) => {
|
|
6890
|
+
* console.log(`[RISK] Signal rejected for ${symbol}: ${reason}`);
|
|
6891
|
+
* },
|
|
6892
|
+
* onAllowed: (symbol, params) => {
|
|
6893
|
+
* console.log(`[RISK] Signal allowed for ${symbol}`);
|
|
4428
6894
|
* },
|
|
4429
6895
|
* },
|
|
4430
6896
|
* });
|
|
4431
6897
|
* ```
|
|
4432
6898
|
*/
|
|
4433
|
-
function
|
|
4434
|
-
backtest$1.loggerService.info(
|
|
4435
|
-
|
|
6899
|
+
function addRisk(riskSchema) {
|
|
6900
|
+
backtest$1.loggerService.info(ADD_RISK_METHOD_NAME, {
|
|
6901
|
+
riskSchema,
|
|
4436
6902
|
});
|
|
4437
|
-
backtest$1.
|
|
4438
|
-
backtest$1.
|
|
6903
|
+
backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
|
|
6904
|
+
backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
|
|
4439
6905
|
}
|
|
4440
6906
|
|
|
4441
6907
|
const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
|
|
4442
6908
|
const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
|
|
4443
6909
|
const LIST_FRAMES_METHOD_NAME = "list.listFrames";
|
|
6910
|
+
const LIST_WALKERS_METHOD_NAME = "list.listWalkers";
|
|
6911
|
+
const LIST_SIZINGS_METHOD_NAME = "list.listSizings";
|
|
6912
|
+
const LIST_RISKS_METHOD_NAME = "list.listRisks";
|
|
4444
6913
|
/**
|
|
4445
6914
|
* Returns a list of all registered exchange schemas.
|
|
4446
6915
|
*
|
|
@@ -4533,6 +7002,111 @@ async function listFrames() {
|
|
|
4533
7002
|
backtest$1.loggerService.log(LIST_FRAMES_METHOD_NAME);
|
|
4534
7003
|
return await backtest$1.frameValidationService.list();
|
|
4535
7004
|
}
|
|
7005
|
+
/**
|
|
7006
|
+
* Returns a list of all registered walker schemas.
|
|
7007
|
+
*
|
|
7008
|
+
* Retrieves all walkers that have been registered via addWalker().
|
|
7009
|
+
* Useful for debugging, documentation, or building dynamic UIs.
|
|
7010
|
+
*
|
|
7011
|
+
* @returns Array of walker schemas with their configurations
|
|
7012
|
+
*
|
|
7013
|
+
* @example
|
|
7014
|
+
* ```typescript
|
|
7015
|
+
* import { listWalkers, addWalker } from "backtest-kit";
|
|
7016
|
+
*
|
|
7017
|
+
* addWalker({
|
|
7018
|
+
* walkerName: "llm-prompt-optimizer",
|
|
7019
|
+
* note: "Compare LLM-based trading strategies",
|
|
7020
|
+
* exchangeName: "binance",
|
|
7021
|
+
* frameName: "1d-backtest",
|
|
7022
|
+
* strategies: ["my-strategy-v1", "my-strategy-v2"],
|
|
7023
|
+
* metric: "sharpeRatio",
|
|
7024
|
+
* });
|
|
7025
|
+
*
|
|
7026
|
+
* const walkers = listWalkers();
|
|
7027
|
+
* console.log(walkers);
|
|
7028
|
+
* // [{ walkerName: "llm-prompt-optimizer", note: "Compare LLM...", ... }]
|
|
7029
|
+
* ```
|
|
7030
|
+
*/
|
|
7031
|
+
async function listWalkers() {
|
|
7032
|
+
backtest$1.loggerService.log(LIST_WALKERS_METHOD_NAME);
|
|
7033
|
+
return await backtest$1.walkerValidationService.list();
|
|
7034
|
+
}
|
|
7035
|
+
/**
|
|
7036
|
+
* Returns a list of all registered sizing schemas.
|
|
7037
|
+
*
|
|
7038
|
+
* Retrieves all sizing configurations that have been registered via addSizing().
|
|
7039
|
+
* Useful for debugging, documentation, or building dynamic UIs.
|
|
7040
|
+
*
|
|
7041
|
+
* @returns Array of sizing schemas with their configurations
|
|
7042
|
+
*
|
|
7043
|
+
* @example
|
|
7044
|
+
* ```typescript
|
|
7045
|
+
* import { listSizings, addSizing } from "backtest-kit";
|
|
7046
|
+
*
|
|
7047
|
+
* addSizing({
|
|
7048
|
+
* sizingName: "conservative",
|
|
7049
|
+
* note: "Low risk fixed percentage sizing",
|
|
7050
|
+
* method: "fixed-percentage",
|
|
7051
|
+
* riskPercentage: 1,
|
|
7052
|
+
* maxPositionPercentage: 10,
|
|
7053
|
+
* });
|
|
7054
|
+
*
|
|
7055
|
+
* addSizing({
|
|
7056
|
+
* sizingName: "kelly",
|
|
7057
|
+
* note: "Kelly Criterion with quarter multiplier",
|
|
7058
|
+
* method: "kelly-criterion",
|
|
7059
|
+
* kellyMultiplier: 0.25,
|
|
7060
|
+
* });
|
|
7061
|
+
*
|
|
7062
|
+
* const sizings = listSizings();
|
|
7063
|
+
* console.log(sizings);
|
|
7064
|
+
* // [
|
|
7065
|
+
* // { sizingName: "conservative", method: "fixed-percentage", ... },
|
|
7066
|
+
* // { sizingName: "kelly", method: "kelly-criterion", ... }
|
|
7067
|
+
* // ]
|
|
7068
|
+
* ```
|
|
7069
|
+
*/
|
|
7070
|
+
async function listSizings() {
|
|
7071
|
+
backtest$1.loggerService.log(LIST_SIZINGS_METHOD_NAME);
|
|
7072
|
+
return await backtest$1.sizingValidationService.list();
|
|
7073
|
+
}
|
|
7074
|
+
/**
|
|
7075
|
+
* Returns a list of all registered risk schemas.
|
|
7076
|
+
*
|
|
7077
|
+
* Retrieves all risk configurations that have been registered via addRisk().
|
|
7078
|
+
* Useful for debugging, documentation, or building dynamic UIs.
|
|
7079
|
+
*
|
|
7080
|
+
* @returns Array of risk schemas with their configurations
|
|
7081
|
+
*
|
|
7082
|
+
* @example
|
|
7083
|
+
* ```typescript
|
|
7084
|
+
* import { listRisks, addRisk } from "backtest-kit";
|
|
7085
|
+
*
|
|
7086
|
+
* addRisk({
|
|
7087
|
+
* riskName: "conservative",
|
|
7088
|
+
* note: "Conservative risk management with tight position limits",
|
|
7089
|
+
* maxConcurrentPositions: 5,
|
|
7090
|
+
* });
|
|
7091
|
+
*
|
|
7092
|
+
* addRisk({
|
|
7093
|
+
* riskName: "aggressive",
|
|
7094
|
+
* note: "Aggressive risk management with loose limits",
|
|
7095
|
+
* maxConcurrentPositions: 10,
|
|
7096
|
+
* });
|
|
7097
|
+
*
|
|
7098
|
+
* const risks = listRisks();
|
|
7099
|
+
* console.log(risks);
|
|
7100
|
+
* // [
|
|
7101
|
+
* // { riskName: "conservative", maxConcurrentPositions: 5, ... },
|
|
7102
|
+
* // { riskName: "aggressive", maxConcurrentPositions: 10, ... }
|
|
7103
|
+
* // ]
|
|
7104
|
+
* ```
|
|
7105
|
+
*/
|
|
7106
|
+
async function listRisks() {
|
|
7107
|
+
backtest$1.loggerService.log(LIST_RISKS_METHOD_NAME);
|
|
7108
|
+
return await backtest$1.riskValidationService.list();
|
|
7109
|
+
}
|
|
4536
7110
|
|
|
4537
7111
|
const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
|
|
4538
7112
|
const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
|
|
@@ -4541,10 +7115,18 @@ const LISTEN_SIGNAL_LIVE_ONCE_METHOD_NAME = "event.listenSignalLiveOnce";
|
|
|
4541
7115
|
const LISTEN_SIGNAL_BACKTEST_METHOD_NAME = "event.listenSignalBacktest";
|
|
4542
7116
|
const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce";
|
|
4543
7117
|
const LISTEN_ERROR_METHOD_NAME = "event.listenError";
|
|
4544
|
-
const
|
|
4545
|
-
const
|
|
7118
|
+
const LISTEN_DONE_LIVE_METHOD_NAME = "event.listenDoneLive";
|
|
7119
|
+
const LISTEN_DONE_LIVE_ONCE_METHOD_NAME = "event.listenDoneLiveOnce";
|
|
7120
|
+
const LISTEN_DONE_BACKTEST_METHOD_NAME = "event.listenDoneBacktest";
|
|
7121
|
+
const LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME = "event.listenDoneBacktestOnce";
|
|
7122
|
+
const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
|
|
7123
|
+
const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
|
|
4546
7124
|
const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
|
|
4547
7125
|
const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
|
|
7126
|
+
const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
|
|
7127
|
+
const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
|
|
7128
|
+
const LISTEN_WALKER_COMPLETE_METHOD_NAME = "event.listenWalkerComplete";
|
|
7129
|
+
const LISTEN_VALIDATION_METHOD_NAME = "event.listenValidation";
|
|
4548
7130
|
/**
|
|
4549
7131
|
* Subscribes to all signal events with queued async processing.
|
|
4550
7132
|
*
|
|
@@ -4736,9 +7318,133 @@ function listenError(fn) {
|
|
|
4736
7318
|
return errorEmitter.subscribe(functoolsKit.queued(async (error) => fn(error)));
|
|
4737
7319
|
}
|
|
4738
7320
|
/**
|
|
4739
|
-
* Subscribes to background execution completion events with queued async processing.
|
|
7321
|
+
* Subscribes to live background execution completion events with queued async processing.
|
|
7322
|
+
*
|
|
7323
|
+
* Emits when Live.background() completes execution.
|
|
7324
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
7325
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
7326
|
+
*
|
|
7327
|
+
* @param fn - Callback function to handle completion events
|
|
7328
|
+
* @returns Unsubscribe function to stop listening to events
|
|
7329
|
+
*
|
|
7330
|
+
* @example
|
|
7331
|
+
* ```typescript
|
|
7332
|
+
* import { listenDoneLive, Live } from "backtest-kit";
|
|
7333
|
+
*
|
|
7334
|
+
* const unsubscribe = listenDoneLive((event) => {
|
|
7335
|
+
* console.log("Live completed:", event.strategyName, event.exchangeName, event.symbol);
|
|
7336
|
+
* });
|
|
7337
|
+
*
|
|
7338
|
+
* Live.background("BTCUSDT", {
|
|
7339
|
+
* strategyName: "my-strategy",
|
|
7340
|
+
* exchangeName: "binance"
|
|
7341
|
+
* });
|
|
7342
|
+
*
|
|
7343
|
+
* // Later: stop listening
|
|
7344
|
+
* unsubscribe();
|
|
7345
|
+
* ```
|
|
7346
|
+
*/
|
|
7347
|
+
function listenDoneLive(fn) {
|
|
7348
|
+
backtest$1.loggerService.log(LISTEN_DONE_LIVE_METHOD_NAME);
|
|
7349
|
+
return doneLiveSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
7350
|
+
}
|
|
7351
|
+
/**
|
|
7352
|
+
* Subscribes to filtered live background execution completion events with one-time execution.
|
|
7353
|
+
*
|
|
7354
|
+
* Emits when Live.background() completes execution.
|
|
7355
|
+
* Executes callback once and automatically unsubscribes.
|
|
7356
|
+
*
|
|
7357
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
7358
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
7359
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
7360
|
+
*
|
|
7361
|
+
* @example
|
|
7362
|
+
* ```typescript
|
|
7363
|
+
* import { listenDoneLiveOnce, Live } from "backtest-kit";
|
|
7364
|
+
*
|
|
7365
|
+
* // Wait for first live completion
|
|
7366
|
+
* listenDoneLiveOnce(
|
|
7367
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
7368
|
+
* (event) => console.log("BTCUSDT live completed:", event.strategyName)
|
|
7369
|
+
* );
|
|
7370
|
+
*
|
|
7371
|
+
* Live.background("BTCUSDT", {
|
|
7372
|
+
* strategyName: "my-strategy",
|
|
7373
|
+
* exchangeName: "binance"
|
|
7374
|
+
* });
|
|
7375
|
+
* ```
|
|
7376
|
+
*/
|
|
7377
|
+
function listenDoneLiveOnce(filterFn, fn) {
|
|
7378
|
+
backtest$1.loggerService.log(LISTEN_DONE_LIVE_ONCE_METHOD_NAME);
|
|
7379
|
+
return doneLiveSubject.filter(filterFn).once(fn);
|
|
7380
|
+
}
|
|
7381
|
+
/**
|
|
7382
|
+
* Subscribes to backtest background execution completion events with queued async processing.
|
|
7383
|
+
*
|
|
7384
|
+
* Emits when Backtest.background() completes execution.
|
|
7385
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
7386
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
7387
|
+
*
|
|
7388
|
+
* @param fn - Callback function to handle completion events
|
|
7389
|
+
* @returns Unsubscribe function to stop listening to events
|
|
7390
|
+
*
|
|
7391
|
+
* @example
|
|
7392
|
+
* ```typescript
|
|
7393
|
+
* import { listenDoneBacktest, Backtest } from "backtest-kit";
|
|
7394
|
+
*
|
|
7395
|
+
* const unsubscribe = listenDoneBacktest((event) => {
|
|
7396
|
+
* console.log("Backtest completed:", event.strategyName, event.exchangeName, event.symbol);
|
|
7397
|
+
* });
|
|
7398
|
+
*
|
|
7399
|
+
* Backtest.background("BTCUSDT", {
|
|
7400
|
+
* strategyName: "my-strategy",
|
|
7401
|
+
* exchangeName: "binance",
|
|
7402
|
+
* frameName: "1d-backtest"
|
|
7403
|
+
* });
|
|
7404
|
+
*
|
|
7405
|
+
* // Later: stop listening
|
|
7406
|
+
* unsubscribe();
|
|
7407
|
+
* ```
|
|
7408
|
+
*/
|
|
7409
|
+
function listenDoneBacktest(fn) {
|
|
7410
|
+
backtest$1.loggerService.log(LISTEN_DONE_BACKTEST_METHOD_NAME);
|
|
7411
|
+
return doneBacktestSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
7412
|
+
}
|
|
7413
|
+
/**
|
|
7414
|
+
* Subscribes to filtered backtest background execution completion events with one-time execution.
|
|
7415
|
+
*
|
|
7416
|
+
* Emits when Backtest.background() completes execution.
|
|
7417
|
+
* Executes callback once and automatically unsubscribes.
|
|
7418
|
+
*
|
|
7419
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
7420
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
7421
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
7422
|
+
*
|
|
7423
|
+
* @example
|
|
7424
|
+
* ```typescript
|
|
7425
|
+
* import { listenDoneBacktestOnce, Backtest } from "backtest-kit";
|
|
7426
|
+
*
|
|
7427
|
+
* // Wait for first backtest completion
|
|
7428
|
+
* listenDoneBacktestOnce(
|
|
7429
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
7430
|
+
* (event) => console.log("BTCUSDT backtest completed:", event.strategyName)
|
|
7431
|
+
* );
|
|
7432
|
+
*
|
|
7433
|
+
* Backtest.background("BTCUSDT", {
|
|
7434
|
+
* strategyName: "my-strategy",
|
|
7435
|
+
* exchangeName: "binance",
|
|
7436
|
+
* frameName: "1d-backtest"
|
|
7437
|
+
* });
|
|
7438
|
+
* ```
|
|
7439
|
+
*/
|
|
7440
|
+
function listenDoneBacktestOnce(filterFn, fn) {
|
|
7441
|
+
backtest$1.loggerService.log(LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME);
|
|
7442
|
+
return doneBacktestSubject.filter(filterFn).once(fn);
|
|
7443
|
+
}
|
|
7444
|
+
/**
|
|
7445
|
+
* Subscribes to walker background execution completion events with queued async processing.
|
|
4740
7446
|
*
|
|
4741
|
-
* Emits when
|
|
7447
|
+
* Emits when Walker.background() completes execution.
|
|
4742
7448
|
* Events are processed sequentially in order received, even if callback is async.
|
|
4743
7449
|
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
4744
7450
|
*
|
|
@@ -4747,33 +7453,162 @@ function listenError(fn) {
|
|
|
4747
7453
|
*
|
|
4748
7454
|
* @example
|
|
4749
7455
|
* ```typescript
|
|
4750
|
-
* import {
|
|
7456
|
+
* import { listenDoneWalker, Walker } from "backtest-kit";
|
|
7457
|
+
*
|
|
7458
|
+
* const unsubscribe = listenDoneWalker((event) => {
|
|
7459
|
+
* console.log("Walker completed:", event.strategyName, event.exchangeName, event.symbol);
|
|
7460
|
+
* });
|
|
7461
|
+
*
|
|
7462
|
+
* Walker.background("BTCUSDT", {
|
|
7463
|
+
* walkerName: "my-walker"
|
|
7464
|
+
* });
|
|
7465
|
+
*
|
|
7466
|
+
* // Later: stop listening
|
|
7467
|
+
* unsubscribe();
|
|
7468
|
+
* ```
|
|
7469
|
+
*/
|
|
7470
|
+
function listenDoneWalker(fn) {
|
|
7471
|
+
backtest$1.loggerService.log(LISTEN_DONE_WALKER_METHOD_NAME);
|
|
7472
|
+
return doneWalkerSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
7473
|
+
}
|
|
7474
|
+
/**
|
|
7475
|
+
* Subscribes to filtered walker background execution completion events with one-time execution.
|
|
7476
|
+
*
|
|
7477
|
+
* Emits when Walker.background() completes execution.
|
|
7478
|
+
* Executes callback once and automatically unsubscribes.
|
|
7479
|
+
*
|
|
7480
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
7481
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
7482
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
7483
|
+
*
|
|
7484
|
+
* @example
|
|
7485
|
+
* ```typescript
|
|
7486
|
+
* import { listenDoneWalkerOnce, Walker } from "backtest-kit";
|
|
7487
|
+
*
|
|
7488
|
+
* // Wait for first walker completion
|
|
7489
|
+
* listenDoneWalkerOnce(
|
|
7490
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
7491
|
+
* (event) => console.log("BTCUSDT walker completed:", event.strategyName)
|
|
7492
|
+
* );
|
|
7493
|
+
*
|
|
7494
|
+
* Walker.background("BTCUSDT", {
|
|
7495
|
+
* walkerName: "my-walker"
|
|
7496
|
+
* });
|
|
7497
|
+
* ```
|
|
7498
|
+
*/
|
|
7499
|
+
function listenDoneWalkerOnce(filterFn, fn) {
|
|
7500
|
+
backtest$1.loggerService.log(LISTEN_DONE_WALKER_ONCE_METHOD_NAME);
|
|
7501
|
+
return doneWalkerSubject.filter(filterFn).once(fn);
|
|
7502
|
+
}
|
|
7503
|
+
/**
|
|
7504
|
+
* Subscribes to backtest progress events with queued async processing.
|
|
7505
|
+
*
|
|
7506
|
+
* Emits during Backtest.background() execution to track progress.
|
|
7507
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
7508
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
7509
|
+
*
|
|
7510
|
+
* @param fn - Callback function to handle progress events
|
|
7511
|
+
* @returns Unsubscribe function to stop listening to events
|
|
7512
|
+
*
|
|
7513
|
+
* @example
|
|
7514
|
+
* ```typescript
|
|
7515
|
+
* import { listenProgress, Backtest } from "backtest-kit";
|
|
7516
|
+
*
|
|
7517
|
+
* const unsubscribe = listenProgress((event) => {
|
|
7518
|
+
* console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
|
|
7519
|
+
* console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
|
|
7520
|
+
* console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
|
|
7521
|
+
* });
|
|
7522
|
+
*
|
|
7523
|
+
* Backtest.background("BTCUSDT", {
|
|
7524
|
+
* strategyName: "my-strategy",
|
|
7525
|
+
* exchangeName: "binance",
|
|
7526
|
+
* frameName: "1d-backtest"
|
|
7527
|
+
* });
|
|
7528
|
+
*
|
|
7529
|
+
* // Later: stop listening
|
|
7530
|
+
* unsubscribe();
|
|
7531
|
+
* ```
|
|
7532
|
+
*/
|
|
7533
|
+
function listenProgress(fn) {
|
|
7534
|
+
backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
|
|
7535
|
+
return progressEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
7536
|
+
}
|
|
7537
|
+
/**
|
|
7538
|
+
* Subscribes to performance metric events with queued async processing.
|
|
7539
|
+
*
|
|
7540
|
+
* Emits during strategy execution to track timing metrics for operations.
|
|
7541
|
+
* Useful for profiling and identifying performance bottlenecks.
|
|
7542
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
7543
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
7544
|
+
*
|
|
7545
|
+
* @param fn - Callback function to handle performance events
|
|
7546
|
+
* @returns Unsubscribe function to stop listening to events
|
|
7547
|
+
*
|
|
7548
|
+
* @example
|
|
7549
|
+
* ```typescript
|
|
7550
|
+
* import { listenPerformance, Backtest } from "backtest-kit";
|
|
7551
|
+
*
|
|
7552
|
+
* const unsubscribe = listenPerformance((event) => {
|
|
7553
|
+
* console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
|
|
7554
|
+
* if (event.duration > 100) {
|
|
7555
|
+
* console.warn("Slow operation detected:", event.metricType);
|
|
7556
|
+
* }
|
|
7557
|
+
* });
|
|
7558
|
+
*
|
|
7559
|
+
* Backtest.background("BTCUSDT", {
|
|
7560
|
+
* strategyName: "my-strategy",
|
|
7561
|
+
* exchangeName: "binance",
|
|
7562
|
+
* frameName: "1d-backtest"
|
|
7563
|
+
* });
|
|
7564
|
+
*
|
|
7565
|
+
* // Later: stop listening
|
|
7566
|
+
* unsubscribe();
|
|
7567
|
+
* ```
|
|
7568
|
+
*/
|
|
7569
|
+
function listenPerformance(fn) {
|
|
7570
|
+
backtest$1.loggerService.log(LISTEN_PERFORMANCE_METHOD_NAME);
|
|
7571
|
+
return performanceEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
7572
|
+
}
|
|
7573
|
+
/**
|
|
7574
|
+
* Subscribes to walker progress events with queued async processing.
|
|
7575
|
+
*
|
|
7576
|
+
* Emits during Walker.run() execution after each strategy completes.
|
|
7577
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
7578
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
7579
|
+
*
|
|
7580
|
+
* @param fn - Callback function to handle walker progress events
|
|
7581
|
+
* @returns Unsubscribe function to stop listening to events
|
|
7582
|
+
*
|
|
7583
|
+
* @example
|
|
7584
|
+
* ```typescript
|
|
7585
|
+
* import { listenWalker, Walker } from "backtest-kit";
|
|
4751
7586
|
*
|
|
4752
|
-
* const unsubscribe =
|
|
4753
|
-
* console.log(
|
|
4754
|
-
*
|
|
4755
|
-
*
|
|
4756
|
-
* }
|
|
7587
|
+
* const unsubscribe = listenWalker((event) => {
|
|
7588
|
+
* console.log(`Progress: ${event.strategiesTested} / ${event.totalStrategies}`);
|
|
7589
|
+
* console.log(`Best strategy: ${event.bestStrategy} (${event.bestMetric})`);
|
|
7590
|
+
* console.log(`Current strategy: ${event.strategyName} (${event.metricValue})`);
|
|
4757
7591
|
* });
|
|
4758
7592
|
*
|
|
4759
|
-
*
|
|
4760
|
-
*
|
|
4761
|
-
* exchangeName: "binance"
|
|
7593
|
+
* Walker.run("BTCUSDT", {
|
|
7594
|
+
* walkerName: "my-walker",
|
|
7595
|
+
* exchangeName: "binance",
|
|
7596
|
+
* frameName: "1d-backtest"
|
|
4762
7597
|
* });
|
|
4763
7598
|
*
|
|
4764
7599
|
* // Later: stop listening
|
|
4765
7600
|
* unsubscribe();
|
|
4766
7601
|
* ```
|
|
4767
7602
|
*/
|
|
4768
|
-
function
|
|
4769
|
-
backtest$1.loggerService.log(
|
|
4770
|
-
return
|
|
7603
|
+
function listenWalker(fn) {
|
|
7604
|
+
backtest$1.loggerService.log(LISTEN_WALKER_METHOD_NAME);
|
|
7605
|
+
return walkerEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
4771
7606
|
}
|
|
4772
7607
|
/**
|
|
4773
|
-
* Subscribes to filtered
|
|
7608
|
+
* Subscribes to filtered walker progress events with one-time execution.
|
|
4774
7609
|
*
|
|
4775
|
-
*
|
|
4776
|
-
*
|
|
7610
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
7611
|
+
* and automatically unsubscribes. Useful for waiting for specific walker conditions.
|
|
4777
7612
|
*
|
|
4778
7613
|
* @param filterFn - Predicate to filter which events trigger the callback
|
|
4779
7614
|
* @param fn - Callback function to handle the filtered event (called only once)
|
|
@@ -4781,47 +7616,60 @@ function listenDone(fn) {
|
|
|
4781
7616
|
*
|
|
4782
7617
|
* @example
|
|
4783
7618
|
* ```typescript
|
|
4784
|
-
* import {
|
|
7619
|
+
* import { listenWalkerOnce, Walker } from "backtest-kit";
|
|
4785
7620
|
*
|
|
4786
|
-
* // Wait for
|
|
4787
|
-
*
|
|
4788
|
-
* (event) => event.
|
|
4789
|
-
* (event) =>
|
|
7621
|
+
* // Wait for walker to complete all strategies
|
|
7622
|
+
* listenWalkerOnce(
|
|
7623
|
+
* (event) => event.strategiesTested === event.totalStrategies,
|
|
7624
|
+
* (event) => {
|
|
7625
|
+
* console.log("Walker completed!");
|
|
7626
|
+
* console.log("Best strategy:", event.bestStrategy, event.bestMetric);
|
|
7627
|
+
* }
|
|
4790
7628
|
* );
|
|
4791
7629
|
*
|
|
4792
|
-
*
|
|
4793
|
-
*
|
|
7630
|
+
* // Wait for specific strategy to be tested
|
|
7631
|
+
* const cancel = listenWalkerOnce(
|
|
7632
|
+
* (event) => event.strategyName === "my-strategy-v2",
|
|
7633
|
+
* (event) => console.log("Strategy v2 tested:", event.metricValue)
|
|
7634
|
+
* );
|
|
7635
|
+
*
|
|
7636
|
+
* Walker.run("BTCUSDT", {
|
|
7637
|
+
* walkerName: "my-walker",
|
|
4794
7638
|
* exchangeName: "binance",
|
|
4795
7639
|
* frameName: "1d-backtest"
|
|
4796
7640
|
* });
|
|
7641
|
+
*
|
|
7642
|
+
* // Cancel if needed before event fires
|
|
7643
|
+
* cancel();
|
|
4797
7644
|
* ```
|
|
4798
7645
|
*/
|
|
4799
|
-
function
|
|
4800
|
-
backtest$1.loggerService.log(
|
|
4801
|
-
return
|
|
7646
|
+
function listenWalkerOnce(filterFn, fn) {
|
|
7647
|
+
backtest$1.loggerService.log(LISTEN_WALKER_ONCE_METHOD_NAME);
|
|
7648
|
+
return walkerEmitter.filter(filterFn).once(fn);
|
|
4802
7649
|
}
|
|
4803
7650
|
/**
|
|
4804
|
-
* Subscribes to
|
|
7651
|
+
* Subscribes to walker completion events with queued async processing.
|
|
4805
7652
|
*
|
|
4806
|
-
* Emits
|
|
7653
|
+
* Emits when Walker.run() completes testing all strategies.
|
|
4807
7654
|
* Events are processed sequentially in order received, even if callback is async.
|
|
4808
7655
|
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
4809
7656
|
*
|
|
4810
|
-
* @param fn - Callback function to handle
|
|
7657
|
+
* @param fn - Callback function to handle walker completion event
|
|
4811
7658
|
* @returns Unsubscribe function to stop listening to events
|
|
4812
7659
|
*
|
|
4813
7660
|
* @example
|
|
4814
7661
|
* ```typescript
|
|
4815
|
-
* import {
|
|
7662
|
+
* import { listenWalkerComplete, Walker } from "backtest-kit";
|
|
4816
7663
|
*
|
|
4817
|
-
* const unsubscribe =
|
|
4818
|
-
* console.log(`
|
|
4819
|
-
* console.log(
|
|
4820
|
-
* console.log(`
|
|
7664
|
+
* const unsubscribe = listenWalkerComplete((results) => {
|
|
7665
|
+
* console.log(`Walker ${results.walkerName} completed!`);
|
|
7666
|
+
* console.log(`Best strategy: ${results.bestStrategy}`);
|
|
7667
|
+
* console.log(`Best ${results.metric}: ${results.bestMetric}`);
|
|
7668
|
+
* console.log(`Tested ${results.totalStrategies} strategies`);
|
|
4821
7669
|
* });
|
|
4822
7670
|
*
|
|
4823
|
-
*
|
|
4824
|
-
*
|
|
7671
|
+
* Walker.run("BTCUSDT", {
|
|
7672
|
+
* walkerName: "my-walker",
|
|
4825
7673
|
* exchangeName: "binance",
|
|
4826
7674
|
* frameName: "1d-backtest"
|
|
4827
7675
|
* });
|
|
@@ -4830,45 +7678,37 @@ function listenDoneOnce(filterFn, fn) {
|
|
|
4830
7678
|
* unsubscribe();
|
|
4831
7679
|
* ```
|
|
4832
7680
|
*/
|
|
4833
|
-
function
|
|
4834
|
-
backtest$1.loggerService.log(
|
|
4835
|
-
return
|
|
7681
|
+
function listenWalkerComplete(fn) {
|
|
7682
|
+
backtest$1.loggerService.log(LISTEN_WALKER_COMPLETE_METHOD_NAME);
|
|
7683
|
+
return walkerCompleteSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
4836
7684
|
}
|
|
4837
7685
|
/**
|
|
4838
|
-
* Subscribes to
|
|
7686
|
+
* Subscribes to risk validation errors with queued async processing.
|
|
4839
7687
|
*
|
|
4840
|
-
* Emits
|
|
4841
|
-
* Useful for
|
|
7688
|
+
* Emits when risk validation functions throw errors during signal checking.
|
|
7689
|
+
* Useful for debugging and monitoring risk validation failures.
|
|
4842
7690
|
* Events are processed sequentially in order received, even if callback is async.
|
|
4843
7691
|
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
4844
7692
|
*
|
|
4845
|
-
* @param fn - Callback function to handle
|
|
7693
|
+
* @param fn - Callback function to handle validation errors
|
|
4846
7694
|
* @returns Unsubscribe function to stop listening to events
|
|
4847
7695
|
*
|
|
4848
7696
|
* @example
|
|
4849
7697
|
* ```typescript
|
|
4850
|
-
* import {
|
|
4851
|
-
*
|
|
4852
|
-
* const unsubscribe = listenPerformance((event) => {
|
|
4853
|
-
* console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
|
|
4854
|
-
* if (event.duration > 100) {
|
|
4855
|
-
* console.warn("Slow operation detected:", event.metricType);
|
|
4856
|
-
* }
|
|
4857
|
-
* });
|
|
7698
|
+
* import { listenValidation } from "./function/event";
|
|
4858
7699
|
*
|
|
4859
|
-
*
|
|
4860
|
-
*
|
|
4861
|
-
*
|
|
4862
|
-
* frameName: "1d-backtest"
|
|
7700
|
+
* const unsubscribe = listenValidation((error) => {
|
|
7701
|
+
* console.error("Risk validation error:", error.message);
|
|
7702
|
+
* // Log to monitoring service for debugging
|
|
4863
7703
|
* });
|
|
4864
7704
|
*
|
|
4865
7705
|
* // Later: stop listening
|
|
4866
7706
|
* unsubscribe();
|
|
4867
7707
|
* ```
|
|
4868
7708
|
*/
|
|
4869
|
-
function
|
|
4870
|
-
backtest$1.loggerService.log(
|
|
4871
|
-
return
|
|
7709
|
+
function listenValidation(fn) {
|
|
7710
|
+
backtest$1.loggerService.log(LISTEN_VALIDATION_METHOD_NAME);
|
|
7711
|
+
return validationSubject.subscribe(functoolsKit.queued(async (error) => fn(error)));
|
|
4872
7712
|
}
|
|
4873
7713
|
|
|
4874
7714
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
@@ -5084,7 +7924,7 @@ class BacktestUtils {
|
|
|
5084
7924
|
break;
|
|
5085
7925
|
}
|
|
5086
7926
|
}
|
|
5087
|
-
await
|
|
7927
|
+
await doneBacktestSubject.next({
|
|
5088
7928
|
exchangeName: context.exchangeName,
|
|
5089
7929
|
strategyName: context.strategyName,
|
|
5090
7930
|
backtest: true,
|
|
@@ -5264,7 +8104,7 @@ class LiveUtils {
|
|
|
5264
8104
|
break;
|
|
5265
8105
|
}
|
|
5266
8106
|
}
|
|
5267
|
-
await
|
|
8107
|
+
await doneLiveSubject.next({
|
|
5268
8108
|
exchangeName: context.exchangeName,
|
|
5269
8109
|
strategyName: context.strategyName,
|
|
5270
8110
|
backtest: false,
|
|
@@ -5481,16 +8321,478 @@ class Performance {
|
|
|
5481
8321
|
}
|
|
5482
8322
|
}
|
|
5483
8323
|
|
|
8324
|
+
const WALKER_METHOD_NAME_RUN = "WalkerUtils.run";
|
|
8325
|
+
const WALKER_METHOD_NAME_BACKGROUND = "WalkerUtils.background";
|
|
8326
|
+
const WALKER_METHOD_NAME_GET_DATA = "WalkerUtils.getData";
|
|
8327
|
+
const WALKER_METHOD_NAME_GET_REPORT = "WalkerUtils.getReport";
|
|
8328
|
+
const WALKER_METHOD_NAME_DUMP = "WalkerUtils.dump";
|
|
8329
|
+
/**
|
|
8330
|
+
* Utility class for walker operations.
|
|
8331
|
+
*
|
|
8332
|
+
* Provides simplified access to walkerGlobalService.run() with logging.
|
|
8333
|
+
* Automatically pulls exchangeName and frameName from walker schema.
|
|
8334
|
+
* Exported as singleton instance for convenient usage.
|
|
8335
|
+
*
|
|
8336
|
+
* @example
|
|
8337
|
+
* ```typescript
|
|
8338
|
+
* import { Walker } from "./classes/Walker";
|
|
8339
|
+
*
|
|
8340
|
+
* for await (const result of Walker.run("BTCUSDT", {
|
|
8341
|
+
* walkerName: "my-walker"
|
|
8342
|
+
* })) {
|
|
8343
|
+
* console.log("Progress:", result.strategiesTested, "/", result.totalStrategies);
|
|
8344
|
+
* console.log("Best strategy:", result.bestStrategy, result.bestMetric);
|
|
8345
|
+
* }
|
|
8346
|
+
* ```
|
|
8347
|
+
*/
|
|
8348
|
+
class WalkerUtils {
|
|
8349
|
+
constructor() {
|
|
8350
|
+
/**
|
|
8351
|
+
* Runs walker comparison for a symbol with context propagation.
|
|
8352
|
+
*
|
|
8353
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
8354
|
+
* @param context - Execution context with walker name
|
|
8355
|
+
* @returns Async generator yielding progress updates after each strategy
|
|
8356
|
+
*/
|
|
8357
|
+
this.run = (symbol, context) => {
|
|
8358
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_RUN, {
|
|
8359
|
+
symbol,
|
|
8360
|
+
context,
|
|
8361
|
+
});
|
|
8362
|
+
backtest$1.walkerValidationService.validate(context.walkerName, WALKER_METHOD_NAME_RUN);
|
|
8363
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
8364
|
+
backtest$1.exchangeValidationService.validate(walkerSchema.exchangeName, WALKER_METHOD_NAME_RUN);
|
|
8365
|
+
backtest$1.frameValidationService.validate(walkerSchema.frameName, WALKER_METHOD_NAME_RUN);
|
|
8366
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
8367
|
+
backtest$1.strategyValidationService.validate(strategyName, WALKER_METHOD_NAME_RUN);
|
|
8368
|
+
}
|
|
8369
|
+
backtest$1.walkerMarkdownService.clear(context.walkerName);
|
|
8370
|
+
// Clear backtest data for all strategies
|
|
8371
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
8372
|
+
backtest$1.backtestMarkdownService.clear(strategyName);
|
|
8373
|
+
backtest$1.strategyGlobalService.clear(strategyName);
|
|
8374
|
+
}
|
|
8375
|
+
return backtest$1.walkerGlobalService.run(symbol, {
|
|
8376
|
+
walkerName: context.walkerName,
|
|
8377
|
+
exchangeName: walkerSchema.exchangeName,
|
|
8378
|
+
frameName: walkerSchema.frameName,
|
|
8379
|
+
});
|
|
8380
|
+
};
|
|
8381
|
+
/**
|
|
8382
|
+
* Runs walker comparison in background without yielding results.
|
|
8383
|
+
*
|
|
8384
|
+
* Consumes all walker progress updates internally without exposing them.
|
|
8385
|
+
* Useful for running walker comparison for side effects only (callbacks, logging).
|
|
8386
|
+
*
|
|
8387
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
8388
|
+
* @param context - Execution context with walker name
|
|
8389
|
+
* @returns Cancellation closure
|
|
8390
|
+
*
|
|
8391
|
+
* @example
|
|
8392
|
+
* ```typescript
|
|
8393
|
+
* // Run walker silently, only callbacks will fire
|
|
8394
|
+
* await Walker.background("BTCUSDT", {
|
|
8395
|
+
* walkerName: "my-walker"
|
|
8396
|
+
* });
|
|
8397
|
+
* console.log("Walker comparison completed");
|
|
8398
|
+
* ```
|
|
8399
|
+
*/
|
|
8400
|
+
this.background = (symbol, context) => {
|
|
8401
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_BACKGROUND, {
|
|
8402
|
+
symbol,
|
|
8403
|
+
context,
|
|
8404
|
+
});
|
|
8405
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
|
|
8406
|
+
let isStopped = false;
|
|
8407
|
+
const task = async () => {
|
|
8408
|
+
for await (const _ of this.run(symbol, context)) {
|
|
8409
|
+
if (isStopped) {
|
|
8410
|
+
break;
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
await doneWalkerSubject.next({
|
|
8414
|
+
exchangeName: walkerSchema.exchangeName,
|
|
8415
|
+
strategyName: context.walkerName,
|
|
8416
|
+
backtest: true,
|
|
8417
|
+
symbol,
|
|
8418
|
+
});
|
|
8419
|
+
};
|
|
8420
|
+
task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
8421
|
+
return () => {
|
|
8422
|
+
isStopped = true;
|
|
8423
|
+
};
|
|
8424
|
+
};
|
|
8425
|
+
/**
|
|
8426
|
+
* Gets walker results data from all strategy comparisons.
|
|
8427
|
+
*
|
|
8428
|
+
* @param symbol - Trading symbol
|
|
8429
|
+
* @param walkerName - Walker name to get data for
|
|
8430
|
+
* @returns Promise resolving to walker results data object
|
|
8431
|
+
*
|
|
8432
|
+
* @example
|
|
8433
|
+
* ```typescript
|
|
8434
|
+
* const results = await Walker.getData("BTCUSDT", "my-walker");
|
|
8435
|
+
* console.log(results.bestStrategy, results.bestMetric);
|
|
8436
|
+
* ```
|
|
8437
|
+
*/
|
|
8438
|
+
this.getData = async (symbol, walkerName) => {
|
|
8439
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_DATA, {
|
|
8440
|
+
symbol,
|
|
8441
|
+
walkerName,
|
|
8442
|
+
});
|
|
8443
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
|
|
8444
|
+
return await backtest$1.walkerMarkdownService.getData(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
8445
|
+
exchangeName: walkerSchema.exchangeName,
|
|
8446
|
+
frameName: walkerSchema.frameName,
|
|
8447
|
+
});
|
|
8448
|
+
};
|
|
8449
|
+
/**
|
|
8450
|
+
* Generates markdown report with all strategy comparisons for a walker.
|
|
8451
|
+
*
|
|
8452
|
+
* @param symbol - Trading symbol
|
|
8453
|
+
* @param walkerName - Walker name to generate report for
|
|
8454
|
+
* @returns Promise resolving to markdown formatted report string
|
|
8455
|
+
*
|
|
8456
|
+
* @example
|
|
8457
|
+
* ```typescript
|
|
8458
|
+
* const markdown = await Walker.getReport("BTCUSDT", "my-walker");
|
|
8459
|
+
* console.log(markdown);
|
|
8460
|
+
* ```
|
|
8461
|
+
*/
|
|
8462
|
+
this.getReport = async (symbol, walkerName) => {
|
|
8463
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_GET_REPORT, {
|
|
8464
|
+
symbol,
|
|
8465
|
+
walkerName,
|
|
8466
|
+
});
|
|
8467
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
|
|
8468
|
+
return await backtest$1.walkerMarkdownService.getReport(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
8469
|
+
exchangeName: walkerSchema.exchangeName,
|
|
8470
|
+
frameName: walkerSchema.frameName,
|
|
8471
|
+
});
|
|
8472
|
+
};
|
|
8473
|
+
/**
|
|
8474
|
+
* Saves walker report to disk.
|
|
8475
|
+
*
|
|
8476
|
+
* @param symbol - Trading symbol
|
|
8477
|
+
* @param walkerName - Walker name to save report for
|
|
8478
|
+
* @param path - Optional directory path to save report (default: "./logs/walker")
|
|
8479
|
+
*
|
|
8480
|
+
* @example
|
|
8481
|
+
* ```typescript
|
|
8482
|
+
* // Save to default path: ./logs/walker/my-walker.md
|
|
8483
|
+
* await Walker.dump("BTCUSDT", "my-walker");
|
|
8484
|
+
*
|
|
8485
|
+
* // Save to custom path: ./custom/path/my-walker.md
|
|
8486
|
+
* await Walker.dump("BTCUSDT", "my-walker", "./custom/path");
|
|
8487
|
+
* ```
|
|
8488
|
+
*/
|
|
8489
|
+
this.dump = async (symbol, walkerName, path) => {
|
|
8490
|
+
backtest$1.loggerService.info(WALKER_METHOD_NAME_DUMP, {
|
|
8491
|
+
symbol,
|
|
8492
|
+
walkerName,
|
|
8493
|
+
path,
|
|
8494
|
+
});
|
|
8495
|
+
const walkerSchema = backtest$1.walkerSchemaService.get(walkerName);
|
|
8496
|
+
await backtest$1.walkerMarkdownService.dump(walkerName, symbol, walkerSchema.metric || "sharpeRatio", {
|
|
8497
|
+
exchangeName: walkerSchema.exchangeName,
|
|
8498
|
+
frameName: walkerSchema.frameName,
|
|
8499
|
+
}, path);
|
|
8500
|
+
};
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
/**
|
|
8504
|
+
* Singleton instance of WalkerUtils for convenient walker operations.
|
|
8505
|
+
*
|
|
8506
|
+
* @example
|
|
8507
|
+
* ```typescript
|
|
8508
|
+
* import { Walker } from "./classes/Walker";
|
|
8509
|
+
*
|
|
8510
|
+
* for await (const result of Walker.run("BTCUSDT", {
|
|
8511
|
+
* walkerName: "my-walker"
|
|
8512
|
+
* })) {
|
|
8513
|
+
* console.log("Progress:", result.strategiesTested, "/", result.totalStrategies);
|
|
8514
|
+
* console.log("Best so far:", result.bestStrategy, result.bestMetric);
|
|
8515
|
+
* }
|
|
8516
|
+
* ```
|
|
8517
|
+
*/
|
|
8518
|
+
const Walker = new WalkerUtils();
|
|
8519
|
+
|
|
8520
|
+
const HEAT_METHOD_NAME_GET_DATA = "HeatUtils.getData";
|
|
8521
|
+
const HEAT_METHOD_NAME_GET_REPORT = "HeatUtils.getReport";
|
|
8522
|
+
const HEAT_METHOD_NAME_DUMP = "HeatUtils.dump";
|
|
8523
|
+
/**
|
|
8524
|
+
* Utility class for portfolio heatmap operations.
|
|
8525
|
+
*
|
|
8526
|
+
* Provides simplified access to heatMarkdownService with logging.
|
|
8527
|
+
* Automatically aggregates statistics across all symbols per strategy.
|
|
8528
|
+
* Exported as singleton instance for convenient usage.
|
|
8529
|
+
*
|
|
8530
|
+
* @example
|
|
8531
|
+
* ```typescript
|
|
8532
|
+
* import { Heat } from "backtest-kit";
|
|
8533
|
+
*
|
|
8534
|
+
* // Get raw heatmap data for a strategy
|
|
8535
|
+
* const stats = await Heat.getData("my-strategy");
|
|
8536
|
+
* console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
|
|
8537
|
+
*
|
|
8538
|
+
* // Generate markdown report
|
|
8539
|
+
* const markdown = await Heat.getReport("my-strategy");
|
|
8540
|
+
* console.log(markdown);
|
|
8541
|
+
*
|
|
8542
|
+
* // Save to disk
|
|
8543
|
+
* await Heat.dump("my-strategy", "./reports");
|
|
8544
|
+
* ```
|
|
8545
|
+
*/
|
|
8546
|
+
class HeatUtils {
|
|
8547
|
+
constructor() {
|
|
8548
|
+
/**
|
|
8549
|
+
* Gets aggregated portfolio heatmap statistics for a strategy.
|
|
8550
|
+
*
|
|
8551
|
+
* Returns per-symbol breakdown and portfolio-wide metrics.
|
|
8552
|
+
* Data is automatically collected from all closed signals for the strategy.
|
|
8553
|
+
*
|
|
8554
|
+
* @param strategyName - Strategy name to get heatmap data for
|
|
8555
|
+
* @returns Promise resolving to heatmap statistics object
|
|
8556
|
+
*
|
|
8557
|
+
* @example
|
|
8558
|
+
* ```typescript
|
|
8559
|
+
* const stats = await Heat.getData("my-strategy");
|
|
8560
|
+
*
|
|
8561
|
+
* console.log(`Total symbols: ${stats.totalSymbols}`);
|
|
8562
|
+
* console.log(`Portfolio Total PNL: ${stats.portfolioTotalPnl}%`);
|
|
8563
|
+
* console.log(`Portfolio Sharpe Ratio: ${stats.portfolioSharpeRatio}`);
|
|
8564
|
+
*
|
|
8565
|
+
* // Iterate through per-symbol statistics
|
|
8566
|
+
* stats.symbols.forEach(row => {
|
|
8567
|
+
* console.log(`${row.symbol}: ${row.totalPnl}% (${row.totalTrades} trades)`);
|
|
8568
|
+
* });
|
|
8569
|
+
* ```
|
|
8570
|
+
*/
|
|
8571
|
+
this.getData = async (strategyName) => {
|
|
8572
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_DATA, { strategyName });
|
|
8573
|
+
return await backtest$1.heatMarkdownService.getData(strategyName);
|
|
8574
|
+
};
|
|
8575
|
+
/**
|
|
8576
|
+
* Generates markdown report with portfolio heatmap table for a strategy.
|
|
8577
|
+
*
|
|
8578
|
+
* Table includes: Symbol, Total PNL, Sharpe Ratio, Max Drawdown, Trades.
|
|
8579
|
+
* Symbols are sorted by Total PNL descending.
|
|
8580
|
+
*
|
|
8581
|
+
* @param strategyName - Strategy name to generate heatmap report for
|
|
8582
|
+
* @returns Promise resolving to markdown formatted report string
|
|
8583
|
+
*
|
|
8584
|
+
* @example
|
|
8585
|
+
* ```typescript
|
|
8586
|
+
* const markdown = await Heat.getReport("my-strategy");
|
|
8587
|
+
* console.log(markdown);
|
|
8588
|
+
* // Output:
|
|
8589
|
+
* // # Portfolio Heatmap: my-strategy
|
|
8590
|
+
* //
|
|
8591
|
+
* // **Total Symbols:** 5 | **Portfolio PNL:** +45.3% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
|
|
8592
|
+
* //
|
|
8593
|
+
* // | Symbol | Total PNL | Sharpe | Max DD | Trades |
|
|
8594
|
+
* // |--------|-----------|--------|--------|--------|
|
|
8595
|
+
* // | BTCUSDT | +15.5% | 2.10 | -2.5% | 45 |
|
|
8596
|
+
* // | ETHUSDT | +12.3% | 1.85 | -3.1% | 38 |
|
|
8597
|
+
* // ...
|
|
8598
|
+
* ```
|
|
8599
|
+
*/
|
|
8600
|
+
this.getReport = async (strategyName) => {
|
|
8601
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_GET_REPORT, { strategyName });
|
|
8602
|
+
return await backtest$1.heatMarkdownService.getReport(strategyName);
|
|
8603
|
+
};
|
|
8604
|
+
/**
|
|
8605
|
+
* Saves heatmap report to disk for a strategy.
|
|
8606
|
+
*
|
|
8607
|
+
* Creates directory if it doesn't exist.
|
|
8608
|
+
* Default filename: {strategyName}.md
|
|
8609
|
+
*
|
|
8610
|
+
* @param strategyName - Strategy name to save heatmap report for
|
|
8611
|
+
* @param path - Optional directory path to save report (default: "./logs/heatmap")
|
|
8612
|
+
*
|
|
8613
|
+
* @example
|
|
8614
|
+
* ```typescript
|
|
8615
|
+
* // Save to default path: ./logs/heatmap/my-strategy.md
|
|
8616
|
+
* await Heat.dump("my-strategy");
|
|
8617
|
+
*
|
|
8618
|
+
* // Save to custom path: ./reports/my-strategy.md
|
|
8619
|
+
* await Heat.dump("my-strategy", "./reports");
|
|
8620
|
+
* ```
|
|
8621
|
+
*/
|
|
8622
|
+
this.dump = async (strategyName, path) => {
|
|
8623
|
+
backtest$1.loggerService.info(HEAT_METHOD_NAME_DUMP, { strategyName, path });
|
|
8624
|
+
await backtest$1.heatMarkdownService.dump(strategyName, path);
|
|
8625
|
+
};
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
/**
|
|
8629
|
+
* Singleton instance of HeatUtils for convenient heatmap operations.
|
|
8630
|
+
*
|
|
8631
|
+
* @example
|
|
8632
|
+
* ```typescript
|
|
8633
|
+
* import { Heat } from "backtest-kit";
|
|
8634
|
+
*
|
|
8635
|
+
* // Strategy-specific heatmap
|
|
8636
|
+
* const stats = await Heat.getData("my-strategy");
|
|
8637
|
+
* console.log(`Portfolio PNL: ${stats.portfolioTotalPnl}%`);
|
|
8638
|
+
* console.log(`Total Symbols: ${stats.totalSymbols}`);
|
|
8639
|
+
*
|
|
8640
|
+
* // Per-symbol breakdown
|
|
8641
|
+
* stats.symbols.forEach(row => {
|
|
8642
|
+
* console.log(`${row.symbol}:`);
|
|
8643
|
+
* console.log(` Total PNL: ${row.totalPnl}%`);
|
|
8644
|
+
* console.log(` Sharpe Ratio: ${row.sharpeRatio}`);
|
|
8645
|
+
* console.log(` Max Drawdown: ${row.maxDrawdown}%`);
|
|
8646
|
+
* console.log(` Trades: ${row.totalTrades}`);
|
|
8647
|
+
* });
|
|
8648
|
+
*
|
|
8649
|
+
* // Generate and save report
|
|
8650
|
+
* await Heat.dump("my-strategy", "./reports");
|
|
8651
|
+
* ```
|
|
8652
|
+
*/
|
|
8653
|
+
const Heat = new HeatUtils();
|
|
8654
|
+
|
|
8655
|
+
const POSITION_SIZE_METHOD_NAME_FIXED = "PositionSize.fixedPercentage";
|
|
8656
|
+
const POSITION_SIZE_METHOD_NAME_KELLY = "PositionSize.kellyCriterion";
|
|
8657
|
+
const POSITION_SIZE_METHOD_NAME_ATR = "PositionSize.atrBased";
|
|
8658
|
+
/**
|
|
8659
|
+
* Utility class for position sizing calculations.
|
|
8660
|
+
*
|
|
8661
|
+
* Provides static methods for each sizing method with validation.
|
|
8662
|
+
* Each method validates that the sizing schema matches the requested method.
|
|
8663
|
+
*
|
|
8664
|
+
* @example
|
|
8665
|
+
* ```typescript
|
|
8666
|
+
* import { PositionSize } from "./classes/PositionSize";
|
|
8667
|
+
*
|
|
8668
|
+
* // Fixed percentage sizing
|
|
8669
|
+
* const quantity = await PositionSize.fixedPercentage(
|
|
8670
|
+
* "BTCUSDT",
|
|
8671
|
+
* 10000,
|
|
8672
|
+
* 50000,
|
|
8673
|
+
* 49000,
|
|
8674
|
+
* { sizingName: "conservative" }
|
|
8675
|
+
* );
|
|
8676
|
+
*
|
|
8677
|
+
* // Kelly Criterion sizing
|
|
8678
|
+
* const quantity = await PositionSize.kellyCriterion(
|
|
8679
|
+
* "BTCUSDT",
|
|
8680
|
+
* 10000,
|
|
8681
|
+
* 50000,
|
|
8682
|
+
* 0.55,
|
|
8683
|
+
* 1.5,
|
|
8684
|
+
* { sizingName: "kelly" }
|
|
8685
|
+
* );
|
|
8686
|
+
*
|
|
8687
|
+
* // ATR-based sizing
|
|
8688
|
+
* const quantity = await PositionSize.atrBased(
|
|
8689
|
+
* "BTCUSDT",
|
|
8690
|
+
* 10000,
|
|
8691
|
+
* 50000,
|
|
8692
|
+
* 500,
|
|
8693
|
+
* { sizingName: "atr-dynamic" }
|
|
8694
|
+
* );
|
|
8695
|
+
* ```
|
|
8696
|
+
*/
|
|
8697
|
+
class PositionSizeUtils {
|
|
8698
|
+
}
|
|
8699
|
+
/**
|
|
8700
|
+
* Calculates position size using fixed percentage risk method.
|
|
8701
|
+
*
|
|
8702
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
8703
|
+
* @param accountBalance - Current account balance
|
|
8704
|
+
* @param priceOpen - Planned entry price
|
|
8705
|
+
* @param priceStopLoss - Stop-loss price
|
|
8706
|
+
* @param context - Execution context with sizing name
|
|
8707
|
+
* @returns Promise resolving to calculated position size
|
|
8708
|
+
* @throws Error if sizing schema method is not "fixed-percentage"
|
|
8709
|
+
*/
|
|
8710
|
+
PositionSizeUtils.fixedPercentage = async (symbol, accountBalance, priceOpen, priceStopLoss, context) => {
|
|
8711
|
+
backtest$1.loggerService.info(POSITION_SIZE_METHOD_NAME_FIXED, {
|
|
8712
|
+
context,
|
|
8713
|
+
symbol,
|
|
8714
|
+
});
|
|
8715
|
+
backtest$1.sizingValidationService.validate(context.sizingName, POSITION_SIZE_METHOD_NAME_FIXED, "fixed-percentage");
|
|
8716
|
+
return await backtest$1.sizingGlobalService.calculate({
|
|
8717
|
+
symbol,
|
|
8718
|
+
accountBalance,
|
|
8719
|
+
priceOpen,
|
|
8720
|
+
priceStopLoss,
|
|
8721
|
+
method: "fixed-percentage",
|
|
8722
|
+
}, context);
|
|
8723
|
+
};
|
|
8724
|
+
/**
|
|
8725
|
+
* Calculates position size using Kelly Criterion method.
|
|
8726
|
+
*
|
|
8727
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
8728
|
+
* @param accountBalance - Current account balance
|
|
8729
|
+
* @param priceOpen - Planned entry price
|
|
8730
|
+
* @param winRate - Win rate (0-1)
|
|
8731
|
+
* @param winLossRatio - Average win/loss ratio
|
|
8732
|
+
* @param context - Execution context with sizing name
|
|
8733
|
+
* @returns Promise resolving to calculated position size
|
|
8734
|
+
* @throws Error if sizing schema method is not "kelly-criterion"
|
|
8735
|
+
*/
|
|
8736
|
+
PositionSizeUtils.kellyCriterion = async (symbol, accountBalance, priceOpen, winRate, winLossRatio, context) => {
|
|
8737
|
+
backtest$1.loggerService.info(POSITION_SIZE_METHOD_NAME_KELLY, {
|
|
8738
|
+
context,
|
|
8739
|
+
symbol,
|
|
8740
|
+
});
|
|
8741
|
+
backtest$1.sizingValidationService.validate(context.sizingName, POSITION_SIZE_METHOD_NAME_KELLY, "kelly-criterion");
|
|
8742
|
+
return await backtest$1.sizingGlobalService.calculate({
|
|
8743
|
+
symbol,
|
|
8744
|
+
accountBalance,
|
|
8745
|
+
priceOpen,
|
|
8746
|
+
winRate,
|
|
8747
|
+
winLossRatio,
|
|
8748
|
+
method: "kelly-criterion",
|
|
8749
|
+
}, context);
|
|
8750
|
+
};
|
|
8751
|
+
/**
|
|
8752
|
+
* Calculates position size using ATR-based method.
|
|
8753
|
+
*
|
|
8754
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
8755
|
+
* @param accountBalance - Current account balance
|
|
8756
|
+
* @param priceOpen - Planned entry price
|
|
8757
|
+
* @param atr - Current ATR value
|
|
8758
|
+
* @param context - Execution context with sizing name
|
|
8759
|
+
* @returns Promise resolving to calculated position size
|
|
8760
|
+
* @throws Error if sizing schema method is not "atr-based"
|
|
8761
|
+
*/
|
|
8762
|
+
PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, context) => {
|
|
8763
|
+
backtest$1.loggerService.info(POSITION_SIZE_METHOD_NAME_ATR, {
|
|
8764
|
+
context,
|
|
8765
|
+
symbol,
|
|
8766
|
+
});
|
|
8767
|
+
backtest$1.sizingValidationService.validate(context.sizingName, POSITION_SIZE_METHOD_NAME_ATR, "atr-based");
|
|
8768
|
+
return await backtest$1.sizingGlobalService.calculate({
|
|
8769
|
+
symbol,
|
|
8770
|
+
accountBalance,
|
|
8771
|
+
priceOpen,
|
|
8772
|
+
atr,
|
|
8773
|
+
method: "atr-based",
|
|
8774
|
+
}, context);
|
|
8775
|
+
};
|
|
8776
|
+
const PositionSize = PositionSizeUtils;
|
|
8777
|
+
|
|
5484
8778
|
exports.Backtest = Backtest;
|
|
5485
8779
|
exports.ExecutionContextService = ExecutionContextService;
|
|
8780
|
+
exports.Heat = Heat;
|
|
5486
8781
|
exports.Live = Live;
|
|
5487
8782
|
exports.MethodContextService = MethodContextService;
|
|
5488
8783
|
exports.Performance = Performance;
|
|
5489
8784
|
exports.PersistBase = PersistBase;
|
|
8785
|
+
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
5490
8786
|
exports.PersistSignalAdaper = PersistSignalAdaper;
|
|
8787
|
+
exports.PositionSize = PositionSize;
|
|
8788
|
+
exports.Walker = Walker;
|
|
5491
8789
|
exports.addExchange = addExchange;
|
|
5492
8790
|
exports.addFrame = addFrame;
|
|
8791
|
+
exports.addRisk = addRisk;
|
|
8792
|
+
exports.addSizing = addSizing;
|
|
5493
8793
|
exports.addStrategy = addStrategy;
|
|
8794
|
+
exports.addWalker = addWalker;
|
|
8795
|
+
exports.emitters = emitters;
|
|
5494
8796
|
exports.formatPrice = formatPrice;
|
|
5495
8797
|
exports.formatQuantity = formatQuantity;
|
|
5496
8798
|
exports.getAveragePrice = getAveragePrice;
|
|
@@ -5500,9 +8802,16 @@ exports.getMode = getMode;
|
|
|
5500
8802
|
exports.lib = backtest;
|
|
5501
8803
|
exports.listExchanges = listExchanges;
|
|
5502
8804
|
exports.listFrames = listFrames;
|
|
8805
|
+
exports.listRisks = listRisks;
|
|
8806
|
+
exports.listSizings = listSizings;
|
|
5503
8807
|
exports.listStrategies = listStrategies;
|
|
5504
|
-
exports.
|
|
5505
|
-
exports.
|
|
8808
|
+
exports.listWalkers = listWalkers;
|
|
8809
|
+
exports.listenDoneBacktest = listenDoneBacktest;
|
|
8810
|
+
exports.listenDoneBacktestOnce = listenDoneBacktestOnce;
|
|
8811
|
+
exports.listenDoneLive = listenDoneLive;
|
|
8812
|
+
exports.listenDoneLiveOnce = listenDoneLiveOnce;
|
|
8813
|
+
exports.listenDoneWalker = listenDoneWalker;
|
|
8814
|
+
exports.listenDoneWalkerOnce = listenDoneWalkerOnce;
|
|
5506
8815
|
exports.listenError = listenError;
|
|
5507
8816
|
exports.listenPerformance = listenPerformance;
|
|
5508
8817
|
exports.listenProgress = listenProgress;
|
|
@@ -5512,4 +8821,8 @@ exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|
|
|
5512
8821
|
exports.listenSignalLive = listenSignalLive;
|
|
5513
8822
|
exports.listenSignalLiveOnce = listenSignalLiveOnce;
|
|
5514
8823
|
exports.listenSignalOnce = listenSignalOnce;
|
|
8824
|
+
exports.listenValidation = listenValidation;
|
|
8825
|
+
exports.listenWalker = listenWalker;
|
|
8826
|
+
exports.listenWalkerComplete = listenWalkerComplete;
|
|
8827
|
+
exports.listenWalkerOnce = listenWalkerOnce;
|
|
5515
8828
|
exports.setLogger = setLogger;
|