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