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