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