backtest-kit 1.10.4 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -9,6 +9,25 @@ var crypto = require('crypto');
9
9
  var os = require('os');
10
10
  var fs$1 = require('fs');
11
11
 
12
+ function _interopNamespaceDefault(e) {
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
30
+
12
31
  const { init, inject, provide } = diKit.createActivator("backtest");
13
32
 
14
33
  /**
@@ -102,6 +121,17 @@ const markdownServices$1 = {
102
121
  outlineMarkdownService: Symbol('outlineMarkdownService'),
103
122
  riskMarkdownService: Symbol('riskMarkdownService'),
104
123
  };
124
+ const reportServices$1 = {
125
+ backtestReportService: Symbol('backtestReportService'),
126
+ liveReportService: Symbol('liveReportService'),
127
+ scheduleReportService: Symbol('scheduleReportService'),
128
+ performanceReportService: Symbol('performanceReportService'),
129
+ walkerReportService: Symbol('walkerReportService'),
130
+ heatReportService: Symbol('heatReportService'),
131
+ partialReportService: Symbol('partialReportService'),
132
+ breakevenReportService: Symbol('breakevenReportService'),
133
+ riskReportService: Symbol('riskReportService'),
134
+ };
105
135
  const validationServices$1 = {
106
136
  exchangeValidationService: Symbol('exchangeValidationService'),
107
137
  strategyValidationService: Symbol('strategyValidationService'),
@@ -127,6 +157,7 @@ const TYPES = {
127
157
  ...logicPrivateServices$1,
128
158
  ...logicPublicServices$1,
129
159
  ...markdownServices$1,
160
+ ...reportServices$1,
130
161
  ...validationServices$1,
131
162
  ...templateServices$1,
132
163
  };
@@ -327,6 +358,12 @@ const GLOBAL_CONFIG = {
327
358
  * Default: 5000 ms (5 seconds)
328
359
  */
329
360
  CC_GET_CANDLES_RETRY_DELAY_MS: 5000,
361
+ /**
362
+ * Maximum number of candles to request per single API call.
363
+ * If a request exceeds this limit, data will be fetched using pagination.
364
+ * Default: 1000 candles per request
365
+ */
366
+ CC_MAX_CANDLES_PER_REQUEST: 1000,
330
367
  /**
331
368
  * Maximum allowed deviation factor for price anomaly detection.
332
369
  * Price should not be more than this factor lower than reference price.
@@ -694,11 +731,29 @@ class ClientExchange {
694
731
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
695
732
  }
696
733
  const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
697
- const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
734
+ let allData = [];
735
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
736
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
737
+ let remaining = limit;
738
+ let currentSince = new Date(since.getTime());
739
+ while (remaining > 0) {
740
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
741
+ const chunkData = await GET_CANDLES_FN({ symbol, interval, limit: chunkLimit }, currentSince, this);
742
+ allData.push(...chunkData);
743
+ remaining -= chunkLimit;
744
+ if (remaining > 0) {
745
+ // Move currentSince forward by the number of candles fetched
746
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
747
+ }
748
+ }
749
+ }
750
+ else {
751
+ allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
752
+ }
698
753
  // Filter candles to strictly match the requested range
699
754
  const whenTimestamp = this.params.execution.context.when.getTime();
700
755
  const sinceTimestamp = since.getTime();
701
- const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
756
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
702
757
  if (filteredData.length < limit) {
703
758
  this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
704
759
  }
@@ -730,10 +785,28 @@ class ClientExchange {
730
785
  if (endTime > now) {
731
786
  return [];
732
787
  }
733
- const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
788
+ let allData = [];
789
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
790
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
791
+ let remaining = limit;
792
+ let currentSince = new Date(since.getTime());
793
+ while (remaining > 0) {
794
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
795
+ const chunkData = await GET_CANDLES_FN({ symbol, interval, limit: chunkLimit }, currentSince, this);
796
+ allData.push(...chunkData);
797
+ remaining -= chunkLimit;
798
+ if (remaining > 0) {
799
+ // Move currentSince forward by the number of candles fetched
800
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
801
+ }
802
+ }
803
+ }
804
+ else {
805
+ allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
806
+ }
734
807
  // Filter candles to strictly match the requested range
735
808
  const sinceTimestamp = since.getTime();
736
- const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
809
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
737
810
  if (filteredData.length < limit) {
738
811
  this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
739
812
  }
@@ -1211,20 +1284,33 @@ async function writeFileAtomic(file, data, options = {}) {
1211
1284
  }
1212
1285
  }
1213
1286
 
1214
- var _a;
1287
+ var _a$2;
1215
1288
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
1216
1289
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
1217
1290
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
1218
1291
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
1292
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON = "PersistSignalUtils.useJson";
1293
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistSignalUtils.useDummy";
1219
1294
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
1220
1295
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
1221
1296
  const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
1297
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
1298
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
1222
1299
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
1223
1300
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
1224
1301
  const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
1302
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON = "PersistPartialUtils.useJson";
1303
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistPartialUtils.useDummy";
1225
1304
  const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER = "PersistBreakevenUtils.usePersistBreakevenAdapter";
1226
1305
  const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA = "PersistBreakevenUtils.readBreakevenData";
1227
1306
  const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA = "PersistBreakevenUtils.writeBreakevenData";
1307
+ const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON = "PersistBreakevenUtils.useJson";
1308
+ const PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY = "PersistBreakevenUtils.useDummy";
1309
+ const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
1310
+ const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
1311
+ const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
1312
+ const PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON = "PersistRiskUtils.useJson";
1313
+ const PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY = "PersistRiskUtils.useDummy";
1228
1314
  const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
1229
1315
  const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
1230
1316
  const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
@@ -1295,7 +1381,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
1295
1381
  constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
1296
1382
  this.entityName = entityName;
1297
1383
  this.baseDir = baseDir;
1298
- this[_a] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1384
+ this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1299
1385
  bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1300
1386
  entityName: this.entityName,
1301
1387
  baseDir,
@@ -1483,7 +1569,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
1483
1569
  *
1484
1570
  * @returns AsyncIterableIterator yielding entities
1485
1571
  */
1486
- async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
1572
+ async *[(_a$2 = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
1487
1573
  for await (const entity of this.values()) {
1488
1574
  yield entity;
1489
1575
  }
@@ -1533,6 +1619,44 @@ const PersistBase = functoolsKit.makeExtendable(class {
1533
1619
  }
1534
1620
  }
1535
1621
  });
1622
+ /**
1623
+ * Dummy persist adapter that discards all writes.
1624
+ * Used for disabling persistence.
1625
+ */
1626
+ class PersistDummy {
1627
+ /**
1628
+ * No-op initialization function.
1629
+ * @returns Promise that resolves immediately
1630
+ */
1631
+ async waitForInit() {
1632
+ }
1633
+ /**
1634
+ * No-op read function.
1635
+ * @returns Promise that resolves with empty object
1636
+ */
1637
+ async readValue() {
1638
+ return {};
1639
+ }
1640
+ /**
1641
+ * No-op has value check.
1642
+ * @returns Promise that resolves to false
1643
+ */
1644
+ async hasValue() {
1645
+ return false;
1646
+ }
1647
+ /**
1648
+ * No-op write function.
1649
+ * @returns Promise that resolves immediately
1650
+ */
1651
+ async writeValue() {
1652
+ }
1653
+ /**
1654
+ * No-op keys generator.
1655
+ * @returns Empty async generator
1656
+ */
1657
+ async *keys() {
1658
+ }
1659
+ }
1536
1660
  /**
1537
1661
  * Utility class for managing signal persistence.
1538
1662
  *
@@ -1612,6 +1736,22 @@ class PersistSignalUtils {
1612
1736
  bt.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1613
1737
  this.PersistSignalFactory = Ctor;
1614
1738
  }
1739
+ /**
1740
+ * Switches to the default JSON persist adapter.
1741
+ * All future persistence writes will use JSON storage.
1742
+ */
1743
+ useJson() {
1744
+ bt.loggerService.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1745
+ this.usePersistSignalAdapter(PersistBase);
1746
+ }
1747
+ /**
1748
+ * Switches to a dummy persist adapter that discards all writes.
1749
+ * All future persistence writes will be no-ops.
1750
+ */
1751
+ useDummy() {
1752
+ bt.loggerService.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1753
+ this.usePersistSignalAdapter(PersistDummy);
1754
+ }
1615
1755
  }
1616
1756
  /**
1617
1757
  * Global singleton instance of PersistSignalUtils.
@@ -1630,9 +1770,6 @@ class PersistSignalUtils {
1630
1770
  * ```
1631
1771
  */
1632
1772
  const PersistSignalAdapter = new PersistSignalUtils();
1633
- const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
1634
- const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
1635
- const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
1636
1773
  /**
1637
1774
  * Utility class for managing risk active positions persistence.
1638
1775
  *
@@ -1712,6 +1849,22 @@ class PersistRiskUtils {
1712
1849
  bt.loggerService.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1713
1850
  this.PersistRiskFactory = Ctor;
1714
1851
  }
1852
+ /**
1853
+ * Switches to the default JSON persist adapter.
1854
+ * All future persistence writes will use JSON storage.
1855
+ */
1856
+ useJson() {
1857
+ bt.loggerService.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1858
+ this.usePersistRiskAdapter(PersistBase);
1859
+ }
1860
+ /**
1861
+ * Switches to a dummy persist adapter that discards all writes.
1862
+ * All future persistence writes will be no-ops.
1863
+ */
1864
+ useDummy() {
1865
+ bt.loggerService.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1866
+ this.usePersistRiskAdapter(PersistDummy);
1867
+ }
1715
1868
  }
1716
1869
  /**
1717
1870
  * Global singleton instance of PersistRiskUtils.
@@ -1809,6 +1962,22 @@ class PersistScheduleUtils {
1809
1962
  bt.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1810
1963
  this.PersistScheduleFactory = Ctor;
1811
1964
  }
1965
+ /**
1966
+ * Switches to the default JSON persist adapter.
1967
+ * All future persistence writes will use JSON storage.
1968
+ */
1969
+ useJson() {
1970
+ bt.loggerService.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1971
+ this.usePersistScheduleAdapter(PersistBase);
1972
+ }
1973
+ /**
1974
+ * Switches to a dummy persist adapter that discards all writes.
1975
+ * All future persistence writes will be no-ops.
1976
+ */
1977
+ useDummy() {
1978
+ bt.loggerService.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1979
+ this.usePersistScheduleAdapter(PersistDummy);
1980
+ }
1812
1981
  }
1813
1982
  /**
1814
1983
  * Global singleton instance of PersistScheduleUtils.
@@ -1908,6 +2077,22 @@ class PersistPartialUtils {
1908
2077
  bt.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1909
2078
  this.PersistPartialFactory = Ctor;
1910
2079
  }
2080
+ /**
2081
+ * Switches to the default JSON persist adapter.
2082
+ * All future persistence writes will use JSON storage.
2083
+ */
2084
+ useJson() {
2085
+ bt.loggerService.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
2086
+ this.usePersistPartialAdapter(PersistBase);
2087
+ }
2088
+ /**
2089
+ * Switches to a dummy persist adapter that discards all writes.
2090
+ * All future persistence writes will be no-ops.
2091
+ */
2092
+ useDummy() {
2093
+ bt.loggerService.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
2094
+ this.usePersistPartialAdapter(PersistDummy);
2095
+ }
1911
2096
  }
1912
2097
  /**
1913
2098
  * Global singleton instance of PersistPartialUtils.
@@ -2042,6 +2227,22 @@ class PersistBreakevenUtils {
2042
2227
  bt.loggerService.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
2043
2228
  this.PersistBreakevenFactory = Ctor;
2044
2229
  }
2230
+ /**
2231
+ * Switches to the default JSON persist adapter.
2232
+ * All future persistence writes will use JSON storage.
2233
+ */
2234
+ useJson() {
2235
+ bt.loggerService.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
2236
+ this.usePersistBreakevenAdapter(PersistBase);
2237
+ }
2238
+ /**
2239
+ * Switches to a dummy persist adapter that discards all writes.
2240
+ * All future persistence writes will be no-ops.
2241
+ */
2242
+ useDummy() {
2243
+ bt.loggerService.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
2244
+ this.usePersistBreakevenAdapter(PersistDummy);
2245
+ }
2045
2246
  }
2046
2247
  /**
2047
2248
  * Global singleton instance of PersistBreakevenUtils.
@@ -2165,14 +2366,18 @@ const TIMEOUT_SYMBOL = Symbol('timeout');
2165
2366
  * ```
2166
2367
  */
2167
2368
  const TO_PUBLIC_SIGNAL = (signal) => {
2168
- const hasTrailingSL = signal._trailingPriceStopLoss !== undefined;
2169
- const hasTrailingTP = signal._trailingPriceTakeProfit !== undefined;
2369
+ const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
2370
+ const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
2371
+ const totalExecuted = ("_partial" in signal && Array.isArray(signal._partial))
2372
+ ? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
2373
+ : 0;
2170
2374
  return {
2171
2375
  ...structuredClone(signal),
2172
2376
  priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
2173
2377
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
2174
2378
  originalPriceStopLoss: signal.priceStopLoss,
2175
2379
  originalPriceTakeProfit: signal.priceTakeProfit,
2380
+ totalExecuted,
2176
2381
  };
2177
2382
  };
2178
2383
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
@@ -2623,7 +2828,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
2623
2828
  percentToClose,
2624
2829
  newTotalClosed,
2625
2830
  });
2626
- return;
2831
+ return false;
2627
2832
  }
2628
2833
  // Add new partial close entry
2629
2834
  signal._partial.push({
@@ -2638,6 +2843,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
2638
2843
  currentPrice,
2639
2844
  tpClosed: tpClosed + percentToClose,
2640
2845
  });
2846
+ return true;
2641
2847
  };
2642
2848
  const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2643
2849
  // Initialize partial array if not present
@@ -2660,7 +2866,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2660
2866
  percentToClose,
2661
2867
  newTotalClosed,
2662
2868
  });
2663
- return;
2869
+ return false;
2664
2870
  }
2665
2871
  // Add new partial close entry
2666
2872
  signal._partial.push({
@@ -2675,6 +2881,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
2675
2881
  currentPrice,
2676
2882
  slClosed: slClosed + percentToClose,
2677
2883
  });
2884
+ return true;
2678
2885
  };
2679
2886
  const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
2680
2887
  // CRITICAL: Always calculate from ORIGINAL SL, not from current trailing SL
@@ -3467,11 +3674,12 @@ const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signal,
3467
3674
  const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
3468
3675
  return await ExecutionContextService.runInContext(async () => {
3469
3676
  return await self.params.risk.checkSignal({
3470
- pendingSignal,
3677
+ pendingSignal: TO_PUBLIC_SIGNAL(pendingSignal),
3471
3678
  symbol: symbol,
3472
3679
  strategyName: self.params.method.context.strategyName,
3473
3680
  exchangeName: self.params.method.context.exchangeName,
3474
3681
  frameName: self.params.method.context.frameName,
3682
+ riskName: self.params.riskName,
3475
3683
  currentPrice,
3476
3684
  timestamp,
3477
3685
  });
@@ -3589,6 +3797,7 @@ const CALL_BREAKEVEN_CLEAR_FN = functoolsKit.trycatch(async (self, symbol, signa
3589
3797
  const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
3590
3798
  const currentTime = self.params.execution.context.when.getTime();
3591
3799
  await CALL_PING_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentTime, self.params.execution.context.backtest);
3800
+ const pnl = toProfitLossDto(scheduled, currentPrice);
3592
3801
  const result = {
3593
3802
  action: "active",
3594
3803
  signal: TO_PUBLIC_SIGNAL(scheduled),
@@ -3599,6 +3808,7 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
3599
3808
  symbol: self.params.execution.context.symbol,
3600
3809
  percentTp: 0,
3601
3810
  percentSl: 0,
3811
+ pnl,
3602
3812
  backtest: self.params.execution.context.backtest,
3603
3813
  };
3604
3814
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
@@ -3767,6 +3977,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3767
3977
  }
3768
3978
  }
3769
3979
  }
3980
+ const pnl = toProfitLossDto(signal, currentPrice);
3770
3981
  const result = {
3771
3982
  action: "active",
3772
3983
  signal: TO_PUBLIC_SIGNAL(signal),
@@ -3777,6 +3988,7 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3777
3988
  symbol: self.params.execution.context.symbol,
3778
3989
  percentTp,
3779
3990
  percentSl,
3991
+ pnl,
3780
3992
  backtest: self.params.execution.context.backtest,
3781
3993
  };
3782
3994
  await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, currentTime, self.params.execution.context.backtest);
@@ -4566,12 +4778,14 @@ class ClientStrategy {
4566
4778
  // Don't cancel - just return last active state
4567
4779
  // In real backtest flow this won't happen as we process all candles at once,
4568
4780
  // but this is correct behavior if someone calls backtest() with partial data
4781
+ const pnl = toProfitLossDto(scheduled, lastPrice);
4569
4782
  const result = {
4570
4783
  action: "active",
4571
4784
  signal: TO_PUBLIC_SIGNAL(scheduled),
4572
4785
  currentPrice: lastPrice,
4573
4786
  percentSl: 0,
4574
4787
  percentTp: 0,
4788
+ pnl,
4575
4789
  strategyName: this.params.method.context.strategyName,
4576
4790
  exchangeName: this.params.method.context.exchangeName,
4577
4791
  frameName: this.params.method.context.frameName,
@@ -4696,7 +4910,8 @@ class ClientStrategy {
4696
4910
  * Behavior:
4697
4911
  * - Adds entry to signal's `_partial` array with type "profit"
4698
4912
  * - Validates percentToClose is in range (0, 100]
4699
- * - Silently skips if total closed would exceed 100%
4913
+ * - Returns false if total closed would exceed 100%
4914
+ * - Returns false if currentPrice already crossed TP level
4700
4915
  * - Persists updated signal state (backtest and live modes)
4701
4916
  * - Calls onWrite callback for persistence testing
4702
4917
  *
@@ -4713,17 +4928,21 @@ class ClientStrategy {
4713
4928
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
4714
4929
  * @param currentPrice - Current market price for this partial close (must be in profit direction)
4715
4930
  * @param backtest - Whether running in backtest mode (controls persistence)
4716
- * @returns Promise that resolves when state is updated and persisted
4931
+ * @returns Promise<boolean> - true if partial close was executed, false if skipped
4717
4932
  *
4718
4933
  * @example
4719
4934
  * ```typescript
4720
4935
  * // Close 30% of position at profit (moving toward TP)
4721
- * await strategy.partialProfit("BTCUSDT", 30, 45000, false);
4936
+ * const success1 = await strategy.partialProfit("BTCUSDT", 30, 45000, false);
4937
+ * // success1 = true (executed)
4722
4938
  *
4723
4939
  * // Later close another 20%
4724
- * await strategy.partialProfit("BTCUSDT", 20, 46000, false);
4940
+ * const success2 = await strategy.partialProfit("BTCUSDT", 20, 46000, false);
4941
+ * // success2 = true (executed, total 50% closed)
4725
4942
  *
4726
- * // Final close will calculate weighted PNL from all partials
4943
+ * // Try to close 60% more (would exceed 100%)
4944
+ * const success3 = await strategy.partialProfit("BTCUSDT", 60, 47000, false);
4945
+ * // success3 = false (skipped, would exceed 100%)
4727
4946
  * ```
4728
4947
  */
4729
4948
  async partialProfit(symbol, percentToClose, currentPrice, backtest) {
@@ -4774,7 +4993,7 @@ class ClientStrategy {
4774
4993
  effectiveTakeProfit,
4775
4994
  reason: "currentPrice >= effectiveTakeProfit (LONG position)"
4776
4995
  });
4777
- return;
4996
+ return false;
4778
4997
  }
4779
4998
  if (this._pendingSignal.position === "short" && currentPrice <= effectiveTakeProfit) {
4780
4999
  this.params.logger.debug("ClientStrategy partialProfit: price already at/below TP, skipping partial close", {
@@ -4784,10 +5003,14 @@ class ClientStrategy {
4784
5003
  effectiveTakeProfit,
4785
5004
  reason: "currentPrice <= effectiveTakeProfit (SHORT position)"
4786
5005
  });
4787
- return;
5006
+ return false;
4788
5007
  }
4789
5008
  // Execute partial close logic
4790
- PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
5009
+ const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
5010
+ // If partial was not executed (exceeded 100%), return false without persistence
5011
+ if (!wasExecuted) {
5012
+ return false;
5013
+ }
4791
5014
  // Persist updated signal state (inline setPendingSignal content)
4792
5015
  // Note: this._pendingSignal already mutated by PARTIAL_PROFIT_FN, no reassignment needed
4793
5016
  this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
@@ -4800,6 +5023,7 @@ class ClientStrategy {
4800
5023
  if (!backtest) {
4801
5024
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
4802
5025
  }
5026
+ return true;
4803
5027
  }
4804
5028
  /**
4805
5029
  * Executes partial close at loss level (moving toward SL).
@@ -4810,7 +5034,8 @@ class ClientStrategy {
4810
5034
  * Behavior:
4811
5035
  * - Adds entry to signal's `_partial` array with type "loss"
4812
5036
  * - Validates percentToClose is in range (0, 100]
4813
- * - Silently skips if total closed would exceed 100%
5037
+ * - Returns false if total closed would exceed 100%
5038
+ * - Returns false if currentPrice already crossed SL level
4814
5039
  * - Persists updated signal state (backtest and live modes)
4815
5040
  * - Calls onWrite callback for persistence testing
4816
5041
  *
@@ -4827,17 +5052,21 @@ class ClientStrategy {
4827
5052
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
4828
5053
  * @param currentPrice - Current market price for this partial close (must be in loss direction)
4829
5054
  * @param backtest - Whether running in backtest mode (controls persistence)
4830
- * @returns Promise that resolves when state is updated and persisted
5055
+ * @returns Promise<boolean> - true if partial close was executed, false if skipped
4831
5056
  *
4832
5057
  * @example
4833
5058
  * ```typescript
4834
5059
  * // Close 40% of position at loss (moving toward SL)
4835
- * await strategy.partialLoss("BTCUSDT", 40, 38000, false);
5060
+ * const success1 = await strategy.partialLoss("BTCUSDT", 40, 38000, false);
5061
+ * // success1 = true (executed)
4836
5062
  *
4837
5063
  * // Later close another 30%
4838
- * await strategy.partialLoss("BTCUSDT", 30, 37000, false);
5064
+ * const success2 = await strategy.partialLoss("BTCUSDT", 30, 37000, false);
5065
+ * // success2 = true (executed, total 70% closed)
4839
5066
  *
4840
- * // Final close will calculate weighted PNL from all partials
5067
+ * // Try to close 40% more (would exceed 100%)
5068
+ * const success3 = await strategy.partialLoss("BTCUSDT", 40, 36000, false);
5069
+ * // success3 = false (skipped, would exceed 100%)
4841
5070
  * ```
4842
5071
  */
4843
5072
  async partialLoss(symbol, percentToClose, currentPrice, backtest) {
@@ -4888,7 +5117,7 @@ class ClientStrategy {
4888
5117
  effectiveStopLoss,
4889
5118
  reason: "currentPrice <= effectiveStopLoss (LONG position)"
4890
5119
  });
4891
- return;
5120
+ return false;
4892
5121
  }
4893
5122
  if (this._pendingSignal.position === "short" && currentPrice >= effectiveStopLoss) {
4894
5123
  this.params.logger.debug("ClientStrategy partialLoss: price already at/above SL, skipping partial close", {
@@ -4898,10 +5127,14 @@ class ClientStrategy {
4898
5127
  effectiveStopLoss,
4899
5128
  reason: "currentPrice >= effectiveStopLoss (SHORT position)"
4900
5129
  });
4901
- return;
5130
+ return false;
4902
5131
  }
4903
5132
  // Execute partial close logic
4904
- PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
5133
+ const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
5134
+ // If partial was not executed (exceeded 100%), return false without persistence
5135
+ if (!wasExecuted) {
5136
+ return false;
5137
+ }
4905
5138
  // Persist updated signal state (inline setPendingSignal content)
4906
5139
  // Note: this._pendingSignal already mutated by PARTIAL_LOSS_FN, no reassignment needed
4907
5140
  this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
@@ -4914,6 +5147,7 @@ class ClientStrategy {
4914
5147
  if (!backtest) {
4915
5148
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
4916
5149
  }
5150
+ return true;
4917
5151
  }
4918
5152
  /**
4919
5153
  * Moves stop-loss to breakeven (entry price) when price reaches threshold.
@@ -5398,7 +5632,10 @@ const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
5398
5632
  * // Combine multiple risk profiles
5399
5633
  * const maxPositionsRisk = new MaxPositionsRisk(3);
5400
5634
  * const correlationRisk = new CorrelationRisk(0.7);
5401
- * const mergedRisk = new MergeRisk([maxPositionsRisk, correlationRisk]);
5635
+ * const mergedRisk = new MergeRisk({
5636
+ * "max-positions": maxPositionsRisk,
5637
+ * "correlation": correlationRisk
5638
+ * });
5402
5639
  *
5403
5640
  * // Check if signal passes all risks
5404
5641
  * const canTrade = await mergedRisk.checkSignal({
@@ -5416,10 +5653,10 @@ class MergeRisk {
5416
5653
  /**
5417
5654
  * Creates a merged risk profile from multiple risk instances.
5418
5655
  *
5419
- * @param _riskList - Array of IRisk instances to combine
5656
+ * @param _riskMap - Object mapping RiskName to IRisk instances to combine
5420
5657
  */
5421
- constructor(_riskList) {
5422
- this._riskList = _riskList;
5658
+ constructor(_riskMap) {
5659
+ this._riskMap = _riskMap;
5423
5660
  }
5424
5661
  /**
5425
5662
  * Checks if signal passes all combined risk profiles.
@@ -5434,8 +5671,11 @@ class MergeRisk {
5434
5671
  bt.loggerService.info("MergeRisk checkSignal", {
5435
5672
  params,
5436
5673
  });
5437
- for (const risk of this._riskList) {
5438
- if (await functoolsKit.not(risk.checkSignal(params))) {
5674
+ for (const [riskName, risk] of Object.entries(this._riskMap)) {
5675
+ if (await functoolsKit.not(risk.checkSignal({
5676
+ ...params,
5677
+ riskName,
5678
+ }))) {
5439
5679
  return false;
5440
5680
  }
5441
5681
  }
@@ -5456,7 +5696,7 @@ class MergeRisk {
5456
5696
  symbol,
5457
5697
  context,
5458
5698
  });
5459
- await Promise.all(this._riskList.map(async (risk) => await risk.addSignal(symbol, context, positionData)));
5699
+ await Promise.all(Object.entries(this._riskMap).map(async ([riskName, risk]) => await risk.addSignal(symbol, { ...context, riskName }, positionData)));
5460
5700
  }
5461
5701
  /**
5462
5702
  * Removes a signal from all child risk profiles.
@@ -5473,7 +5713,7 @@ class MergeRisk {
5473
5713
  symbol,
5474
5714
  context,
5475
5715
  });
5476
- await Promise.all(this._riskList.map(async (risk) => await risk.removeSignal(symbol, context)));
5716
+ await Promise.all(Object.entries(this._riskMap).map(async ([riskName, risk]) => await risk.removeSignal(symbol, { ...context, riskName })));
5477
5717
  }
5478
5718
  }
5479
5719
  /**
@@ -5711,13 +5951,22 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
5711
5951
  }
5712
5952
  // Есть только riskList (без riskName)
5713
5953
  if (!hasRiskName && hasRiskList) {
5714
- return new MergeRisk(dto.riskList.map((riskName) => self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest)));
5954
+ return new MergeRisk(dto.riskList.reduce((acc, riskName) => {
5955
+ acc[riskName] = self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest);
5956
+ return acc;
5957
+ }, {}));
5715
5958
  }
5716
5959
  // Есть и riskName, и riskList - объединяем (riskName в начало)
5717
- return new MergeRisk([
5718
- self.riskConnectionService.getRisk(dto.riskName, exchangeName, frameName, backtest),
5719
- ...dto.riskList.map((riskName) => self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest)),
5720
- ]);
5960
+ return new MergeRisk({
5961
+ [dto.riskName]: self.riskConnectionService.getRisk(dto.riskName, exchangeName, frameName, backtest),
5962
+ ...dto.riskList.reduce((acc, riskName) => {
5963
+ if (riskName === dto.riskName) {
5964
+ return acc;
5965
+ }
5966
+ acc[riskName] = self.riskConnectionService.getRisk(riskName, exchangeName, frameName, backtest);
5967
+ return acc;
5968
+ }, {})
5969
+ });
5721
5970
  };
5722
5971
  /**
5723
5972
  * Creates a unique key for memoizing ClientStrategy instances.
@@ -6063,18 +6312,21 @@ class StrategyConnectionService {
6063
6312
  * @param context - Execution context with strategyName, exchangeName, frameName
6064
6313
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
6065
6314
  * @param currentPrice - Current market price for this partial close
6066
- * @returns Promise that resolves when state is updated and persisted
6315
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
6067
6316
  *
6068
6317
  * @example
6069
6318
  * ```typescript
6070
6319
  * // Close 30% of position at profit
6071
- * await strategyConnectionService.partialProfit(
6320
+ * const success = await strategyConnectionService.partialProfit(
6072
6321
  * false,
6073
6322
  * "BTCUSDT",
6074
6323
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
6075
6324
  * 30,
6076
6325
  * 45000
6077
6326
  * );
6327
+ * if (success) {
6328
+ * console.log('Partial profit executed');
6329
+ * }
6078
6330
  * ```
6079
6331
  */
6080
6332
  this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
@@ -6086,7 +6338,7 @@ class StrategyConnectionService {
6086
6338
  backtest,
6087
6339
  });
6088
6340
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
6089
- await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
6341
+ return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
6090
6342
  };
6091
6343
  /**
6092
6344
  * Executes partial close at loss level (moving toward SL).
@@ -6101,18 +6353,21 @@ class StrategyConnectionService {
6101
6353
  * @param context - Execution context with strategyName, exchangeName, frameName
6102
6354
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
6103
6355
  * @param currentPrice - Current market price for this partial close
6104
- * @returns Promise that resolves when state is updated and persisted
6356
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
6105
6357
  *
6106
6358
  * @example
6107
6359
  * ```typescript
6108
6360
  * // Close 40% of position at loss
6109
- * await strategyConnectionService.partialLoss(
6361
+ * const success = await strategyConnectionService.partialLoss(
6110
6362
  * false,
6111
6363
  * "BTCUSDT",
6112
6364
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
6113
6365
  * 40,
6114
6366
  * 38000
6115
6367
  * );
6368
+ * if (success) {
6369
+ * console.log('Partial loss executed');
6370
+ * }
6116
6371
  * ```
6117
6372
  */
6118
6373
  this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
@@ -6124,7 +6379,7 @@ class StrategyConnectionService {
6124
6379
  backtest,
6125
6380
  });
6126
6381
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
6127
- await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
6382
+ return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
6128
6383
  };
6129
6384
  /**
6130
6385
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -6704,6 +6959,9 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
6704
6959
  const TO_RISK_SIGNAL = (signal, currentPrice) => {
6705
6960
  const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
6706
6961
  const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
6962
+ const totalExecuted = ("_partial" in signal && Array.isArray(signal._partial))
6963
+ ? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
6964
+ : 0;
6707
6965
  return {
6708
6966
  ...structuredClone(signal),
6709
6967
  priceOpen: signal.priceOpen ?? currentPrice,
@@ -6711,6 +6969,7 @@ const TO_RISK_SIGNAL = (signal, currentPrice) => {
6711
6969
  priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
6712
6970
  originalPriceStopLoss: signal.priceStopLoss,
6713
6971
  originalPriceTakeProfit: signal.priceTakeProfit,
6972
+ totalExecuted,
6714
6973
  };
6715
6974
  };
6716
6975
  /** Key generator for active position map */
@@ -7554,18 +7813,21 @@ class StrategyCoreService {
7554
7813
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
7555
7814
  * @param currentPrice - Current market price for this partial close (must be in profit direction)
7556
7815
  * @param context - Execution context with strategyName, exchangeName, frameName
7557
- * @returns Promise that resolves when state is updated and persisted
7816
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
7558
7817
  *
7559
7818
  * @example
7560
7819
  * ```typescript
7561
7820
  * // Close 30% of position at profit
7562
- * await strategyCoreService.partialProfit(
7821
+ * const success = await strategyCoreService.partialProfit(
7563
7822
  * false,
7564
7823
  * "BTCUSDT",
7565
7824
  * 30,
7566
7825
  * 45000,
7567
7826
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
7568
7827
  * );
7828
+ * if (success) {
7829
+ * console.log('Partial profit executed');
7830
+ * }
7569
7831
  * ```
7570
7832
  */
7571
7833
  this.partialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
@@ -7592,18 +7854,21 @@ class StrategyCoreService {
7592
7854
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
7593
7855
  * @param currentPrice - Current market price for this partial close (must be in loss direction)
7594
7856
  * @param context - Execution context with strategyName, exchangeName, frameName
7595
- * @returns Promise that resolves when state is updated and persisted
7857
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
7596
7858
  *
7597
7859
  * @example
7598
7860
  * ```typescript
7599
7861
  * // Close 40% of position at loss
7600
- * await strategyCoreService.partialLoss(
7862
+ * const success = await strategyCoreService.partialLoss(
7601
7863
  * false,
7602
7864
  * "BTCUSDT",
7603
7865
  * 40,
7604
7866
  * 38000,
7605
7867
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
7606
7868
  * );
7869
+ * if (success) {
7870
+ * console.log('Partial loss executed');
7871
+ * }
7607
7872
  * ```
7608
7873
  */
7609
7874
  this.partialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
@@ -9038,6 +9303,9 @@ class WalkerLogicPrivateService {
9038
9303
  stoppedCount: stoppedStrategies.size,
9039
9304
  });
9040
9305
  });
9306
+ // Ensure BacktestMarkdownService is subscribed
9307
+ // to enable statistics collection
9308
+ this.backtestMarkdownService.subscribe();
9041
9309
  try {
9042
9310
  // Run backtest for each strategy
9043
9311
  for (const strategyName of strategies) {
@@ -9530,6 +9798,18 @@ const backtest_columns = [
9530
9798
  format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
9531
9799
  isVisible: () => true,
9532
9800
  },
9801
+ {
9802
+ key: "originalPriceTakeProfit",
9803
+ label: "Original TP",
9804
+ format: (data) => `${data.signal.originalPriceTakeProfit.toFixed(8)} USD`,
9805
+ isVisible: () => true,
9806
+ },
9807
+ {
9808
+ key: "originalPriceStopLoss",
9809
+ label: "Original SL",
9810
+ format: (data) => `${data.signal.originalPriceStopLoss.toFixed(8)} USD`,
9811
+ isVisible: () => true,
9812
+ },
9533
9813
  {
9534
9814
  key: "pnl",
9535
9815
  label: "PNL (net)",
@@ -9539,6 +9819,12 @@ const backtest_columns = [
9539
9819
  },
9540
9820
  isVisible: () => true,
9541
9821
  },
9822
+ {
9823
+ key: "totalExecuted",
9824
+ label: "Total Executed",
9825
+ format: (data) => `${data.signal.totalExecuted.toFixed(1)}%`,
9826
+ isVisible: () => true,
9827
+ },
9542
9828
  {
9543
9829
  key: "partialCloses",
9544
9830
  label: "Partial Closes",
@@ -9793,6 +10079,28 @@ const live_columns = [
9793
10079
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
9794
10080
  isVisible: () => true,
9795
10081
  },
10082
+ {
10083
+ key: "originalPriceTakeProfit",
10084
+ label: "Original TP",
10085
+ format: (data) => data.originalPriceTakeProfit !== undefined
10086
+ ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
10087
+ : "N/A",
10088
+ isVisible: () => true,
10089
+ },
10090
+ {
10091
+ key: "originalPriceStopLoss",
10092
+ label: "Original SL",
10093
+ format: (data) => data.originalPriceStopLoss !== undefined
10094
+ ? `${data.originalPriceStopLoss.toFixed(8)} USD`
10095
+ : "N/A",
10096
+ isVisible: () => true,
10097
+ },
10098
+ {
10099
+ key: "totalExecuted",
10100
+ label: "Total Executed",
10101
+ format: (data) => data.totalExecuted !== undefined ? `${data.totalExecuted.toFixed(1)}%` : "N/A",
10102
+ isVisible: () => true,
10103
+ },
9796
10104
  {
9797
10105
  key: "percentTp",
9798
10106
  label: "% to TP",
@@ -10225,6 +10533,30 @@ const risk_columns = [
10225
10533
  : "N/A",
10226
10534
  isVisible: () => true,
10227
10535
  },
10536
+ {
10537
+ key: "originalPriceTakeProfit",
10538
+ label: "Original TP",
10539
+ format: (data) => data.pendingSignal.originalPriceTakeProfit !== undefined
10540
+ ? `${data.pendingSignal.originalPriceTakeProfit.toFixed(8)} USD`
10541
+ : "N/A",
10542
+ isVisible: () => true,
10543
+ },
10544
+ {
10545
+ key: "originalPriceStopLoss",
10546
+ label: "Original SL",
10547
+ format: (data) => data.pendingSignal.originalPriceStopLoss !== undefined
10548
+ ? `${data.pendingSignal.originalPriceStopLoss.toFixed(8)} USD`
10549
+ : "N/A",
10550
+ isVisible: () => true,
10551
+ },
10552
+ {
10553
+ key: "totalExecuted",
10554
+ label: "Total Executed",
10555
+ format: (data) => data.pendingSignal.totalExecuted !== undefined
10556
+ ? `${data.pendingSignal.totalExecuted.toFixed(1)}%`
10557
+ : "N/A",
10558
+ isVisible: () => true,
10559
+ },
10228
10560
  {
10229
10561
  key: "currentPrice",
10230
10562
  label: "Current Price",
@@ -10359,6 +10691,28 @@ const schedule_columns = [
10359
10691
  format: (data) => `${data.stopLoss.toFixed(8)} USD`,
10360
10692
  isVisible: () => true,
10361
10693
  },
10694
+ {
10695
+ key: "originalPriceTakeProfit",
10696
+ label: "Original TP",
10697
+ format: (data) => data.originalPriceTakeProfit !== undefined
10698
+ ? `${data.originalPriceTakeProfit.toFixed(8)} USD`
10699
+ : "N/A",
10700
+ isVisible: () => true,
10701
+ },
10702
+ {
10703
+ key: "originalPriceStopLoss",
10704
+ label: "Original SL",
10705
+ format: (data) => data.originalPriceStopLoss !== undefined
10706
+ ? `${data.originalPriceStopLoss.toFixed(8)} USD`
10707
+ : "N/A",
10708
+ isVisible: () => true,
10709
+ },
10710
+ {
10711
+ key: "totalExecuted",
10712
+ label: "Total Executed",
10713
+ format: (data) => data.totalExecuted !== undefined ? `${data.totalExecuted.toFixed(1)}%` : "N/A",
10714
+ isVisible: () => true,
10715
+ },
10362
10716
  {
10363
10717
  key: "duration",
10364
10718
  label: "Wait Time (min)",
@@ -10604,77 +10958,482 @@ const COLUMN_CONFIG = {
10604
10958
  */
10605
10959
  const DEFAULT_COLUMNS = Object.freeze({ ...COLUMN_CONFIG });
10606
10960
 
10607
- /**
10608
- * Creates a unique key for memoizing ReportStorage instances.
10609
- * Key format: "symbol:strategyName:exchangeName:frameName:backtest" or "symbol:strategyName:exchangeName:live"
10610
- * @param symbol - Trading pair symbol
10611
- * @param strategyName - Name of the strategy
10612
- * @param exchangeName - Exchange name
10613
- * @param frameName - Frame name
10614
- * @param backtest - Whether running in backtest mode
10615
- * @returns Unique string key for memoization
10616
- */
10617
- const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
10618
- const parts = [symbol, strategyName, exchangeName];
10619
- if (frameName)
10620
- parts.push(frameName);
10621
- parts.push(backtest ? "backtest" : "live");
10622
- return parts.join(":");
10961
+ var _a$1, _b$1, _c$1;
10962
+ const MARKDOWN_METHOD_NAME_ENABLE = "MarkdownUtils.enable";
10963
+ const MARKDOWN_METHOD_NAME_USE_ADAPTER = "MarkdownAdapter.useMarkdownAdapter";
10964
+ const WAIT_FOR_INIT_SYMBOL$1 = Symbol("wait-for-init");
10965
+ const WRITE_SAFE_SYMBOL$1 = Symbol("write-safe");
10966
+ /**
10967
+ * Default configuration that enables all markdown services.
10968
+ * Used when no specific configuration is provided to `enable()`.
10969
+ */
10970
+ const WILDCARD_TARGET$1 = {
10971
+ backtest: true,
10972
+ breakeven: true,
10973
+ heat: true,
10974
+ live: true,
10975
+ partial: true,
10976
+ performance: true,
10977
+ risk: true,
10978
+ schedule: true,
10979
+ walker: true,
10623
10980
  };
10624
10981
  /**
10625
- * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
10982
+ * JSONL-based markdown adapter with append-only writes.
10626
10983
  *
10627
- * @param value - Value to check
10628
- * @returns true if value is unsafe, false otherwise
10984
+ * Features:
10985
+ * - Writes markdown reports as JSONL entries to a single file per markdown type
10986
+ * - Stream-based writes with backpressure handling
10987
+ * - 15-second timeout protection for write operations
10988
+ * - Automatic directory creation
10989
+ * - Error handling via exitEmitter
10990
+ * - Search metadata for filtering (symbol, strategy, exchange, frame, signalId)
10991
+ *
10992
+ * File format: ./dump/markdown/{markdownName}.jsonl
10993
+ * Each line contains: markdownName, data, symbol, strategyName, exchangeName, frameName, signalId, timestamp
10994
+ *
10995
+ * Use this adapter for centralized logging and post-processing with JSONL tools.
10996
+ */
10997
+ const MarkdownFileBase = functoolsKit.makeExtendable((_c$1 = class {
10998
+ /**
10999
+ * Creates a new JSONL markdown adapter instance.
11000
+ *
11001
+ * @param markdownName - Type of markdown report (backtest, live, walker, etc.)
11002
+ */
11003
+ constructor(markdownName) {
11004
+ this.markdownName = markdownName;
11005
+ /** WriteStream instance for append-only writes, null until initialized */
11006
+ this._stream = null;
11007
+ /** Base directory for all JSONL markdown files */
11008
+ this._baseDir = path.join(process.cwd(), "./dump/markdown");
11009
+ /**
11010
+ * Singleshot initialization function that creates directory and stream.
11011
+ * Protected by singleshot to ensure one-time execution.
11012
+ * Sets up error handler that emits to exitEmitter.
11013
+ */
11014
+ this[_a$1] = functoolsKit.singleshot(async () => {
11015
+ await fs__namespace.mkdir(this._baseDir, { recursive: true });
11016
+ this._stream = fs$1.createWriteStream(this._filePath, { flags: "a" });
11017
+ this._stream.on('error', (err) => {
11018
+ exitEmitter.next(new Error(`MarkdownFileAdapter stream error for markdownName=${this.markdownName} message=${functoolsKit.getErrorMessage(err)}`));
11019
+ });
11020
+ });
11021
+ /**
11022
+ * Timeout-protected write function with backpressure handling.
11023
+ * Waits for drain event if write buffer is full.
11024
+ * Times out after 15 seconds and returns TIMEOUT_SYMBOL.
11025
+ */
11026
+ this[_b$1] = functoolsKit.timeout(async (line) => {
11027
+ if (!this._stream.write(line)) {
11028
+ await new Promise((resolve) => {
11029
+ this._stream.once('drain', resolve);
11030
+ });
11031
+ }
11032
+ }, 15000);
11033
+ this._filePath = path.join(this._baseDir, `${markdownName}.jsonl`);
11034
+ }
11035
+ /**
11036
+ * Initializes the JSONL file and write stream.
11037
+ * Safe to call multiple times - singleshot ensures one-time execution.
11038
+ *
11039
+ * @returns Promise that resolves when initialization is complete
11040
+ */
11041
+ async waitForInit() {
11042
+ await this[WAIT_FOR_INIT_SYMBOL$1]();
11043
+ }
11044
+ /**
11045
+ * Writes markdown content to JSONL file with metadata.
11046
+ * Appends a single line with JSON object containing:
11047
+ * - markdownName: Type of report
11048
+ * - data: Markdown content
11049
+ * - Search flags: symbol, strategyName, exchangeName, frameName, signalId
11050
+ * - timestamp: Current timestamp in milliseconds
11051
+ *
11052
+ * @param data - Markdown content to write
11053
+ * @param options - Path and metadata options
11054
+ * @throws Error if stream not initialized or write timeout exceeded
11055
+ */
11056
+ async dump(data, options) {
11057
+ bt.loggerService.debug("MarkdownFileAdapter.dump", {
11058
+ markdownName: this.markdownName,
11059
+ options,
11060
+ });
11061
+ if (!this._stream) {
11062
+ throw new Error(`Stream not initialized for markdown ${this.markdownName}. Call waitForInit() first.`);
11063
+ }
11064
+ const searchFlags = {};
11065
+ {
11066
+ if (options.symbol) {
11067
+ searchFlags.symbol = options.symbol;
11068
+ }
11069
+ if (options.strategyName) {
11070
+ searchFlags.strategyName = options.strategyName;
11071
+ }
11072
+ if (options.exchangeName) {
11073
+ searchFlags.exchangeName = options.exchangeName;
11074
+ }
11075
+ if (options.frameName) {
11076
+ searchFlags.frameName = options.frameName;
11077
+ }
11078
+ if (options.signalId) {
11079
+ searchFlags.signalId = options.signalId;
11080
+ }
11081
+ }
11082
+ const line = JSON.stringify({
11083
+ markdownName: this.markdownName,
11084
+ data,
11085
+ ...searchFlags,
11086
+ timestamp: Date.now(),
11087
+ }) + "\n";
11088
+ const status = await this[WRITE_SAFE_SYMBOL$1](line);
11089
+ if (status === functoolsKit.TIMEOUT_SYMBOL) {
11090
+ throw new Error(`Timeout writing to markdown ${this.markdownName}`);
11091
+ }
11092
+ }
11093
+ },
11094
+ _a$1 = WAIT_FOR_INIT_SYMBOL$1,
11095
+ _b$1 = WRITE_SAFE_SYMBOL$1,
11096
+ _c$1));
11097
+ /**
11098
+ * Folder-based markdown adapter with separate files per report.
11099
+ *
11100
+ * Features:
11101
+ * - Writes each markdown report as a separate .md file
11102
+ * - File path based on options.path and options.file
11103
+ * - Automatic directory creation
11104
+ * - No stream management (direct writeFile)
11105
+ * - Suitable for human-readable report directories
11106
+ *
11107
+ * File format: {options.path}/{options.file}
11108
+ * Example: ./dump/backtest/BTCUSDT_my-strategy_binance_2024-Q1_backtest-1736601234567.md
11109
+ *
11110
+ * Use this adapter (default) for organized report directories and manual review.
10629
11111
  */
10630
- function isUnsafe$3(value) {
10631
- if (typeof value !== "number") {
10632
- return true;
11112
+ const MarkdownFolderBase = functoolsKit.makeExtendable(class {
11113
+ /**
11114
+ * Creates a new folder-based markdown adapter instance.
11115
+ *
11116
+ * @param markdownName - Type of markdown report (backtest, live, walker, etc.)
11117
+ */
11118
+ constructor(markdownName) {
11119
+ this.markdownName = markdownName;
10633
11120
  }
10634
- if (isNaN(value)) {
10635
- return true;
11121
+ /**
11122
+ * No-op initialization for folder adapter.
11123
+ * This adapter doesn't need initialization since it uses direct writeFile.
11124
+ *
11125
+ * @returns Promise that resolves immediately
11126
+ */
11127
+ async waitForInit() {
10636
11128
  }
10637
- if (!isFinite(value)) {
10638
- return true;
11129
+ /**
11130
+ * Writes markdown content to a separate file.
11131
+ * Creates directory structure automatically.
11132
+ * File path is determined by options.path and options.file.
11133
+ *
11134
+ * @param content - Markdown content to write
11135
+ * @param options - Path and file options for the dump
11136
+ * @throws Error if directory creation or file write fails
11137
+ */
11138
+ async dump(content, options) {
11139
+ bt.loggerService.debug("MarkdownFolderAdapter.dump", {
11140
+ markdownName: this.markdownName,
11141
+ options,
11142
+ });
11143
+ // Combine into full file path
11144
+ const filePath = path.join(process.cwd(), options.path, options.file);
11145
+ // Extract directory from file path
11146
+ const dir = path.dirname(filePath);
11147
+ await fs__namespace.mkdir(dir, { recursive: true });
11148
+ await fs__namespace.writeFile(filePath, content, "utf8");
10639
11149
  }
10640
- return false;
10641
- }
10642
- /** Maximum number of signals to store in backtest reports */
10643
- const MAX_EVENTS$7 = 250;
11150
+ });
10644
11151
  /**
10645
- * Storage class for accumulating closed signals per strategy.
10646
- * Maintains a list of all closed signals and provides methods to generate reports.
11152
+ * Dummy markdown adapter that discards all writes.
11153
+ * Used for disabling markdown report generation.
10647
11154
  */
10648
- let ReportStorage$6 = class ReportStorage {
10649
- constructor() {
10650
- /** Internal list of all closed signals for this strategy */
10651
- this._signalList = [];
10652
- }
11155
+ class MarkdownDummy {
10653
11156
  /**
10654
- * Adds a closed signal to the storage.
10655
- *
10656
- * @param data - Closed signal data with PNL and close reason
11157
+ * No-op dump function.
11158
+ * @returns Promise that resolves immediately
10657
11159
  */
10658
- addSignal(data) {
10659
- this._signalList.unshift(data);
10660
- // Trim queue if exceeded MAX_EVENTS
10661
- if (this._signalList.length > MAX_EVENTS$7) {
10662
- this._signalList.pop();
10663
- }
11160
+ async dump() {
10664
11161
  }
10665
11162
  /**
10666
- * Calculates statistical data from closed signals (Controller).
10667
- * Returns null for any unsafe numeric values (NaN, Infinity, etc).
10668
- *
10669
- * @returns Statistical data (empty object if no signals)
11163
+ * No-op initialization function.
11164
+ * @returns Promise that resolves immediately
10670
11165
  */
10671
- async getData() {
10672
- if (this._signalList.length === 0) {
10673
- return {
10674
- signalList: [],
10675
- totalSignals: 0,
10676
- winCount: 0,
10677
- lossCount: 0,
11166
+ async waitForInit() {
11167
+ }
11168
+ }
11169
+ /**
11170
+ * Utility class for managing markdown report services.
11171
+ *
11172
+ * Provides methods to enable/disable markdown report generation across
11173
+ * different service types (backtest, live, walker, performance, etc.).
11174
+ *
11175
+ * Typically extended by MarkdownAdapter for additional functionality.
11176
+ */
11177
+ class MarkdownUtils {
11178
+ constructor() {
11179
+ /**
11180
+ * Enables markdown report services selectively.
11181
+ *
11182
+ * Subscribes to specified markdown services and returns a cleanup function
11183
+ * that unsubscribes from all enabled services at once.
11184
+ *
11185
+ * Each enabled service will:
11186
+ * - Start listening to relevant events
11187
+ * - Accumulate data for reports
11188
+ * - Generate markdown files when requested
11189
+ *
11190
+ * IMPORTANT: Always call the returned unsubscribe function to prevent memory leaks.
11191
+ *
11192
+ * @param config - Service configuration object. Defaults to enabling all services.
11193
+ * @param config.backtest - Enable backtest result reports with full trade history
11194
+ * @param config.breakeven - Enable breakeven event tracking (when stop loss moves to entry)
11195
+ * @param config.partial - Enable partial profit/loss event tracking
11196
+ * @param config.heat - Enable portfolio heatmap analysis across all symbols
11197
+ * @param config.walker - Enable walker strategy comparison and optimization reports
11198
+ * @param config.performance - Enable performance bottleneck analysis
11199
+ * @param config.risk - Enable risk rejection tracking (signals blocked by risk limits)
11200
+ * @param config.schedule - Enable scheduled signal tracking (signals waiting for trigger)
11201
+ * @param config.live - Enable live trading event reports (all tick events)
11202
+ *
11203
+ * @returns Cleanup function that unsubscribes from all enabled services
11204
+ */
11205
+ this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
11206
+ bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
11207
+ backtest: bt$1,
11208
+ breakeven,
11209
+ heat,
11210
+ live,
11211
+ partial,
11212
+ performance,
11213
+ risk,
11214
+ schedule,
11215
+ walker,
11216
+ });
11217
+ const unList = [];
11218
+ if (bt$1) {
11219
+ unList.push(bt.backtestMarkdownService.subscribe());
11220
+ }
11221
+ if (breakeven) {
11222
+ unList.push(bt.breakevenMarkdownService.subscribe());
11223
+ }
11224
+ if (heat) {
11225
+ unList.push(bt.heatMarkdownService.subscribe());
11226
+ }
11227
+ if (live) {
11228
+ unList.push(bt.liveMarkdownService.subscribe());
11229
+ }
11230
+ if (partial) {
11231
+ unList.push(bt.partialMarkdownService.subscribe());
11232
+ }
11233
+ if (performance) {
11234
+ unList.push(bt.performanceMarkdownService.subscribe());
11235
+ }
11236
+ if (risk) {
11237
+ unList.push(bt.riskMarkdownService.subscribe());
11238
+ }
11239
+ if (schedule) {
11240
+ unList.push(bt.scheduleMarkdownService.subscribe());
11241
+ }
11242
+ if (walker) {
11243
+ unList.push(bt.walkerMarkdownService.subscribe());
11244
+ }
11245
+ return functoolsKit.compose(...unList.map((un) => () => void un()));
11246
+ };
11247
+ }
11248
+ }
11249
+ /**
11250
+ * Markdown adapter with pluggable storage backend and instance memoization.
11251
+ *
11252
+ * Features:
11253
+ * - Adapter pattern for swappable storage implementations
11254
+ * - Memoized storage instances (one per markdown type)
11255
+ * - Default adapter: MarkdownFolderBase (separate files)
11256
+ * - Alternative adapter: MarkdownFileBase (JSONL append)
11257
+ * - Lazy initialization on first write
11258
+ * - Convenience methods: useMd(), useJsonl()
11259
+ */
11260
+ class MarkdownAdapter extends MarkdownUtils {
11261
+ constructor() {
11262
+ super(...arguments);
11263
+ /**
11264
+ * Current markdown storage adapter constructor.
11265
+ * Defaults to MarkdownFolderBase for separate file storage.
11266
+ * Can be changed via useMarkdownAdapter().
11267
+ */
11268
+ this.MarkdownFactory = MarkdownFolderBase;
11269
+ /**
11270
+ * Memoized storage instances cache.
11271
+ * Key: markdownName (backtest, live, walker, etc.)
11272
+ * Value: TMarkdownBase instance created with current MarkdownFactory.
11273
+ * Ensures single instance per markdown type for the lifetime of the application.
11274
+ */
11275
+ this.getMarkdownStorage = functoolsKit.memoize(([markdownName]) => markdownName, (markdownName) => Reflect.construct(this.MarkdownFactory, [markdownName]));
11276
+ }
11277
+ /**
11278
+ * Sets the markdown storage adapter constructor.
11279
+ * All future markdown instances will use this adapter.
11280
+ *
11281
+ * @param Ctor - Constructor for markdown storage adapter
11282
+ */
11283
+ useMarkdownAdapter(Ctor) {
11284
+ bt.loggerService.info(MARKDOWN_METHOD_NAME_USE_ADAPTER);
11285
+ this.MarkdownFactory = Ctor;
11286
+ }
11287
+ /**
11288
+ * Writes markdown data to storage using the configured adapter.
11289
+ * Automatically initializes storage on first write for each markdown type.
11290
+ *
11291
+ * @param markdownName - Type of markdown report (backtest, live, walker, etc.)
11292
+ * @param content - Markdown content to write
11293
+ * @param options - Path, file, and metadata options
11294
+ * @returns Promise that resolves when write is complete
11295
+ * @throws Error if write fails or storage initialization fails
11296
+ *
11297
+ * @internal - Use service-specific dump methods instead (e.g., Backtest.dump)
11298
+ */
11299
+ async writeData(markdownName, content, options) {
11300
+ bt.loggerService.debug("MarkdownAdapter.writeData", {
11301
+ markdownName,
11302
+ options,
11303
+ });
11304
+ const isInitial = !this.getMarkdownStorage.has(markdownName);
11305
+ const markdown = this.getMarkdownStorage(markdownName);
11306
+ await markdown.waitForInit(isInitial);
11307
+ await markdown.dump(content, options);
11308
+ }
11309
+ /**
11310
+ * Switches to folder-based markdown storage (default).
11311
+ * Shorthand for useMarkdownAdapter(MarkdownFolderBase).
11312
+ * Each dump creates a separate .md file.
11313
+ */
11314
+ useMd() {
11315
+ bt.loggerService.debug("MarkdownAdapter.useMd");
11316
+ this.useMarkdownAdapter(MarkdownFolderBase);
11317
+ }
11318
+ /**
11319
+ * Switches to JSONL-based markdown storage.
11320
+ * Shorthand for useMarkdownAdapter(MarkdownFileBase).
11321
+ * All dumps append to a single .jsonl file per markdown type.
11322
+ */
11323
+ useJsonl() {
11324
+ bt.loggerService.debug("MarkdownAdapter.useJsonl");
11325
+ this.useMarkdownAdapter(MarkdownFileBase);
11326
+ }
11327
+ /**
11328
+ * Switches to a dummy markdown adapter that discards all writes.
11329
+ * All future markdown writes will be no-ops.
11330
+ */
11331
+ useDummy() {
11332
+ bt.loggerService.debug("MarkdownAdapter.useDummy");
11333
+ this.useMarkdownAdapter(MarkdownDummy);
11334
+ }
11335
+ }
11336
+ /**
11337
+ * Global singleton instance of MarkdownAdapter.
11338
+ * Provides markdown report generation with pluggable storage backends.
11339
+ */
11340
+ const Markdown = new MarkdownAdapter();
11341
+
11342
+ /**
11343
+ * Creates a unique key for memoizing ReportStorage instances.
11344
+ * Key format: "symbol:strategyName:exchangeName:frameName:backtest" or "symbol:strategyName:exchangeName:live"
11345
+ * @param symbol - Trading pair symbol
11346
+ * @param strategyName - Name of the strategy
11347
+ * @param exchangeName - Exchange name
11348
+ * @param frameName - Frame name
11349
+ * @param backtest - Whether running in backtest mode
11350
+ * @returns Unique string key for memoization
11351
+ */
11352
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
11353
+ const parts = [symbol, strategyName, exchangeName];
11354
+ if (frameName)
11355
+ parts.push(frameName);
11356
+ parts.push(backtest ? "backtest" : "live");
11357
+ return parts.join(":");
11358
+ };
11359
+ /**
11360
+ * Creates a filename for markdown report based on memoization key components.
11361
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
11362
+ * @param symbol - Trading pair symbol
11363
+ * @param strategyName - Name of the strategy
11364
+ * @param exchangeName - Exchange name
11365
+ * @param frameName - Frame name
11366
+ * @param timestamp - Unix timestamp in milliseconds
11367
+ * @returns Filename string
11368
+ */
11369
+ const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
11370
+ const parts = [symbol, strategyName, exchangeName];
11371
+ if (frameName) {
11372
+ parts.push(frameName);
11373
+ parts.push("backtest");
11374
+ }
11375
+ else
11376
+ parts.push("live");
11377
+ return `${parts.join("_")}-${timestamp}.md`;
11378
+ };
11379
+ /**
11380
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
11381
+ *
11382
+ * @param value - Value to check
11383
+ * @returns true if value is unsafe, false otherwise
11384
+ */
11385
+ function isUnsafe$3(value) {
11386
+ if (typeof value !== "number") {
11387
+ return true;
11388
+ }
11389
+ if (isNaN(value)) {
11390
+ return true;
11391
+ }
11392
+ if (!isFinite(value)) {
11393
+ return true;
11394
+ }
11395
+ return false;
11396
+ }
11397
+ /** Maximum number of signals to store in backtest reports */
11398
+ const MAX_EVENTS$7 = 250;
11399
+ /**
11400
+ * Storage class for accumulating closed signals per strategy.
11401
+ * Maintains a list of all closed signals and provides methods to generate reports.
11402
+ */
11403
+ let ReportStorage$6 = class ReportStorage {
11404
+ constructor(symbol, strategyName, exchangeName, frameName) {
11405
+ this.symbol = symbol;
11406
+ this.strategyName = strategyName;
11407
+ this.exchangeName = exchangeName;
11408
+ this.frameName = frameName;
11409
+ /** Internal list of all closed signals for this strategy */
11410
+ this._signalList = [];
11411
+ }
11412
+ /**
11413
+ * Adds a closed signal to the storage.
11414
+ *
11415
+ * @param data - Closed signal data with PNL and close reason
11416
+ */
11417
+ addSignal(data) {
11418
+ this._signalList.unshift(data);
11419
+ // Trim queue if exceeded MAX_EVENTS
11420
+ if (this._signalList.length > MAX_EVENTS$7) {
11421
+ this._signalList.pop();
11422
+ }
11423
+ }
11424
+ /**
11425
+ * Calculates statistical data from closed signals (Controller).
11426
+ * Returns null for any unsafe numeric values (NaN, Infinity, etc).
11427
+ *
11428
+ * @returns Statistical data (empty object if no signals)
11429
+ */
11430
+ async getData() {
11431
+ if (this._signalList.length === 0) {
11432
+ return {
11433
+ signalList: [],
11434
+ totalSignals: 0,
11435
+ winCount: 0,
11436
+ lossCount: 0,
10678
11437
  winRate: null,
10679
11438
  avgPnl: null,
10680
11439
  totalPnl: null,
@@ -10779,19 +11538,19 @@ let ReportStorage$6 = class ReportStorage {
10779
11538
  * @param path - Directory path to save report (default: "./dump/backtest")
10780
11539
  * @param columns - Column configuration for formatting the table
10781
11540
  */
10782
- async dump(strategyName, path$1 = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
11541
+ async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
10783
11542
  const markdown = await this.getReport(strategyName, columns);
10784
- try {
10785
- const dir = path.join(process.cwd(), path$1);
10786
- await fs.mkdir(dir, { recursive: true });
10787
- const filename = `${strategyName}.md`;
10788
- const filepath = path.join(dir, filename);
10789
- await fs.writeFile(filepath, markdown, "utf-8");
10790
- console.log(`Backtest report saved: ${filepath}`);
10791
- }
10792
- catch (error) {
10793
- console.error(`Failed to save markdown report:`, error);
10794
- }
11543
+ const timestamp = Date.now();
11544
+ const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
11545
+ await Markdown.writeData("backtest", markdown, {
11546
+ path,
11547
+ file: filename,
11548
+ symbol: this.symbol,
11549
+ strategyName: this.strategyName,
11550
+ exchangeName: this.exchangeName,
11551
+ frameName: this.frameName,
11552
+ signalId: "",
11553
+ });
10795
11554
  }
10796
11555
  };
10797
11556
  /**
@@ -10829,7 +11588,7 @@ class BacktestMarkdownService {
10829
11588
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
10830
11589
  * Each combination gets its own isolated storage instance.
10831
11590
  */
10832
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$6());
11591
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
10833
11592
  /**
10834
11593
  * Processes tick events and accumulates closed signals.
10835
11594
  * Should be called from IStrategyCallbacks.onTick.
@@ -10885,6 +11644,9 @@ class BacktestMarkdownService {
10885
11644
  frameName,
10886
11645
  backtest,
10887
11646
  });
11647
+ if (!this.subscribe.hasValue()) {
11648
+ throw new Error("BacktestMarkdownService not initialized. Call subscribe() before getting data.");
11649
+ }
10888
11650
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
10889
11651
  return storage.getData();
10890
11652
  };
@@ -10915,6 +11677,9 @@ class BacktestMarkdownService {
10915
11677
  frameName,
10916
11678
  backtest,
10917
11679
  });
11680
+ if (!this.subscribe.hasValue()) {
11681
+ throw new Error("BacktestMarkdownService not initialized. Call subscribe() before generating reports.");
11682
+ }
10918
11683
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
10919
11684
  return storage.getReport(strategyName, columns);
10920
11685
  };
@@ -10951,6 +11716,9 @@ class BacktestMarkdownService {
10951
11716
  backtest,
10952
11717
  path,
10953
11718
  });
11719
+ if (!this.subscribe.hasValue()) {
11720
+ throw new Error("BacktestMarkdownService not initialized. Call subscribe() before dumping reports.");
11721
+ }
10954
11722
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
10955
11723
  await storage.dump(strategyName, path, columns);
10956
11724
  };
@@ -10985,20 +11753,47 @@ class BacktestMarkdownService {
10985
11753
  }
10986
11754
  };
10987
11755
  /**
10988
- * Initializes the service by subscribing to backtest signal events.
10989
- * Uses singleshot to ensure initialization happens only once.
10990
- * Automatically called on first use.
11756
+ * Subscribes to backtest signal emitter to receive tick events.
11757
+ * Protected against multiple subscriptions.
11758
+ * Returns an unsubscribe function to stop receiving events.
10991
11759
  *
10992
11760
  * @example
10993
11761
  * ```typescript
10994
11762
  * const service = new BacktestMarkdownService();
10995
- * await service.init(); // Subscribe to backtest events
11763
+ * const unsubscribe = service.subscribe();
11764
+ * // ... later
11765
+ * unsubscribe();
10996
11766
  * ```
10997
11767
  */
10998
- this.init = functoolsKit.singleshot(async () => {
11768
+ this.subscribe = functoolsKit.singleshot(() => {
10999
11769
  this.loggerService.log("backtestMarkdownService init");
11000
- this.unsubscribe = signalBacktestEmitter.subscribe(this.tick);
11770
+ const unsubscribe = signalBacktestEmitter.subscribe(this.tick);
11771
+ return () => {
11772
+ this.subscribe.clear();
11773
+ this.clear();
11774
+ unsubscribe();
11775
+ };
11001
11776
  });
11777
+ /**
11778
+ * Unsubscribes from backtest signal emitter to stop receiving tick events.
11779
+ * Calls the unsubscribe function returned by subscribe().
11780
+ * If not subscribed, does nothing.
11781
+ *
11782
+ * @example
11783
+ * ```typescript
11784
+ * const service = new BacktestMarkdownService();
11785
+ * service.subscribe();
11786
+ * // ... later
11787
+ * service.unsubscribe();
11788
+ * ```
11789
+ */
11790
+ this.unsubscribe = async () => {
11791
+ this.loggerService.log("backtestMarkdownService unsubscribe");
11792
+ if (this.subscribe.hasValue()) {
11793
+ const lastSubscription = this.subscribe();
11794
+ lastSubscription();
11795
+ }
11796
+ };
11002
11797
  }
11003
11798
  }
11004
11799
 
@@ -11019,6 +11814,26 @@ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest
11019
11814
  parts.push(backtest ? "backtest" : "live");
11020
11815
  return parts.join(":");
11021
11816
  };
11817
+ /**
11818
+ * Creates a filename for markdown report based on memoization key components.
11819
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
11820
+ * @param symbol - Trading pair symbol
11821
+ * @param strategyName - Name of the strategy
11822
+ * @param exchangeName - Exchange name
11823
+ * @param frameName - Frame name
11824
+ * @param timestamp - Unix timestamp in milliseconds
11825
+ * @returns Filename string
11826
+ */
11827
+ const CREATE_FILE_NAME_FN$7 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
11828
+ const parts = [symbol, strategyName, exchangeName];
11829
+ if (frameName) {
11830
+ parts.push(frameName);
11831
+ parts.push("backtest");
11832
+ }
11833
+ else
11834
+ parts.push("live");
11835
+ return `${parts.join("_")}-${timestamp}.md`;
11836
+ };
11022
11837
  /**
11023
11838
  * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
11024
11839
  *
@@ -11044,7 +11859,11 @@ const MAX_EVENTS$6 = 250;
11044
11859
  * Maintains a chronological list of all events (idle, opened, active, closed).
11045
11860
  */
11046
11861
  let ReportStorage$5 = class ReportStorage {
11047
- constructor() {
11862
+ constructor(symbol, strategyName, exchangeName, frameName) {
11863
+ this.symbol = symbol;
11864
+ this.strategyName = strategyName;
11865
+ this.exchangeName = exchangeName;
11866
+ this.frameName = frameName;
11048
11867
  /** Internal list of all tick events for this strategy */
11049
11868
  this._eventList = [];
11050
11869
  }
@@ -11093,6 +11912,9 @@ let ReportStorage$5 = class ReportStorage {
11093
11912
  openPrice: data.signal.priceOpen,
11094
11913
  takeProfit: data.signal.priceTakeProfit,
11095
11914
  stopLoss: data.signal.priceStopLoss,
11915
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
11916
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
11917
+ totalExecuted: data.signal.totalExecuted,
11096
11918
  });
11097
11919
  // Trim queue if exceeded MAX_EVENTS
11098
11920
  if (this._eventList.length > MAX_EVENTS$6) {
@@ -11117,8 +11939,12 @@ let ReportStorage$5 = class ReportStorage {
11117
11939
  openPrice: data.signal.priceOpen,
11118
11940
  takeProfit: data.signal.priceTakeProfit,
11119
11941
  stopLoss: data.signal.priceStopLoss,
11942
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
11943
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
11944
+ totalExecuted: data.signal.totalExecuted,
11120
11945
  percentTp: data.percentTp,
11121
11946
  percentSl: data.percentSl,
11947
+ pnl: data.pnl.pnlPercentage,
11122
11948
  };
11123
11949
  // Find the last active event with the same signalId
11124
11950
  const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
@@ -11153,6 +11979,9 @@ let ReportStorage$5 = class ReportStorage {
11153
11979
  openPrice: data.signal.priceOpen,
11154
11980
  takeProfit: data.signal.priceTakeProfit,
11155
11981
  stopLoss: data.signal.priceStopLoss,
11982
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
11983
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
11984
+ totalExecuted: data.signal.totalExecuted,
11156
11985
  pnl: data.pnl.pnlPercentage,
11157
11986
  closeReason: data.closeReason,
11158
11987
  duration: durationMin,
@@ -11295,19 +12124,19 @@ let ReportStorage$5 = class ReportStorage {
11295
12124
  * @param path - Directory path to save report (default: "./dump/live")
11296
12125
  * @param columns - Column configuration for formatting the table
11297
12126
  */
11298
- async dump(strategyName, path$1 = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
12127
+ async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
11299
12128
  const markdown = await this.getReport(strategyName, columns);
11300
- try {
11301
- const dir = path.join(process.cwd(), path$1);
11302
- await fs.mkdir(dir, { recursive: true });
11303
- const filename = `${strategyName}.md`;
11304
- const filepath = path.join(dir, filename);
11305
- await fs.writeFile(filepath, markdown, "utf-8");
11306
- console.log(`Live trading report saved: ${filepath}`);
11307
- }
11308
- catch (error) {
11309
- console.error(`Failed to save markdown report:`, error);
11310
- }
12129
+ const timestamp = Date.now();
12130
+ const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
12131
+ await Markdown.writeData("live", markdown, {
12132
+ path,
12133
+ signalId: "",
12134
+ file: filename,
12135
+ symbol: this.symbol,
12136
+ strategyName: this.strategyName,
12137
+ exchangeName: this.exchangeName,
12138
+ frameName: this.frameName
12139
+ });
11311
12140
  }
11312
12141
  };
11313
12142
  /**
@@ -11348,7 +12177,49 @@ class LiveMarkdownService {
11348
12177
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
11349
12178
  * Each combination gets its own isolated storage instance.
11350
12179
  */
11351
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$5());
12180
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
12181
+ /**
12182
+ * Subscribes to live signal emitter to receive tick events.
12183
+ * Protected against multiple subscriptions.
12184
+ * Returns an unsubscribe function to stop receiving events.
12185
+ *
12186
+ * @example
12187
+ * ```typescript
12188
+ * const service = new LiveMarkdownService();
12189
+ * const unsubscribe = service.subscribe();
12190
+ * // ... later
12191
+ * unsubscribe();
12192
+ * ```
12193
+ */
12194
+ this.subscribe = functoolsKit.singleshot(() => {
12195
+ this.loggerService.log("liveMarkdownService init");
12196
+ const unsubscribe = signalLiveEmitter.subscribe(this.tick);
12197
+ return () => {
12198
+ this.subscribe.clear();
12199
+ this.clear();
12200
+ unsubscribe();
12201
+ };
12202
+ });
12203
+ /**
12204
+ * Unsubscribes from live signal emitter to stop receiving tick events.
12205
+ * Calls the unsubscribe function returned by subscribe().
12206
+ * If not subscribed, does nothing.
12207
+ *
12208
+ * @example
12209
+ * ```typescript
12210
+ * const service = new LiveMarkdownService();
12211
+ * service.subscribe();
12212
+ * // ... later
12213
+ * service.unsubscribe();
12214
+ * ```
12215
+ */
12216
+ this.unsubscribe = async () => {
12217
+ this.loggerService.log("liveMarkdownService unsubscribe");
12218
+ if (this.subscribe.hasValue()) {
12219
+ const lastSubscription = this.subscribe();
12220
+ lastSubscription();
12221
+ }
12222
+ };
11352
12223
  /**
11353
12224
  * Processes tick events and accumulates all event types.
11354
12225
  * Should be called from IStrategyCallbacks.onTick.
@@ -11414,6 +12285,9 @@ class LiveMarkdownService {
11414
12285
  frameName,
11415
12286
  backtest,
11416
12287
  });
12288
+ if (!this.subscribe.hasValue()) {
12289
+ throw new Error("LiveMarkdownService not initialized. Call subscribe() before getting data.");
12290
+ }
11417
12291
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11418
12292
  return storage.getData();
11419
12293
  };
@@ -11444,6 +12318,9 @@ class LiveMarkdownService {
11444
12318
  frameName,
11445
12319
  backtest,
11446
12320
  });
12321
+ if (!this.subscribe.hasValue()) {
12322
+ throw new Error("LiveMarkdownService not initialized. Call subscribe() before generating reports.");
12323
+ }
11447
12324
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11448
12325
  return storage.getReport(strategyName, columns);
11449
12326
  };
@@ -11480,6 +12357,9 @@ class LiveMarkdownService {
11480
12357
  backtest,
11481
12358
  path,
11482
12359
  });
12360
+ if (!this.subscribe.hasValue()) {
12361
+ throw new Error("LiveMarkdownService not initialized. Call subscribe() before dumping reports.");
12362
+ }
11483
12363
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11484
12364
  await storage.dump(strategyName, path, columns);
11485
12365
  };
@@ -11513,21 +12393,6 @@ class LiveMarkdownService {
11513
12393
  this.getStorage.clear();
11514
12394
  }
11515
12395
  };
11516
- /**
11517
- * Initializes the service by subscribing to live signal events.
11518
- * Uses singleshot to ensure initialization happens only once.
11519
- * Automatically called on first use.
11520
- *
11521
- * @example
11522
- * ```typescript
11523
- * const service = new LiveMarkdownService();
11524
- * await service.init(); // Subscribe to live events
11525
- * ```
11526
- */
11527
- this.init = functoolsKit.singleshot(async () => {
11528
- this.loggerService.log("liveMarkdownService init");
11529
- this.unsubscribe = signalLiveEmitter.subscribe(this.tick);
11530
- });
11531
12396
  }
11532
12397
  }
11533
12398
 
@@ -11548,6 +12413,26 @@ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest
11548
12413
  parts.push(backtest ? "backtest" : "live");
11549
12414
  return parts.join(":");
11550
12415
  };
12416
+ /**
12417
+ * Creates a filename for markdown report based on memoization key components.
12418
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
12419
+ * @param symbol - Trading pair symbol
12420
+ * @param strategyName - Name of the strategy
12421
+ * @param exchangeName - Exchange name
12422
+ * @param frameName - Frame name
12423
+ * @param timestamp - Unix timestamp in milliseconds
12424
+ * @returns Filename string
12425
+ */
12426
+ const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
12427
+ const parts = [symbol, strategyName, exchangeName];
12428
+ if (frameName) {
12429
+ parts.push(frameName);
12430
+ parts.push("backtest");
12431
+ }
12432
+ else
12433
+ parts.push("live");
12434
+ return `${parts.join("_")}-${timestamp}.md`;
12435
+ };
11551
12436
  /** Maximum number of events to store in schedule reports */
11552
12437
  const MAX_EVENTS$5 = 250;
11553
12438
  /**
@@ -11555,7 +12440,11 @@ const MAX_EVENTS$5 = 250;
11555
12440
  * Maintains a chronological list of scheduled and cancelled events.
11556
12441
  */
11557
12442
  let ReportStorage$4 = class ReportStorage {
11558
- constructor() {
12443
+ constructor(symbol, strategyName, exchangeName, frameName) {
12444
+ this.symbol = symbol;
12445
+ this.strategyName = strategyName;
12446
+ this.exchangeName = exchangeName;
12447
+ this.frameName = frameName;
11559
12448
  /** Internal list of all scheduled events for this strategy */
11560
12449
  this._eventList = [];
11561
12450
  }
@@ -11576,6 +12465,9 @@ let ReportStorage$4 = class ReportStorage {
11576
12465
  priceOpen: data.signal.priceOpen,
11577
12466
  takeProfit: data.signal.priceTakeProfit,
11578
12467
  stopLoss: data.signal.priceStopLoss,
12468
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12469
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
12470
+ totalExecuted: data.signal.totalExecuted,
11579
12471
  });
11580
12472
  // Trim queue if exceeded MAX_EVENTS
11581
12473
  if (this._eventList.length > MAX_EVENTS$5) {
@@ -11601,6 +12493,9 @@ let ReportStorage$4 = class ReportStorage {
11601
12493
  priceOpen: data.signal.priceOpen,
11602
12494
  takeProfit: data.signal.priceTakeProfit,
11603
12495
  stopLoss: data.signal.priceStopLoss,
12496
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12497
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
12498
+ totalExecuted: data.signal.totalExecuted,
11604
12499
  duration: durationMin,
11605
12500
  };
11606
12501
  this._eventList.unshift(newEvent);
@@ -11628,6 +12523,9 @@ let ReportStorage$4 = class ReportStorage {
11628
12523
  priceOpen: data.signal.priceOpen,
11629
12524
  takeProfit: data.signal.priceTakeProfit,
11630
12525
  stopLoss: data.signal.priceStopLoss,
12526
+ originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12527
+ originalPriceStopLoss: data.signal.originalPriceStopLoss,
12528
+ totalExecuted: data.signal.totalExecuted,
11631
12529
  closeTimestamp: data.closeTimestamp,
11632
12530
  duration: durationMin,
11633
12531
  cancelReason: data.reason,
@@ -11739,19 +12637,19 @@ let ReportStorage$4 = class ReportStorage {
11739
12637
  * @param path - Directory path to save report (default: "./dump/schedule")
11740
12638
  * @param columns - Column configuration for formatting the table
11741
12639
  */
11742
- async dump(strategyName, path$1 = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
12640
+ async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
11743
12641
  const markdown = await this.getReport(strategyName, columns);
11744
- try {
11745
- const dir = path.join(process.cwd(), path$1);
11746
- await fs.mkdir(dir, { recursive: true });
11747
- const filename = `${strategyName}.md`;
11748
- const filepath = path.join(dir, filename);
11749
- await fs.writeFile(filepath, markdown, "utf-8");
11750
- console.log(`Scheduled signals report saved: ${filepath}`);
11751
- }
11752
- catch (error) {
11753
- console.error(`Failed to save markdown report:`, error);
11754
- }
12642
+ const timestamp = Date.now();
12643
+ const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
12644
+ await Markdown.writeData("schedule", markdown, {
12645
+ path,
12646
+ file: filename,
12647
+ symbol: this.symbol,
12648
+ signalId: "",
12649
+ strategyName: this.strategyName,
12650
+ exchangeName: this.exchangeName,
12651
+ frameName: this.frameName
12652
+ });
11755
12653
  }
11756
12654
  };
11757
12655
  /**
@@ -11783,28 +12681,70 @@ class ScheduleMarkdownService {
11783
12681
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
11784
12682
  * Each combination gets its own isolated storage instance.
11785
12683
  */
11786
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$4());
12684
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
11787
12685
  /**
11788
- * Processes tick events and accumulates scheduled/opened/cancelled events.
11789
- * Should be called from signalEmitter subscription.
11790
- *
11791
- * Processes only scheduled, opened and cancelled event types.
11792
- *
11793
- * @param data - Tick result from strategy execution with frameName wrapper
12686
+ * Subscribes to signal emitter to receive scheduled signal events.
12687
+ * Protected against multiple subscriptions.
12688
+ * Returns an unsubscribe function to stop receiving events.
11794
12689
  *
11795
12690
  * @example
11796
12691
  * ```typescript
11797
12692
  * const service = new ScheduleMarkdownService();
11798
- * // Service automatically subscribes in init()
12693
+ * const unsubscribe = service.subscribe();
12694
+ * // ... later
12695
+ * unsubscribe();
11799
12696
  * ```
11800
12697
  */
11801
- this.tick = async (data) => {
11802
- this.loggerService.log("scheduleMarkdownService tick", {
11803
- data,
11804
- });
11805
- const storage = this.getStorage(data.symbol, data.strategyName, data.exchangeName, data.frameName, data.backtest);
11806
- if (data.action === "scheduled") {
11807
- storage.addScheduledEvent(data);
12698
+ this.subscribe = functoolsKit.singleshot(() => {
12699
+ this.loggerService.log("scheduleMarkdownService init");
12700
+ const unsubscribe = signalEmitter.subscribe(this.tick);
12701
+ return () => {
12702
+ this.subscribe.clear();
12703
+ this.clear();
12704
+ unsubscribe();
12705
+ };
12706
+ });
12707
+ /**
12708
+ * Unsubscribes from signal emitter to stop receiving scheduled signal events.
12709
+ * Calls the unsubscribe function returned by subscribe().
12710
+ * If not subscribed, does nothing.
12711
+ *
12712
+ * @example
12713
+ * ```typescript
12714
+ * const service = new ScheduleMarkdownService();
12715
+ * service.subscribe();
12716
+ * // ... later
12717
+ * service.unsubscribe();
12718
+ * ```
12719
+ */
12720
+ this.unsubscribe = async () => {
12721
+ this.loggerService.log("scheduleMarkdownService unsubscribe");
12722
+ if (this.subscribe.hasValue()) {
12723
+ const lastSubscription = this.subscribe();
12724
+ lastSubscription();
12725
+ }
12726
+ };
12727
+ /**
12728
+ * Processes tick events and accumulates scheduled/opened/cancelled events.
12729
+ * Should be called from signalEmitter subscription.
12730
+ *
12731
+ * Processes only scheduled, opened and cancelled event types.
12732
+ *
12733
+ * @param data - Tick result from strategy execution with frameName wrapper
12734
+ *
12735
+ * @example
12736
+ * ```typescript
12737
+ * const service = new ScheduleMarkdownService();
12738
+ * // Service automatically subscribes in init()
12739
+ * ```
12740
+ */
12741
+ this.tick = async (data) => {
12742
+ this.loggerService.log("scheduleMarkdownService tick", {
12743
+ data,
12744
+ });
12745
+ const storage = this.getStorage(data.symbol, data.strategyName, data.exchangeName, data.frameName, data.backtest);
12746
+ if (data.action === "scheduled") {
12747
+ storage.addScheduledEvent(data);
11808
12748
  }
11809
12749
  else if (data.action === "opened") {
11810
12750
  // Check if this opened signal was previously scheduled
@@ -11843,6 +12783,9 @@ class ScheduleMarkdownService {
11843
12783
  frameName,
11844
12784
  backtest,
11845
12785
  });
12786
+ if (!this.subscribe.hasValue()) {
12787
+ throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before getting data.");
12788
+ }
11846
12789
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11847
12790
  return storage.getData();
11848
12791
  };
@@ -11873,6 +12816,9 @@ class ScheduleMarkdownService {
11873
12816
  frameName,
11874
12817
  backtest,
11875
12818
  });
12819
+ if (!this.subscribe.hasValue()) {
12820
+ throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before generating reports.");
12821
+ }
11876
12822
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11877
12823
  return storage.getReport(strategyName, columns);
11878
12824
  };
@@ -11909,6 +12855,9 @@ class ScheduleMarkdownService {
11909
12855
  backtest,
11910
12856
  path,
11911
12857
  });
12858
+ if (!this.subscribe.hasValue()) {
12859
+ throw new Error("ScheduleMarkdownService not initialized. Call subscribe() before dumping reports.");
12860
+ }
11912
12861
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
11913
12862
  await storage.dump(strategyName, path, columns);
11914
12863
  };
@@ -11942,21 +12891,6 @@ class ScheduleMarkdownService {
11942
12891
  this.getStorage.clear();
11943
12892
  }
11944
12893
  };
11945
- /**
11946
- * Initializes the service by subscribing to live signal events.
11947
- * Uses singleshot to ensure initialization happens only once.
11948
- * Automatically called on first use.
11949
- *
11950
- * @example
11951
- * ```typescript
11952
- * const service = new ScheduleMarkdownService();
11953
- * await service.init(); // Subscribe to live events
11954
- * ```
11955
- */
11956
- this.init = functoolsKit.singleshot(async () => {
11957
- this.loggerService.log("scheduleMarkdownService init");
11958
- this.unsubscribe = signalEmitter.subscribe(this.tick);
11959
- });
11960
12894
  }
11961
12895
  }
11962
12896
 
@@ -11977,6 +12911,26 @@ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest
11977
12911
  parts.push(backtest ? "backtest" : "live");
11978
12912
  return parts.join(":");
11979
12913
  };
12914
+ /**
12915
+ * Creates a filename for markdown report based on memoization key components.
12916
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
12917
+ * @param symbol - Trading pair symbol
12918
+ * @param strategyName - Name of the strategy
12919
+ * @param exchangeName - Exchange name
12920
+ * @param frameName - Frame name
12921
+ * @param timestamp - Unix timestamp in milliseconds
12922
+ * @returns Filename string
12923
+ */
12924
+ const CREATE_FILE_NAME_FN$5 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
12925
+ const parts = [symbol, strategyName, exchangeName];
12926
+ if (frameName) {
12927
+ parts.push(frameName);
12928
+ parts.push("backtest");
12929
+ }
12930
+ else
12931
+ parts.push("live");
12932
+ return `${parts.join("_")}-${timestamp}.md`;
12933
+ };
11980
12934
  /**
11981
12935
  * Calculates percentile value from sorted array.
11982
12936
  */
@@ -11993,7 +12947,11 @@ const MAX_EVENTS$4 = 10000;
11993
12947
  * Maintains a list of all performance events and provides aggregated statistics.
11994
12948
  */
11995
12949
  class PerformanceStorage {
11996
- constructor() {
12950
+ constructor(symbol, strategyName, exchangeName, frameName) {
12951
+ this.symbol = symbol;
12952
+ this.strategyName = strategyName;
12953
+ this.exchangeName = exchangeName;
12954
+ this.frameName = frameName;
11997
12955
  /** Internal list of all performance events for this strategy */
11998
12956
  this._events = [];
11999
12957
  }
@@ -12144,19 +13102,19 @@ class PerformanceStorage {
12144
13102
  * @param path - Directory path to save report
12145
13103
  * @param columns - Column configuration for formatting the table
12146
13104
  */
12147
- async dump(strategyName, path$1 = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
13105
+ async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
12148
13106
  const markdown = await this.getReport(strategyName, columns);
12149
- try {
12150
- const dir = path.join(process.cwd(), path$1);
12151
- await fs.mkdir(dir, { recursive: true });
12152
- const filename = `${strategyName}.md`;
12153
- const filepath = path.join(dir, filename);
12154
- await fs.writeFile(filepath, markdown, "utf-8");
12155
- console.log(`Performance report saved: ${filepath}`);
12156
- }
12157
- catch (error) {
12158
- console.error(`Failed to save performance report:`, error);
12159
- }
13107
+ const timestamp = Date.now();
13108
+ const filename = CREATE_FILE_NAME_FN$5(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
13109
+ await Markdown.writeData("performance", markdown, {
13110
+ path,
13111
+ file: filename,
13112
+ symbol: this.symbol,
13113
+ signalId: "",
13114
+ strategyName: this.strategyName,
13115
+ exchangeName: this.exchangeName,
13116
+ frameName: this.frameName
13117
+ });
12160
13118
  }
12161
13119
  }
12162
13120
  /**
@@ -12194,7 +13152,49 @@ class PerformanceMarkdownService {
12194
13152
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
12195
13153
  * Each combination gets its own isolated storage instance.
12196
13154
  */
12197
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), () => new PerformanceStorage());
13155
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
13156
+ /**
13157
+ * Subscribes to performance emitter to receive performance events.
13158
+ * Protected against multiple subscriptions.
13159
+ * Returns an unsubscribe function to stop receiving events.
13160
+ *
13161
+ * @example
13162
+ * ```typescript
13163
+ * const service = new PerformanceMarkdownService();
13164
+ * const unsubscribe = service.subscribe();
13165
+ * // ... later
13166
+ * unsubscribe();
13167
+ * ```
13168
+ */
13169
+ this.subscribe = functoolsKit.singleshot(() => {
13170
+ this.loggerService.log("performanceMarkdownService init");
13171
+ const unsubscribe = performanceEmitter.subscribe(this.track);
13172
+ return () => {
13173
+ this.subscribe.clear();
13174
+ this.clear();
13175
+ unsubscribe();
13176
+ };
13177
+ });
13178
+ /**
13179
+ * Unsubscribes from performance emitter to stop receiving events.
13180
+ * Calls the unsubscribe function returned by subscribe().
13181
+ * If not subscribed, does nothing.
13182
+ *
13183
+ * @example
13184
+ * ```typescript
13185
+ * const service = new PerformanceMarkdownService();
13186
+ * service.subscribe();
13187
+ * // ... later
13188
+ * service.unsubscribe();
13189
+ * ```
13190
+ */
13191
+ this.unsubscribe = async () => {
13192
+ this.loggerService.log("performanceMarkdownService unsubscribe");
13193
+ if (this.subscribe.hasValue()) {
13194
+ const lastSubscription = this.subscribe();
13195
+ lastSubscription();
13196
+ }
13197
+ };
12198
13198
  /**
12199
13199
  * Processes performance events and accumulates metrics.
12200
13200
  * Should be called from performance tracking code.
@@ -12237,6 +13237,9 @@ class PerformanceMarkdownService {
12237
13237
  frameName,
12238
13238
  backtest,
12239
13239
  });
13240
+ if (!this.subscribe.hasValue()) {
13241
+ throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before getting data.");
13242
+ }
12240
13243
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
12241
13244
  return storage.getData(strategyName);
12242
13245
  };
@@ -12265,6 +13268,9 @@ class PerformanceMarkdownService {
12265
13268
  frameName,
12266
13269
  backtest,
12267
13270
  });
13271
+ if (!this.subscribe.hasValue()) {
13272
+ throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before generating reports.");
13273
+ }
12268
13274
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
12269
13275
  return storage.getReport(strategyName, columns);
12270
13276
  };
@@ -12297,6 +13303,9 @@ class PerformanceMarkdownService {
12297
13303
  backtest,
12298
13304
  path,
12299
13305
  });
13306
+ if (!this.subscribe.hasValue()) {
13307
+ throw new Error("PerformanceMarkdownService not initialized. Call subscribe() before dumping reports.");
13308
+ }
12300
13309
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
12301
13310
  await storage.dump(strategyName, path, columns);
12302
13311
  };
@@ -12317,17 +13326,16 @@ class PerformanceMarkdownService {
12317
13326
  this.getStorage.clear();
12318
13327
  }
12319
13328
  };
12320
- /**
12321
- * Initializes the service by subscribing to performance events.
12322
- * Uses singleshot to ensure initialization happens only once.
12323
- */
12324
- this.init = functoolsKit.singleshot(async () => {
12325
- this.loggerService.log("performanceMarkdownService init");
12326
- this.unsubscribe = performanceEmitter.subscribe(this.track);
12327
- });
12328
13329
  }
12329
13330
  }
12330
13331
 
13332
+ /**
13333
+ * Creates a filename for markdown report based on walker name.
13334
+ * Filename format: "walkerName-timestamp.md"
13335
+ */
13336
+ const CREATE_FILE_NAME_FN$4 = (walkerName, timestamp) => {
13337
+ return `${walkerName}-${timestamp}.md`;
13338
+ };
12331
13339
  /**
12332
13340
  * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
12333
13341
  */
@@ -12545,19 +13553,19 @@ let ReportStorage$3 = class ReportStorage {
12545
13553
  * @param strategyColumns - Column configuration for strategy comparison table
12546
13554
  * @param pnlColumns - Column configuration for PNL table
12547
13555
  */
12548
- async dump(symbol, metric, context, path$1 = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
13556
+ async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
12549
13557
  const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
12550
- try {
12551
- const dir = path.join(process.cwd(), path$1);
12552
- await fs.mkdir(dir, { recursive: true });
12553
- const filename = `${this.walkerName}.md`;
12554
- const filepath = path.join(dir, filename);
12555
- await fs.writeFile(filepath, markdown, "utf-8");
12556
- console.log(`Walker report saved: ${filepath}`);
12557
- }
12558
- catch (error) {
12559
- console.error(`Failed to save walker report:`, error);
12560
- }
13558
+ const timestamp = Date.now();
13559
+ const filename = CREATE_FILE_NAME_FN$4(this.walkerName, timestamp);
13560
+ await Markdown.writeData("walker", markdown, {
13561
+ path,
13562
+ file: filename,
13563
+ symbol: "",
13564
+ signalId: "",
13565
+ strategyName: "",
13566
+ exchangeName: "",
13567
+ frameName: ""
13568
+ });
12561
13569
  }
12562
13570
  };
12563
13571
  /**
@@ -12585,6 +13593,48 @@ class WalkerMarkdownService {
12585
13593
  * Each walker gets its own isolated storage instance.
12586
13594
  */
12587
13595
  this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$3(walkerName));
13596
+ /**
13597
+ * Subscribes to walker emitter to receive walker progress events.
13598
+ * Protected against multiple subscriptions.
13599
+ * Returns an unsubscribe function to stop receiving events.
13600
+ *
13601
+ * @example
13602
+ * ```typescript
13603
+ * const service = new WalkerMarkdownService();
13604
+ * const unsubscribe = service.subscribe();
13605
+ * // ... later
13606
+ * unsubscribe();
13607
+ * ```
13608
+ */
13609
+ this.subscribe = functoolsKit.singleshot(() => {
13610
+ this.loggerService.log("walkerMarkdownService init");
13611
+ const unsubscribe = walkerEmitter.subscribe(this.tick);
13612
+ return () => {
13613
+ this.subscribe.clear();
13614
+ this.clear();
13615
+ unsubscribe();
13616
+ };
13617
+ });
13618
+ /**
13619
+ * Unsubscribes from walker emitter to stop receiving events.
13620
+ * Calls the unsubscribe function returned by subscribe().
13621
+ * If not subscribed, does nothing.
13622
+ *
13623
+ * @example
13624
+ * ```typescript
13625
+ * const service = new WalkerMarkdownService();
13626
+ * service.subscribe();
13627
+ * // ... later
13628
+ * service.unsubscribe();
13629
+ * ```
13630
+ */
13631
+ this.unsubscribe = async () => {
13632
+ this.loggerService.log("walkerMarkdownService unsubscribe");
13633
+ if (this.subscribe.hasValue()) {
13634
+ const lastSubscription = this.subscribe();
13635
+ lastSubscription();
13636
+ }
13637
+ };
12588
13638
  /**
12589
13639
  * Processes walker progress events and accumulates strategy results.
12590
13640
  * Should be called from walkerEmitter.
@@ -12628,6 +13678,9 @@ class WalkerMarkdownService {
12628
13678
  metric,
12629
13679
  context,
12630
13680
  });
13681
+ if (!this.subscribe.hasValue()) {
13682
+ throw new Error("WalkerMarkdownService not initialized. Call subscribe() before getting data.");
13683
+ }
12631
13684
  const storage = this.getStorage(walkerName);
12632
13685
  return storage.getData(symbol, metric, context);
12633
13686
  };
@@ -12657,6 +13710,9 @@ class WalkerMarkdownService {
12657
13710
  metric,
12658
13711
  context,
12659
13712
  });
13713
+ if (!this.subscribe.hasValue()) {
13714
+ throw new Error("WalkerMarkdownService not initialized. Call subscribe() before generating reports.");
13715
+ }
12660
13716
  const storage = this.getStorage(walkerName);
12661
13717
  return storage.getReport(symbol, metric, context, strategyColumns, pnlColumns);
12662
13718
  };
@@ -12692,6 +13748,9 @@ class WalkerMarkdownService {
12692
13748
  context,
12693
13749
  path,
12694
13750
  });
13751
+ if (!this.subscribe.hasValue()) {
13752
+ throw new Error("WalkerMarkdownService not initialized. Call subscribe() before dumping reports.");
13753
+ }
12695
13754
  const storage = this.getStorage(walkerName);
12696
13755
  await storage.dump(symbol, metric, context, path, strategyColumns, pnlColumns);
12697
13756
  };
@@ -12719,21 +13778,6 @@ class WalkerMarkdownService {
12719
13778
  });
12720
13779
  this.getStorage.clear(walkerName);
12721
13780
  };
12722
- /**
12723
- * Initializes the service by subscribing to walker events.
12724
- * Uses singleshot to ensure initialization happens only once.
12725
- * Automatically called on first use.
12726
- *
12727
- * @example
12728
- * ```typescript
12729
- * const service = new WalkerMarkdownService();
12730
- * await service.init(); // Subscribe to walker events
12731
- * ```
12732
- */
12733
- this.init = functoolsKit.singleshot(async () => {
12734
- this.loggerService.log("walkerMarkdownService init");
12735
- this.unsubscribe = walkerEmitter.subscribe(this.tick);
12736
- });
12737
13781
  }
12738
13782
  }
12739
13783
 
@@ -12752,6 +13796,20 @@ const CREATE_KEY_FN$7 = (exchangeName, frameName, backtest) => {
12752
13796
  parts.push(backtest ? "backtest" : "live");
12753
13797
  return parts.join(":");
12754
13798
  };
13799
+ /**
13800
+ * Creates a filename for markdown report based on memoization key components.
13801
+ * Filename format: "strategyName_exchangeName_frameName-timestamp.md"
13802
+ */
13803
+ const CREATE_FILE_NAME_FN$3 = (strategyName, exchangeName, frameName, timestamp) => {
13804
+ const parts = [strategyName, exchangeName];
13805
+ if (frameName) {
13806
+ parts.push(frameName);
13807
+ parts.push("backtest");
13808
+ }
13809
+ else
13810
+ parts.push("live");
13811
+ return `${parts.join("_")}-${timestamp}.md`;
13812
+ };
12755
13813
  const HEATMAP_METHOD_NAME_GET_DATA = "HeatMarkdownService.getData";
12756
13814
  const HEATMAP_METHOD_NAME_GET_REPORT = "HeatMarkdownService.getReport";
12757
13815
  const HEATMAP_METHOD_NAME_DUMP = "HeatMarkdownService.dump";
@@ -12781,7 +13839,10 @@ const MAX_EVENTS$3 = 250;
12781
13839
  * Maintains symbol-level statistics and provides portfolio-wide metrics.
12782
13840
  */
12783
13841
  class HeatmapStorage {
12784
- constructor() {
13842
+ constructor(exchangeName, frameName, backtest) {
13843
+ this.exchangeName = exchangeName;
13844
+ this.frameName = frameName;
13845
+ this.backtest = backtest;
12785
13846
  /** Internal storage of closed signals per symbol */
12786
13847
  this.symbolData = new Map();
12787
13848
  }
@@ -13046,19 +14107,19 @@ class HeatmapStorage {
13046
14107
  * @param path - Directory path to save report (default: "./dump/heatmap")
13047
14108
  * @param columns - Column configuration for formatting the table
13048
14109
  */
13049
- async dump(strategyName, path$1 = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
14110
+ async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
13050
14111
  const markdown = await this.getReport(strategyName, columns);
13051
- try {
13052
- const dir = path.join(process.cwd(), path$1);
13053
- await fs.mkdir(dir, { recursive: true });
13054
- const filename = `${strategyName}.md`;
13055
- const filepath = path.join(dir, filename);
13056
- await fs.writeFile(filepath, markdown, "utf-8");
13057
- console.log(`Heatmap report saved: ${filepath}`);
13058
- }
13059
- catch (error) {
13060
- console.error(`Failed to save heatmap report:`, error);
13061
- }
14112
+ const timestamp = Date.now();
14113
+ const filename = CREATE_FILE_NAME_FN$3(strategyName, this.exchangeName, this.frameName, timestamp);
14114
+ await Markdown.writeData("heat", markdown, {
14115
+ path,
14116
+ file: filename,
14117
+ symbol: "",
14118
+ strategyName: "",
14119
+ signalId: "",
14120
+ exchangeName: this.exchangeName,
14121
+ frameName: this.frameName
14122
+ });
13062
14123
  }
13063
14124
  }
13064
14125
  /**
@@ -13095,7 +14156,49 @@ class HeatMarkdownService {
13095
14156
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
13096
14157
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
13097
14158
  */
13098
- this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(exchangeName, frameName, backtest), () => new HeatmapStorage());
14159
+ this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
14160
+ /**
14161
+ * Subscribes to signal emitter to receive tick events.
14162
+ * Protected against multiple subscriptions.
14163
+ * Returns an unsubscribe function to stop receiving events.
14164
+ *
14165
+ * @example
14166
+ * ```typescript
14167
+ * const service = new HeatMarkdownService();
14168
+ * const unsubscribe = service.subscribe();
14169
+ * // ... later
14170
+ * unsubscribe();
14171
+ * ```
14172
+ */
14173
+ this.subscribe = functoolsKit.singleshot(() => {
14174
+ this.loggerService.log("heatMarkdownService init");
14175
+ const unsubscribe = signalEmitter.subscribe(this.tick);
14176
+ return () => {
14177
+ this.subscribe.clear();
14178
+ this.clear();
14179
+ unsubscribe();
14180
+ };
14181
+ });
14182
+ /**
14183
+ * Unsubscribes from signal emitter to stop receiving tick events.
14184
+ * Calls the unsubscribe function returned by subscribe().
14185
+ * If not subscribed, does nothing.
14186
+ *
14187
+ * @example
14188
+ * ```typescript
14189
+ * const service = new HeatMarkdownService();
14190
+ * service.subscribe();
14191
+ * // ... later
14192
+ * service.unsubscribe();
14193
+ * ```
14194
+ */
14195
+ this.unsubscribe = async () => {
14196
+ this.loggerService.log("heatMarkdownService unsubscribe");
14197
+ if (this.subscribe.hasValue()) {
14198
+ const lastSubscription = this.subscribe();
14199
+ lastSubscription();
14200
+ }
14201
+ };
13099
14202
  /**
13100
14203
  * Processes tick events and accumulates closed signals.
13101
14204
  * Should be called from signal emitter subscription.
@@ -13141,6 +14244,9 @@ class HeatMarkdownService {
13141
14244
  frameName,
13142
14245
  backtest,
13143
14246
  });
14247
+ if (!this.subscribe.hasValue()) {
14248
+ throw new Error("HeatMarkdownService not initialized. Call subscribe() before getting data.");
14249
+ }
13144
14250
  const storage = this.getStorage(exchangeName, frameName, backtest);
13145
14251
  return storage.getData();
13146
14252
  };
@@ -13178,6 +14284,9 @@ class HeatMarkdownService {
13178
14284
  frameName,
13179
14285
  backtest,
13180
14286
  });
14287
+ if (!this.subscribe.hasValue()) {
14288
+ throw new Error("HeatMarkdownService not initialized. Call subscribe() before generating reports.");
14289
+ }
13181
14290
  const storage = this.getStorage(exchangeName, frameName, backtest);
13182
14291
  return storage.getReport(strategyName, columns);
13183
14292
  };
@@ -13213,6 +14322,9 @@ class HeatMarkdownService {
13213
14322
  backtest,
13214
14323
  path,
13215
14324
  });
14325
+ if (!this.subscribe.hasValue()) {
14326
+ throw new Error("HeatMarkdownService not initialized. Call subscribe() before dumping reports.");
14327
+ }
13216
14328
  const storage = this.getStorage(exchangeName, frameName, backtest);
13217
14329
  await storage.dump(strategyName, path, columns);
13218
14330
  };
@@ -13246,21 +14358,6 @@ class HeatMarkdownService {
13246
14358
  this.getStorage.clear();
13247
14359
  }
13248
14360
  };
13249
- /**
13250
- * Initializes the service by subscribing to signal events.
13251
- * Uses singleshot to ensure initialization happens only once.
13252
- * Automatically called on first use.
13253
- *
13254
- * @example
13255
- * ```typescript
13256
- * const service = new HeatMarkdownService();
13257
- * await service.init(); // Subscribe to signal events
13258
- * ```
13259
- */
13260
- this.init = functoolsKit.singleshot(async () => {
13261
- this.loggerService.log("heatMarkdownService init");
13262
- this.unsubscribe = signalEmitter.subscribe(this.tick);
13263
- });
13264
14361
  }
13265
14362
  }
13266
14363
 
@@ -15677,6 +16774,20 @@ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest
15677
16774
  parts.push(backtest ? "backtest" : "live");
15678
16775
  return parts.join(":");
15679
16776
  };
16777
+ /**
16778
+ * Creates a filename for markdown report based on memoization key components.
16779
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
16780
+ */
16781
+ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
16782
+ const parts = [symbol, strategyName, exchangeName];
16783
+ if (frameName) {
16784
+ parts.push(frameName);
16785
+ parts.push("backtest");
16786
+ }
16787
+ else
16788
+ parts.push("live");
16789
+ return `${parts.join("_")}-${timestamp}.md`;
16790
+ };
15680
16791
  /** Maximum number of events to store in partial reports */
15681
16792
  const MAX_EVENTS$2 = 250;
15682
16793
  /**
@@ -15684,7 +16795,11 @@ const MAX_EVENTS$2 = 250;
15684
16795
  * Maintains a chronological list of profit and loss level events.
15685
16796
  */
15686
16797
  let ReportStorage$2 = class ReportStorage {
15687
- constructor() {
16798
+ constructor(symbol, strategyName, exchangeName, frameName) {
16799
+ this.symbol = symbol;
16800
+ this.strategyName = strategyName;
16801
+ this.exchangeName = exchangeName;
16802
+ this.frameName = frameName;
15688
16803
  /** Internal list of all partial events for this symbol */
15689
16804
  this._eventList = [];
15690
16805
  }
@@ -15807,19 +16922,19 @@ let ReportStorage$2 = class ReportStorage {
15807
16922
  * @param path - Directory path to save report (default: "./dump/partial")
15808
16923
  * @param columns - Column configuration for formatting the table
15809
16924
  */
15810
- async dump(symbol, strategyName, path$1 = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
16925
+ async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
15811
16926
  const markdown = await this.getReport(symbol, strategyName, columns);
15812
- try {
15813
- const dir = path.join(process.cwd(), path$1);
15814
- await fs.mkdir(dir, { recursive: true });
15815
- const filename = `${symbol}_${strategyName}.md`;
15816
- const filepath = path.join(dir, filename);
15817
- await fs.writeFile(filepath, markdown, "utf-8");
15818
- console.log(`Partial profit/loss report saved: ${filepath}`);
15819
- }
15820
- catch (error) {
15821
- console.error(`Failed to save markdown report:`, error);
15822
- }
16927
+ const timestamp = Date.now();
16928
+ const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
16929
+ await Markdown.writeData("partial", markdown, {
16930
+ path,
16931
+ file: filename,
16932
+ symbol: this.symbol,
16933
+ signalId: "",
16934
+ strategyName: this.strategyName,
16935
+ exchangeName: this.exchangeName,
16936
+ frameName: this.frameName
16937
+ });
15823
16938
  }
15824
16939
  };
15825
16940
  /**
@@ -15851,7 +16966,51 @@ class PartialMarkdownService {
15851
16966
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
15852
16967
  * Each combination gets its own isolated storage instance.
15853
16968
  */
15854
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$2());
16969
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
16970
+ /**
16971
+ * Subscribes to partial profit/loss signal emitters to receive events.
16972
+ * Protected against multiple subscriptions.
16973
+ * Returns an unsubscribe function to stop receiving events.
16974
+ *
16975
+ * @example
16976
+ * ```typescript
16977
+ * const service = new PartialMarkdownService();
16978
+ * const unsubscribe = service.subscribe();
16979
+ * // ... later
16980
+ * unsubscribe();
16981
+ * ```
16982
+ */
16983
+ this.subscribe = functoolsKit.singleshot(() => {
16984
+ this.loggerService.log("partialMarkdownService init");
16985
+ const unProfit = partialProfitSubject.subscribe(this.tickProfit);
16986
+ const unLoss = partialLossSubject.subscribe(this.tickLoss);
16987
+ return () => {
16988
+ this.subscribe.clear();
16989
+ this.clear();
16990
+ unProfit();
16991
+ unLoss();
16992
+ };
16993
+ });
16994
+ /**
16995
+ * Unsubscribes from partial profit/loss signal emitters to stop receiving events.
16996
+ * Calls the unsubscribe function returned by subscribe().
16997
+ * If not subscribed, does nothing.
16998
+ *
16999
+ * @example
17000
+ * ```typescript
17001
+ * const service = new PartialMarkdownService();
17002
+ * service.subscribe();
17003
+ * // ... later
17004
+ * service.unsubscribe();
17005
+ * ```
17006
+ */
17007
+ this.unsubscribe = async () => {
17008
+ this.loggerService.log("partialMarkdownService unsubscribe");
17009
+ if (this.subscribe.hasValue()) {
17010
+ const lastSubscription = this.subscribe();
17011
+ lastSubscription();
17012
+ }
17013
+ };
15855
17014
  /**
15856
17015
  * Processes profit events and accumulates them.
15857
17016
  * Should be called from partialProfitSubject subscription.
@@ -15916,6 +17075,9 @@ class PartialMarkdownService {
15916
17075
  frameName,
15917
17076
  backtest,
15918
17077
  });
17078
+ if (!this.subscribe.hasValue()) {
17079
+ throw new Error("PartialMarkdownService not initialized. Call subscribe() before getting data.");
17080
+ }
15919
17081
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
15920
17082
  return storage.getData();
15921
17083
  };
@@ -15946,6 +17108,9 @@ class PartialMarkdownService {
15946
17108
  frameName,
15947
17109
  backtest,
15948
17110
  });
17111
+ if (!this.subscribe.hasValue()) {
17112
+ throw new Error("PartialMarkdownService not initialized. Call subscribe() before generating reports.");
17113
+ }
15949
17114
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
15950
17115
  return storage.getReport(symbol, strategyName, columns);
15951
17116
  };
@@ -15982,6 +17147,9 @@ class PartialMarkdownService {
15982
17147
  backtest,
15983
17148
  path,
15984
17149
  });
17150
+ if (!this.subscribe.hasValue()) {
17151
+ throw new Error("PartialMarkdownService not initialized. Call subscribe() before dumping reports.");
17152
+ }
15985
17153
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
15986
17154
  await storage.dump(symbol, strategyName, path, columns);
15987
17155
  };
@@ -16015,23 +17183,6 @@ class PartialMarkdownService {
16015
17183
  this.getStorage.clear();
16016
17184
  }
16017
17185
  };
16018
- /**
16019
- * Initializes the service by subscribing to partial profit/loss events.
16020
- * Uses singleshot to ensure initialization happens only once.
16021
- * Automatically called on first use.
16022
- *
16023
- * @example
16024
- * ```typescript
16025
- * const service = new PartialMarkdownService();
16026
- * await service.init(); // Subscribe to profit/loss events
16027
- * ```
16028
- */
16029
- this.init = functoolsKit.singleshot(async () => {
16030
- this.loggerService.log("partialMarkdownService init");
16031
- const unProfit = partialProfitSubject.subscribe(this.tickProfit);
16032
- const unLoss = partialLossSubject.subscribe(this.tickLoss);
16033
- this.unsubscribe = functoolsKit.compose(() => unProfit(), () => unLoss());
16034
- });
16035
17186
  }
16036
17187
  }
16037
17188
 
@@ -16715,6 +17866,20 @@ const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest
16715
17866
  parts.push(backtest ? "backtest" : "live");
16716
17867
  return parts.join(":");
16717
17868
  };
17869
+ /**
17870
+ * Creates a filename for markdown report based on memoization key components.
17871
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
17872
+ */
17873
+ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
17874
+ const parts = [symbol, strategyName, exchangeName];
17875
+ if (frameName) {
17876
+ parts.push(frameName);
17877
+ parts.push("backtest");
17878
+ }
17879
+ else
17880
+ parts.push("live");
17881
+ return `${parts.join("_")}-${timestamp}.md`;
17882
+ };
16718
17883
  /** Maximum number of events to store in breakeven reports */
16719
17884
  const MAX_EVENTS$1 = 250;
16720
17885
  /**
@@ -16722,7 +17887,11 @@ const MAX_EVENTS$1 = 250;
16722
17887
  * Maintains a chronological list of breakeven events.
16723
17888
  */
16724
17889
  let ReportStorage$1 = class ReportStorage {
16725
- constructor() {
17890
+ constructor(symbol, strategyName, exchangeName, frameName) {
17891
+ this.symbol = symbol;
17892
+ this.strategyName = strategyName;
17893
+ this.exchangeName = exchangeName;
17894
+ this.frameName = frameName;
16726
17895
  /** Internal list of all breakeven events for this symbol */
16727
17896
  this._eventList = [];
16728
17897
  }
@@ -16811,19 +17980,19 @@ let ReportStorage$1 = class ReportStorage {
16811
17980
  * @param path - Directory path to save report (default: "./dump/breakeven")
16812
17981
  * @param columns - Column configuration for formatting the table
16813
17982
  */
16814
- async dump(symbol, strategyName, path$1 = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
17983
+ async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
16815
17984
  const markdown = await this.getReport(symbol, strategyName, columns);
16816
- try {
16817
- const dir = path.join(process.cwd(), path$1);
16818
- await fs.mkdir(dir, { recursive: true });
16819
- const filename = `${symbol}_${strategyName}.md`;
16820
- const filepath = path.join(dir, filename);
16821
- await fs.writeFile(filepath, markdown, "utf-8");
16822
- console.log(`Breakeven report saved: ${filepath}`);
16823
- }
16824
- catch (error) {
16825
- console.error(`Failed to save markdown report:`, error);
16826
- }
17985
+ const timestamp = Date.now();
17986
+ const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
17987
+ await Markdown.writeData("breakeven", markdown, {
17988
+ path,
17989
+ file: filename,
17990
+ symbol: this.symbol,
17991
+ strategyName: this.strategyName,
17992
+ exchangeName: this.exchangeName,
17993
+ signalId: "",
17994
+ frameName: this.frameName
17995
+ });
16827
17996
  }
16828
17997
  };
16829
17998
  /**
@@ -16855,7 +18024,49 @@ class BreakevenMarkdownService {
16855
18024
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
16856
18025
  * Each combination gets its own isolated storage instance.
16857
18026
  */
16858
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage$1());
18027
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
18028
+ /**
18029
+ * Subscribes to breakeven signal emitter to receive events.
18030
+ * Protected against multiple subscriptions.
18031
+ * Returns an unsubscribe function to stop receiving events.
18032
+ *
18033
+ * @example
18034
+ * ```typescript
18035
+ * const service = new BreakevenMarkdownService();
18036
+ * const unsubscribe = service.subscribe();
18037
+ * // ... later
18038
+ * unsubscribe();
18039
+ * ```
18040
+ */
18041
+ this.subscribe = functoolsKit.singleshot(() => {
18042
+ this.loggerService.log("breakevenMarkdownService init");
18043
+ const unBreakeven = breakevenSubject.subscribe(this.tickBreakeven);
18044
+ return () => {
18045
+ this.subscribe.clear();
18046
+ this.clear();
18047
+ unBreakeven();
18048
+ };
18049
+ });
18050
+ /**
18051
+ * Unsubscribes from breakeven signal emitter to stop receiving events.
18052
+ * Calls the unsubscribe function returned by subscribe().
18053
+ * If not subscribed, does nothing.
18054
+ *
18055
+ * @example
18056
+ * ```typescript
18057
+ * const service = new BreakevenMarkdownService();
18058
+ * service.subscribe();
18059
+ * // ... later
18060
+ * service.unsubscribe();
18061
+ * ```
18062
+ */
18063
+ this.unsubscribe = async () => {
18064
+ this.loggerService.log("breakevenMarkdownService unsubscribe");
18065
+ if (this.subscribe.hasValue()) {
18066
+ const lastSubscription = this.subscribe();
18067
+ lastSubscription();
18068
+ }
18069
+ };
16859
18070
  /**
16860
18071
  * Processes breakeven events and accumulates them.
16861
18072
  * Should be called from breakevenSubject subscription.
@@ -16901,6 +18112,9 @@ class BreakevenMarkdownService {
16901
18112
  frameName,
16902
18113
  backtest,
16903
18114
  });
18115
+ if (!this.subscribe.hasValue()) {
18116
+ throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before getting data.");
18117
+ }
16904
18118
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
16905
18119
  return storage.getData();
16906
18120
  };
@@ -16931,6 +18145,9 @@ class BreakevenMarkdownService {
16931
18145
  frameName,
16932
18146
  backtest,
16933
18147
  });
18148
+ if (!this.subscribe.hasValue()) {
18149
+ throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before generating reports.");
18150
+ }
16934
18151
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
16935
18152
  return storage.getReport(symbol, strategyName, columns);
16936
18153
  };
@@ -16967,6 +18184,9 @@ class BreakevenMarkdownService {
16967
18184
  backtest,
16968
18185
  path,
16969
18186
  });
18187
+ if (!this.subscribe.hasValue()) {
18188
+ throw new Error("BreakevenMarkdownService not initialized. Call subscribe() before dumping reports.");
18189
+ }
16970
18190
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
16971
18191
  await storage.dump(symbol, strategyName, path, columns);
16972
18192
  };
@@ -17000,22 +18220,6 @@ class BreakevenMarkdownService {
17000
18220
  this.getStorage.clear();
17001
18221
  }
17002
18222
  };
17003
- /**
17004
- * Initializes the service by subscribing to breakeven events.
17005
- * Uses singleshot to ensure initialization happens only once.
17006
- * Automatically called on first use.
17007
- *
17008
- * @example
17009
- * ```typescript
17010
- * const service = new BreakevenMarkdownService();
17011
- * await service.init(); // Subscribe to breakeven events
17012
- * ```
17013
- */
17014
- this.init = functoolsKit.singleshot(async () => {
17015
- this.loggerService.log("breakevenMarkdownService init");
17016
- const unBreakeven = breakevenSubject.subscribe(this.tickBreakeven);
17017
- this.unsubscribe = functoolsKit.compose(() => unBreakeven());
17018
- });
17019
18223
  }
17020
18224
  }
17021
18225
 
@@ -17169,18 +18373,12 @@ const WARN_KB = 30;
17169
18373
  * @param outputDir - Output directory path (default: "./dump/strategy")
17170
18374
  * @returns Promise that resolves when all files are written
17171
18375
  */
17172
- const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/strategy") => {
18376
+ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/outline") => {
17173
18377
  // Extract system messages and system reminders from existing data
17174
18378
  const systemMessages = history.filter((m) => m.role === "system");
17175
18379
  const userMessages = history.filter((m) => m.role === "user");
17176
18380
  const subfolderPath = path.join(outputDir, String(signalId));
17177
- try {
17178
- await fs$1.promises.access(subfolderPath);
17179
- return;
17180
- }
17181
- catch {
17182
- await fs$1.promises.mkdir(subfolderPath, { recursive: true });
17183
- }
18381
+ // Generate system prompt markdown
17184
18382
  {
17185
18383
  let summary = "# Outline Result Summary\n";
17186
18384
  {
@@ -17203,14 +18401,21 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
17203
18401
  summary += "\n";
17204
18402
  });
17205
18403
  }
17206
- const summaryFile = path.join(subfolderPath, "00_system_prompt.md");
17207
- await fs$1.promises.writeFile(summaryFile, summary, "utf8");
18404
+ await Markdown.writeData("outline", summary, {
18405
+ path: subfolderPath,
18406
+ file: "00_system_prompt.md",
18407
+ symbol: "",
18408
+ signalId: String(signalId),
18409
+ strategyName: "",
18410
+ exchangeName: "",
18411
+ frameName: ""
18412
+ });
17208
18413
  }
18414
+ // Generate user messages
17209
18415
  {
17210
18416
  await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
17211
18417
  const messageNum = String(idx + 1).padStart(2, "0");
17212
18418
  const contentFileName = `${messageNum}_user_message.md`;
17213
- const contentFilePath = path.join(subfolderPath, contentFileName);
17214
18419
  {
17215
18420
  const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
17216
18421
  const messageSizeKb = Math.floor(messageSizeBytes / 1024);
@@ -17222,13 +18427,21 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
17222
18427
  content += `**ResultId**: ${String(signalId)}\n\n`;
17223
18428
  content += message.content;
17224
18429
  content += "\n";
17225
- await fs$1.promises.writeFile(contentFilePath, content, "utf8");
18430
+ await Markdown.writeData("outline", content, {
18431
+ path: subfolderPath,
18432
+ file: contentFileName,
18433
+ signalId: String(signalId),
18434
+ symbol: "",
18435
+ strategyName: "",
18436
+ exchangeName: "",
18437
+ frameName: ""
18438
+ });
17226
18439
  }));
17227
18440
  }
18441
+ // Generate LLM output
17228
18442
  {
17229
18443
  const messageNum = String(userMessages.length + 1).padStart(2, "0");
17230
18444
  const contentFileName = `${messageNum}_llm_output.md`;
17231
- const contentFilePath = path.join(subfolderPath, contentFileName);
17232
18445
  let content = "# Full Outline Result\n\n";
17233
18446
  content += `**ResultId**: ${String(signalId)}\n\n`;
17234
18447
  if (signal) {
@@ -17237,7 +18450,15 @@ const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/str
17237
18450
  content += JSON.stringify(signal, null, 2);
17238
18451
  content += "\n```\n";
17239
18452
  }
17240
- await fs$1.promises.writeFile(contentFilePath, content, "utf8");
18453
+ await Markdown.writeData("outline", content, {
18454
+ path: subfolderPath,
18455
+ file: contentFileName,
18456
+ symbol: "",
18457
+ signalId: String(signalId),
18458
+ strategyName: "",
18459
+ exchangeName: "",
18460
+ frameName: ""
18461
+ });
17241
18462
  }
17242
18463
  };
17243
18464
  /**
@@ -17303,7 +18524,7 @@ class OutlineMarkdownService {
17303
18524
  * (slippage + fees) to guarantee profitable trades when TakeProfit is hit
17304
18525
  * - **Range constraints**: Validates MIN < MAX relationships (e.g., StopLoss distances)
17305
18526
  * - **Time-based parameters**: Ensures positive integer values for timeouts and lifetimes
17306
- * - **Candle parameters**: Validates retry counts, delays, and anomaly detection thresholds
18527
+ * - **Candle parameters**: Validates retry counts, delays, anomaly detection thresholds, and max candles per request
17307
18528
  *
17308
18529
  * @throws {Error} If any validation fails, throws with detailed breakdown of all errors
17309
18530
  *
@@ -17412,6 +18633,9 @@ class ConfigValidationService {
17412
18633
  if (!Number.isInteger(GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) || GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN <= 0) {
17413
18634
  errors.push(`CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN must be a positive integer, got ${GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN}`);
17414
18635
  }
18636
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) || GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST <= 0) {
18637
+ errors.push(`CC_MAX_CANDLES_PER_REQUEST must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST}`);
18638
+ }
17415
18639
  // Throw aggregated errors if any
17416
18640
  if (errors.length > 0) {
17417
18641
  const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
@@ -17440,6 +18664,20 @@ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest
17440
18664
  parts.push(backtest ? "backtest" : "live");
17441
18665
  return parts.join(":");
17442
18666
  };
18667
+ /**
18668
+ * Creates a filename for markdown report based on memoization key components.
18669
+ * Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
18670
+ */
18671
+ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
18672
+ const parts = [symbol, strategyName, exchangeName];
18673
+ if (frameName) {
18674
+ parts.push(frameName);
18675
+ parts.push("backtest");
18676
+ }
18677
+ else
18678
+ parts.push("live");
18679
+ return `${parts.join("_")}-${timestamp}.md`;
18680
+ };
17443
18681
  /** Maximum number of events to store in risk reports */
17444
18682
  const MAX_EVENTS = 250;
17445
18683
  /**
@@ -17447,7 +18685,11 @@ const MAX_EVENTS = 250;
17447
18685
  * Maintains a chronological list of rejected signals due to risk limits.
17448
18686
  */
17449
18687
  class ReportStorage {
17450
- constructor() {
18688
+ constructor(symbol, strategyName, exchangeName, frameName) {
18689
+ this.symbol = symbol;
18690
+ this.strategyName = strategyName;
18691
+ this.exchangeName = exchangeName;
18692
+ this.frameName = frameName;
17451
18693
  /** Internal list of all risk rejection events for this symbol */
17452
18694
  this._eventList = [];
17453
18695
  }
@@ -17540,19 +18782,19 @@ class ReportStorage {
17540
18782
  * @param path - Directory path to save report (default: "./dump/risk")
17541
18783
  * @param columns - Column configuration for formatting the table
17542
18784
  */
17543
- async dump(symbol, strategyName, path$1 = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
18785
+ async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
17544
18786
  const markdown = await this.getReport(symbol, strategyName, columns);
17545
- try {
17546
- const dir = path.join(process.cwd(), path$1);
17547
- await fs.mkdir(dir, { recursive: true });
17548
- const filename = `${symbol}_${strategyName}.md`;
17549
- const filepath = path.join(dir, filename);
17550
- await fs.writeFile(filepath, markdown, "utf-8");
17551
- console.log(`Risk rejection report saved: ${filepath}`);
17552
- }
17553
- catch (error) {
17554
- console.error(`Failed to save markdown report:`, error);
17555
- }
18787
+ const timestamp = Date.now();
18788
+ const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
18789
+ await Markdown.writeData("risk", markdown, {
18790
+ path,
18791
+ file: filename,
18792
+ symbol: this.symbol,
18793
+ signalId: "",
18794
+ strategyName: this.strategyName,
18795
+ exchangeName: this.exchangeName,
18796
+ frameName: this.frameName
18797
+ });
17556
18798
  }
17557
18799
  }
17558
18800
  /**
@@ -17584,7 +18826,49 @@ class RiskMarkdownService {
17584
18826
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
17585
18827
  * Each combination gets its own isolated storage instance.
17586
18828
  */
17587
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), () => new ReportStorage());
18829
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
18830
+ /**
18831
+ * Subscribes to risk rejection emitter to receive rejection events.
18832
+ * Protected against multiple subscriptions.
18833
+ * Returns an unsubscribe function to stop receiving events.
18834
+ *
18835
+ * @example
18836
+ * ```typescript
18837
+ * const service = new RiskMarkdownService();
18838
+ * const unsubscribe = service.subscribe();
18839
+ * // ... later
18840
+ * unsubscribe();
18841
+ * ```
18842
+ */
18843
+ this.subscribe = functoolsKit.singleshot(() => {
18844
+ this.loggerService.log("riskMarkdownService init");
18845
+ const unsubscribe = riskSubject.subscribe(this.tickRejection);
18846
+ return () => {
18847
+ this.subscribe.clear();
18848
+ this.clear();
18849
+ unsubscribe();
18850
+ };
18851
+ });
18852
+ /**
18853
+ * Unsubscribes from risk rejection emitter to stop receiving events.
18854
+ * Calls the unsubscribe function returned by subscribe().
18855
+ * If not subscribed, does nothing.
18856
+ *
18857
+ * @example
18858
+ * ```typescript
18859
+ * const service = new RiskMarkdownService();
18860
+ * service.subscribe();
18861
+ * // ... later
18862
+ * service.unsubscribe();
18863
+ * ```
18864
+ */
18865
+ this.unsubscribe = async () => {
18866
+ this.loggerService.log("riskMarkdownService unsubscribe");
18867
+ if (this.subscribe.hasValue()) {
18868
+ const lastSubscription = this.subscribe();
18869
+ lastSubscription();
18870
+ }
18871
+ };
17588
18872
  /**
17589
18873
  * Processes risk rejection events and accumulates them.
17590
18874
  * Should be called from riskSubject subscription.
@@ -17630,6 +18914,9 @@ class RiskMarkdownService {
17630
18914
  frameName,
17631
18915
  backtest,
17632
18916
  });
18917
+ if (!this.subscribe.hasValue()) {
18918
+ throw new Error("RiskMarkdownService not initialized. Call subscribe() before getting data.");
18919
+ }
17633
18920
  const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
17634
18921
  return storage.getData();
17635
18922
  };
@@ -17660,201 +18947,1219 @@ class RiskMarkdownService {
17660
18947
  frameName,
17661
18948
  backtest,
17662
18949
  });
17663
- const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
17664
- return storage.getReport(symbol, strategyName, columns);
18950
+ if (!this.subscribe.hasValue()) {
18951
+ throw new Error("RiskMarkdownService not initialized. Call subscribe() before generating reports.");
18952
+ }
18953
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
18954
+ return storage.getReport(symbol, strategyName, columns);
18955
+ };
18956
+ /**
18957
+ * Saves symbol-strategy report to disk.
18958
+ * Creates directory if it doesn't exist.
18959
+ * Delegates to ReportStorage.dump().
18960
+ *
18961
+ * @param symbol - Trading pair symbol to save report for
18962
+ * @param strategyName - Strategy name to save report for
18963
+ * @param exchangeName - Exchange name
18964
+ * @param frameName - Frame name
18965
+ * @param backtest - True if backtest mode, false if live mode
18966
+ * @param path - Directory path to save report (default: "./dump/risk")
18967
+ * @param columns - Column configuration for formatting the table
18968
+ *
18969
+ * @example
18970
+ * ```typescript
18971
+ * const service = new RiskMarkdownService();
18972
+ *
18973
+ * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
18974
+ * await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false);
18975
+ *
18976
+ * // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
18977
+ * await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false, "./custom/path");
18978
+ * ```
18979
+ */
18980
+ this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) => {
18981
+ this.loggerService.log("riskMarkdownService dump", {
18982
+ symbol,
18983
+ strategyName,
18984
+ exchangeName,
18985
+ frameName,
18986
+ backtest,
18987
+ path,
18988
+ });
18989
+ if (!this.subscribe.hasValue()) {
18990
+ throw new Error("RiskMarkdownService not initialized. Call subscribe() before dumping reports.");
18991
+ }
18992
+ const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
18993
+ await storage.dump(symbol, strategyName, path, columns);
18994
+ };
18995
+ /**
18996
+ * Clears accumulated event data from storage.
18997
+ * If payload is provided, clears only that specific symbol-strategy-exchange-frame-backtest combination's data.
18998
+ * If nothing is provided, clears all data.
18999
+ *
19000
+ * @param payload - Optional payload with symbol, strategyName, exchangeName, frameName, backtest
19001
+ *
19002
+ * @example
19003
+ * ```typescript
19004
+ * const service = new RiskMarkdownService();
19005
+ *
19006
+ * // Clear specific combination
19007
+ * await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy", exchangeName: "binance", frameName: "1h", backtest: false });
19008
+ *
19009
+ * // Clear all data
19010
+ * await service.clear();
19011
+ * ```
19012
+ */
19013
+ this.clear = async (payload) => {
19014
+ this.loggerService.log("riskMarkdownService clear", {
19015
+ payload,
19016
+ });
19017
+ if (payload) {
19018
+ const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19019
+ this.getStorage.clear(key);
19020
+ }
19021
+ else {
19022
+ this.getStorage.clear();
19023
+ }
19024
+ };
19025
+ }
19026
+ }
19027
+
19028
+ /**
19029
+ * Service for validating column configurations to ensure consistency with ColumnModel interface
19030
+ * and prevent invalid column definitions.
19031
+ *
19032
+ * Performs comprehensive validation on all column definitions in COLUMN_CONFIG:
19033
+ * - **Required fields**: All columns must have key, label, format, and isVisible properties
19034
+ * - **Unique keys**: All key values must be unique within each column collection
19035
+ * - **Function validation**: format and isVisible must be callable functions
19036
+ * - **Data types**: key and label must be non-empty strings
19037
+ *
19038
+ * @throws {Error} If any validation fails, throws with detailed breakdown of all errors
19039
+ *
19040
+ * @example
19041
+ * ```typescript
19042
+ * const validator = new ColumnValidationService();
19043
+ * validator.validate(); // Throws if column configuration is invalid
19044
+ * ```
19045
+ *
19046
+ * @example Validation failure output:
19047
+ * ```
19048
+ * Column configuration validation failed:
19049
+ * 1. backtest_columns[0]: Missing required field "format"
19050
+ * 2. heat_columns: Duplicate key "symbol" at indexes 1, 5
19051
+ * 3. live_columns[3].isVisible must be a function, got "boolean"
19052
+ * ```
19053
+ */
19054
+ class ColumnValidationService {
19055
+ constructor() {
19056
+ /**
19057
+ * @private
19058
+ * @readonly
19059
+ * Injected logger service instance
19060
+ */
19061
+ this.loggerService = inject(TYPES.loggerService);
19062
+ /**
19063
+ * Validates all column configurations in COLUMN_CONFIG for structural correctness.
19064
+ *
19065
+ * Checks:
19066
+ * 1. All required fields (key, label, format, isVisible) are present in each column
19067
+ * 2. key and label are non-empty strings
19068
+ * 3. format and isVisible are functions (not other types)
19069
+ * 4. All keys are unique within each column collection
19070
+ *
19071
+ * @throws Error if configuration is invalid
19072
+ */
19073
+ this.validate = () => {
19074
+ this.loggerService.log("columnValidationService validate");
19075
+ const errors = [];
19076
+ // Iterate through all column collections in COLUMN_CONFIG
19077
+ for (const [configKey, columns] of Object.entries(COLUMN_CONFIG)) {
19078
+ if (!Array.isArray(columns)) {
19079
+ errors.push(`${configKey} is not an array, got ${typeof columns}`);
19080
+ continue;
19081
+ }
19082
+ // Track keys for uniqueness check
19083
+ const keyMap = new Map();
19084
+ // Validate each column in the collection
19085
+ columns.forEach((column, index) => {
19086
+ if (!column || typeof column !== "object") {
19087
+ errors.push(`${configKey}[${index}]: Column must be an object, got ${typeof column}`);
19088
+ return;
19089
+ }
19090
+ // Check for all required fields
19091
+ const requiredFields = ["key", "label", "format", "isVisible"];
19092
+ for (const field of requiredFields) {
19093
+ if (!(field in column)) {
19094
+ errors.push(`${configKey}[${index}]: Missing required field "${field}"`);
19095
+ }
19096
+ }
19097
+ // Validate key and label are non-empty strings
19098
+ if (typeof column.key !== "string" || column.key.trim() === "") {
19099
+ errors.push(`${configKey}[${index}].key must be a non-empty string, got ${typeof column.key === "string" ? `"${column.key}"` : typeof column.key}`);
19100
+ }
19101
+ else {
19102
+ // Track key for uniqueness check
19103
+ if (!keyMap.has(column.key)) {
19104
+ keyMap.set(column.key, []);
19105
+ }
19106
+ keyMap.get(column.key).push(index);
19107
+ }
19108
+ if (typeof column.label !== "string" || column.label.trim() === "") {
19109
+ errors.push(`${configKey}[${index}].label must be a non-empty string, got ${typeof column.label === "string" ? `"${column.label}"` : typeof column.label}`);
19110
+ }
19111
+ // Validate format is a function
19112
+ if (typeof column.format !== "function") {
19113
+ errors.push(`${configKey}[${index}].format must be a function, got "${typeof column.format}"`);
19114
+ }
19115
+ // Validate isVisible is a function
19116
+ if (typeof column.isVisible !== "function") {
19117
+ errors.push(`${configKey}[${index}].isVisible must be a function, got "${typeof column.isVisible}"`);
19118
+ }
19119
+ });
19120
+ // Check for duplicate keys
19121
+ for (const [key, indexes] of keyMap.entries()) {
19122
+ if (indexes.length > 1) {
19123
+ errors.push(`${configKey}: Duplicate key "${key}" at indexes ${indexes.join(", ")}`);
19124
+ }
19125
+ }
19126
+ }
19127
+ // Throw aggregated errors if any
19128
+ if (errors.length > 0) {
19129
+ const errorMessage = `Column configuration validation failed:\n${errors
19130
+ .map((e, i) => ` ${i + 1}. ${e}`)
19131
+ .join("\n")}`;
19132
+ this.loggerService.warn(errorMessage);
19133
+ throw new Error(errorMessage);
19134
+ }
19135
+ this.loggerService.log("columnValidationService validation passed");
19136
+ };
19137
+ }
19138
+ }
19139
+
19140
+ var _a, _b, _c;
19141
+ const REPORT_BASE_METHOD_NAME_CTOR = "ReportBase.CTOR";
19142
+ const REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT = "ReportBase.waitForInit";
19143
+ const REPORT_BASE_METHOD_NAME_WRITE = "ReportBase.write";
19144
+ const REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER = "ReportUtils.useReportAdapter";
19145
+ const REPORT_UTILS_METHOD_NAME_WRITE_DATA = "ReportUtils.writeReportData";
19146
+ const REPORT_UTILS_METHOD_NAME_ENABLE = "ReportUtils.enable";
19147
+ const REPORT_UTILS_METHOD_NAME_USE_DUMMY = "ReportUtils.useDummy";
19148
+ const REPORT_UTILS_METHOD_NAME_USE_JSONL = "ReportUtils.useJsonl";
19149
+ const WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
19150
+ const WRITE_SAFE_SYMBOL = Symbol("write-safe");
19151
+ /**
19152
+ * JSONL-based report adapter with append-only writes.
19153
+ *
19154
+ * Features:
19155
+ * - Writes events as JSONL entries to a single file per report type
19156
+ * - Stream-based writes with backpressure handling
19157
+ * - 15-second timeout protection for write operations
19158
+ * - Automatic directory creation
19159
+ * - Error handling via exitEmitter
19160
+ * - Search metadata for filtering (symbol, strategy, exchange, frame, signalId, walkerName)
19161
+ *
19162
+ * File format: ./dump/report/{reportName}.jsonl
19163
+ * Each line contains: reportName, data, metadata, timestamp
19164
+ *
19165
+ * Use this adapter for event logging and post-processing analytics.
19166
+ */
19167
+ const ReportBase = functoolsKit.makeExtendable((_c = class {
19168
+ /**
19169
+ * Creates a new JSONL report adapter instance.
19170
+ *
19171
+ * @param reportName - Type of report (backtest, live, walker, etc.)
19172
+ * @param baseDir - Base directory for report files, defaults to ./dump/report
19173
+ */
19174
+ constructor(reportName, baseDir = path.join(process.cwd(), "./dump/report")) {
19175
+ this.reportName = reportName;
19176
+ this.baseDir = baseDir;
19177
+ /** WriteStream instance for append-only writes, null until initialized */
19178
+ this._stream = null;
19179
+ /**
19180
+ * Singleshot initialization function that creates directory and stream.
19181
+ * Protected by singleshot to ensure one-time execution.
19182
+ * Sets up error handler that emits to exitEmitter.
19183
+ */
19184
+ this[_a] = functoolsKit.singleshot(async () => {
19185
+ await fs__namespace.mkdir(this.baseDir, { recursive: true });
19186
+ this._stream = fs$1.createWriteStream(this._filePath, { flags: "a" });
19187
+ this._stream.on('error', (err) => {
19188
+ exitEmitter.next(new Error(`ReportBase stream error for reportName=${this.reportName} message=${functoolsKit.getErrorMessage(err)}`));
19189
+ });
19190
+ });
19191
+ /**
19192
+ * Timeout-protected write function with backpressure handling.
19193
+ * Waits for drain event if write buffer is full.
19194
+ * Times out after 15 seconds and returns TIMEOUT_SYMBOL.
19195
+ */
19196
+ this[_b] = functoolsKit.timeout(async (line) => {
19197
+ if (!this._stream.write(line)) {
19198
+ await new Promise((resolve) => {
19199
+ this._stream.once('drain', resolve);
19200
+ });
19201
+ }
19202
+ }, 15000);
19203
+ bt.loggerService.debug(REPORT_BASE_METHOD_NAME_CTOR, {
19204
+ reportName: this.reportName,
19205
+ baseDir,
19206
+ });
19207
+ this._filePath = path.join(this.baseDir, `${this.reportName}.jsonl`);
19208
+ }
19209
+ /**
19210
+ * Initializes the JSONL file and write stream.
19211
+ * Safe to call multiple times - singleshot ensures one-time execution.
19212
+ *
19213
+ * @param initial - Whether this is the first initialization (informational only)
19214
+ * @returns Promise that resolves when initialization is complete
19215
+ */
19216
+ async waitForInit(initial) {
19217
+ bt.loggerService.debug(REPORT_BASE_METHOD_NAME_WAIT_FOR_INIT, {
19218
+ reportName: this.reportName,
19219
+ initial,
19220
+ });
19221
+ await this[WAIT_FOR_INIT_SYMBOL]();
19222
+ }
19223
+ /**
19224
+ * Writes event data to JSONL file with metadata.
19225
+ * Appends a single line with JSON object containing:
19226
+ * - reportName: Type of report
19227
+ * - data: Event data object
19228
+ * - Search flags: symbol, strategyName, exchangeName, frameName, signalId, walkerName
19229
+ * - timestamp: Current timestamp in milliseconds
19230
+ *
19231
+ * @param data - Event data object to write
19232
+ * @param options - Metadata options for filtering and search
19233
+ * @throws Error if stream not initialized or write timeout exceeded
19234
+ */
19235
+ async write(data, options) {
19236
+ bt.loggerService.debug(REPORT_BASE_METHOD_NAME_WRITE, {
19237
+ reportName: this.reportName,
19238
+ options,
19239
+ });
19240
+ if (!this._stream) {
19241
+ throw new Error(`Stream not initialized for report ${this.reportName}. Call waitForInit() first.`);
19242
+ }
19243
+ const searchFlags = {};
19244
+ if (options.symbol) {
19245
+ searchFlags.symbol = options.symbol;
19246
+ }
19247
+ if (options.strategyName) {
19248
+ searchFlags.strategyName = options.strategyName;
19249
+ }
19250
+ if (options.exchangeName) {
19251
+ searchFlags.exchangeName = options.exchangeName;
19252
+ }
19253
+ if (options.frameName) {
19254
+ searchFlags.frameName = options.frameName;
19255
+ }
19256
+ if (options.signalId) {
19257
+ searchFlags.signalId = options.signalId;
19258
+ }
19259
+ if (options.walkerName) {
19260
+ searchFlags.walkerName = options.walkerName;
19261
+ }
19262
+ const line = JSON.stringify({
19263
+ reportName: this.reportName,
19264
+ data,
19265
+ ...searchFlags,
19266
+ timestamp: Date.now(),
19267
+ }) + "\n";
19268
+ const status = await this[WRITE_SAFE_SYMBOL](line);
19269
+ if (status === functoolsKit.TIMEOUT_SYMBOL) {
19270
+ throw new Error(`Timeout writing to report ${this.reportName}`);
19271
+ }
19272
+ }
19273
+ },
19274
+ _a = WAIT_FOR_INIT_SYMBOL,
19275
+ _b = WRITE_SAFE_SYMBOL,
19276
+ _c));
19277
+ /**
19278
+ * Dummy report adapter that discards all writes.
19279
+ * Used for disabling report logging.
19280
+ */
19281
+ class ReportDummy {
19282
+ /**
19283
+ * No-op initialization function.
19284
+ * @returns Promise that resolves immediately
19285
+ */
19286
+ async waitForInit() {
19287
+ }
19288
+ /**
19289
+ * No-op write function.
19290
+ * @returns Promise that resolves immediately
19291
+ */
19292
+ async write() {
19293
+ }
19294
+ }
19295
+ /**
19296
+ * Default configuration that enables all report services.
19297
+ * Used when no specific configuration is provided to enable().
19298
+ */
19299
+ const WILDCARD_TARGET = {
19300
+ backtest: true,
19301
+ breakeven: true,
19302
+ heat: true,
19303
+ live: true,
19304
+ partial: true,
19305
+ performance: true,
19306
+ risk: true,
19307
+ schedule: true,
19308
+ walker: true,
19309
+ };
19310
+ /**
19311
+ * Utility class for managing report services.
19312
+ *
19313
+ * Provides methods to enable/disable JSONL event logging across
19314
+ * different service types (backtest, live, walker, performance, etc.).
19315
+ *
19316
+ * Typically extended by ReportAdapter for additional functionality.
19317
+ */
19318
+ class ReportUtils {
19319
+ constructor() {
19320
+ /**
19321
+ * Enables report services selectively.
19322
+ *
19323
+ * Subscribes to specified report services and returns a cleanup function
19324
+ * that unsubscribes from all enabled services at once.
19325
+ *
19326
+ * Each enabled service will:
19327
+ * - Start listening to relevant events
19328
+ * - Write events to JSONL files in real-time
19329
+ * - Include metadata for filtering and analytics
19330
+ *
19331
+ * IMPORTANT: Always call the returned unsubscribe function to prevent memory leaks.
19332
+ *
19333
+ * @param config - Service configuration object. Defaults to enabling all services.
19334
+ * @param config.backtest - Enable backtest closed signal logging
19335
+ * @param config.breakeven - Enable breakeven event logging
19336
+ * @param config.partial - Enable partial close event logging
19337
+ * @param config.heat - Enable heatmap data logging
19338
+ * @param config.walker - Enable walker iteration logging
19339
+ * @param config.performance - Enable performance metrics logging
19340
+ * @param config.risk - Enable risk rejection logging
19341
+ * @param config.schedule - Enable scheduled signal logging
19342
+ * @param config.live - Enable live trading event logging
19343
+ *
19344
+ * @returns Cleanup function that unsubscribes from all enabled services
19345
+ */
19346
+ this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET) => {
19347
+ bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
19348
+ backtest: bt$1,
19349
+ breakeven,
19350
+ heat,
19351
+ live,
19352
+ partial,
19353
+ performance,
19354
+ risk,
19355
+ schedule,
19356
+ walker,
19357
+ });
19358
+ const unList = [];
19359
+ if (bt$1) {
19360
+ unList.push(bt.backtestReportService.subscribe());
19361
+ }
19362
+ if (breakeven) {
19363
+ unList.push(bt.breakevenReportService.subscribe());
19364
+ }
19365
+ if (heat) {
19366
+ unList.push(bt.heatReportService.subscribe());
19367
+ }
19368
+ if (live) {
19369
+ unList.push(bt.liveReportService.subscribe());
19370
+ }
19371
+ if (partial) {
19372
+ unList.push(bt.partialReportService.subscribe());
19373
+ }
19374
+ if (performance) {
19375
+ unList.push(bt.performanceReportService.subscribe());
19376
+ }
19377
+ if (risk) {
19378
+ unList.push(bt.riskReportService.subscribe());
19379
+ }
19380
+ if (schedule) {
19381
+ unList.push(bt.scheduleReportService.subscribe());
19382
+ }
19383
+ if (walker) {
19384
+ unList.push(bt.walkerReportService.subscribe());
19385
+ }
19386
+ return functoolsKit.compose(...unList.map((un) => () => void un()));
19387
+ };
19388
+ }
19389
+ }
19390
+ /**
19391
+ * Report adapter with pluggable storage backend and instance memoization.
19392
+ *
19393
+ * Features:
19394
+ * - Adapter pattern for swappable storage implementations
19395
+ * - Memoized storage instances (one per report type)
19396
+ * - Default adapter: ReportBase (JSONL append)
19397
+ * - Lazy initialization on first write
19398
+ * - Real-time event logging to JSONL files
19399
+ *
19400
+ * Used for structured event logging and analytics pipelines.
19401
+ */
19402
+ class ReportAdapter extends ReportUtils {
19403
+ constructor() {
19404
+ super(...arguments);
19405
+ /**
19406
+ * Current report storage adapter constructor.
19407
+ * Defaults to ReportBase for JSONL storage.
19408
+ * Can be changed via useReportAdapter().
19409
+ */
19410
+ this.ReportFactory = ReportBase;
19411
+ /**
19412
+ * Memoized storage instances cache.
19413
+ * Key: reportName (backtest, live, walker, etc.)
19414
+ * Value: TReportBase instance created with current ReportFactory.
19415
+ * Ensures single instance per report type for the lifetime of the application.
19416
+ */
19417
+ this.getReportStorage = functoolsKit.memoize(([reportName]) => reportName, (reportName) => Reflect.construct(this.ReportFactory, [reportName, "./dump/report"]));
19418
+ /**
19419
+ * Writes report data to storage using the configured adapter.
19420
+ * Automatically initializes storage on first write for each report type.
19421
+ *
19422
+ * @param reportName - Type of report (backtest, live, walker, etc.)
19423
+ * @param data - Event data object to write
19424
+ * @param options - Metadata options for filtering and search
19425
+ * @returns Promise that resolves when write is complete
19426
+ * @throws Error if write fails or storage initialization fails
19427
+ *
19428
+ * @internal - Automatically called by report services, not for direct use
19429
+ */
19430
+ this.writeData = async (reportName, data, options) => {
19431
+ bt.loggerService.info(REPORT_UTILS_METHOD_NAME_WRITE_DATA, {
19432
+ reportName,
19433
+ options,
19434
+ });
19435
+ const isInitial = !this.getReportStorage.has(reportName);
19436
+ const reportStorage = this.getReportStorage(reportName);
19437
+ await reportStorage.waitForInit(isInitial);
19438
+ await reportStorage.write(data, options);
19439
+ };
19440
+ }
19441
+ /**
19442
+ * Sets the report storage adapter constructor.
19443
+ * All future report instances will use this adapter.
19444
+ *
19445
+ * @param Ctor - Constructor for report storage adapter
19446
+ */
19447
+ useReportAdapter(Ctor) {
19448
+ bt.loggerService.info(REPORT_UTILS_METHOD_NAME_USE_REPORT_ADAPTER);
19449
+ this.ReportFactory = Ctor;
19450
+ }
19451
+ /**
19452
+ * Switches to a dummy report adapter that discards all writes.
19453
+ * All future report writes will be no-ops.
19454
+ */
19455
+ useDummy() {
19456
+ bt.loggerService.log(REPORT_UTILS_METHOD_NAME_USE_DUMMY);
19457
+ this.useReportAdapter(ReportDummy);
19458
+ }
19459
+ /**
19460
+ * Switches to the default JSONL report adapter.
19461
+ * All future report writes will use JSONL storage.
19462
+ */
19463
+ useJsonl() {
19464
+ bt.loggerService.log(REPORT_UTILS_METHOD_NAME_USE_JSONL);
19465
+ this.useReportAdapter(ReportBase);
19466
+ }
19467
+ }
19468
+ /**
19469
+ * Global singleton instance of ReportAdapter.
19470
+ * Provides JSONL event logging with pluggable storage backends.
19471
+ */
19472
+ const Report = new ReportAdapter();
19473
+
19474
+ const BACKTEST_REPORT_METHOD_NAME_SUBSCRIBE = "BacktestReportService.subscribe";
19475
+ const BACKTEST_REPORT_METHOD_NAME_UNSUBSCRIBE = "BacktestReportService.unsubscribe";
19476
+ const BACKTEST_REPORT_METHOD_NAME_TICK = "BacktestReportService.tick";
19477
+ class BacktestReportService {
19478
+ constructor() {
19479
+ this.loggerService = inject(TYPES.loggerService);
19480
+ this.tick = async (data) => {
19481
+ this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_TICK, { data });
19482
+ const baseEvent = {
19483
+ timestamp: Date.now(),
19484
+ action: data.action,
19485
+ symbol: data.symbol,
19486
+ strategyName: data.strategyName,
19487
+ exchangeName: data.exchangeName,
19488
+ frameName: data.frameName,
19489
+ backtest: true,
19490
+ currentPrice: data.currentPrice,
19491
+ };
19492
+ const searchOptions = {
19493
+ symbol: data.symbol,
19494
+ strategyName: data.strategyName,
19495
+ exchangeName: data.exchangeName,
19496
+ frameName: data.frameName,
19497
+ signalId: data.action === "idle" ? "" : data.signal?.id,
19498
+ walkerName: "",
19499
+ };
19500
+ if (data.action === "idle") {
19501
+ await Report.writeData("backtest", baseEvent, searchOptions);
19502
+ }
19503
+ else if (data.action === "opened") {
19504
+ await Report.writeData("backtest", {
19505
+ ...baseEvent,
19506
+ signalId: data.signal?.id,
19507
+ position: data.signal?.position,
19508
+ note: data.signal?.note,
19509
+ priceOpen: data.signal?.priceOpen,
19510
+ priceTakeProfit: data.signal?.priceTakeProfit,
19511
+ priceStopLoss: data.signal?.priceStopLoss,
19512
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19513
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19514
+ totalExecuted: data.signal?.totalExecuted,
19515
+ openTime: data.signal?.pendingAt,
19516
+ scheduledAt: data.signal?.scheduledAt,
19517
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19518
+ }, { ...searchOptions, signalId: data.signal?.id });
19519
+ }
19520
+ else if (data.action === "active") {
19521
+ await Report.writeData("backtest", {
19522
+ ...baseEvent,
19523
+ signalId: data.signal?.id,
19524
+ position: data.signal?.position,
19525
+ note: data.signal?.note,
19526
+ priceOpen: data.signal?.priceOpen,
19527
+ priceTakeProfit: data.signal?.priceTakeProfit,
19528
+ priceStopLoss: data.signal?.priceStopLoss,
19529
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19530
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19531
+ _partial: data.signal?._partial,
19532
+ totalExecuted: data.signal?.totalExecuted,
19533
+ openTime: data.signal?.pendingAt,
19534
+ scheduledAt: data.signal?.scheduledAt,
19535
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19536
+ percentTp: data.percentTp,
19537
+ percentSl: data.percentSl,
19538
+ pnl: data.pnl.pnlPercentage,
19539
+ pnlPriceOpen: data.pnl.priceOpen,
19540
+ pnlPriceClose: data.pnl.priceClose,
19541
+ }, { ...searchOptions, signalId: data.signal?.id });
19542
+ }
19543
+ else if (data.action === "closed") {
19544
+ const durationMs = data.closeTimestamp - data.signal?.pendingAt;
19545
+ const durationMin = Math.round(durationMs / 60000);
19546
+ await Report.writeData("backtest", {
19547
+ ...baseEvent,
19548
+ signalId: data.signal?.id,
19549
+ position: data.signal?.position,
19550
+ note: data.signal?.note,
19551
+ priceOpen: data.signal?.priceOpen,
19552
+ priceTakeProfit: data.signal?.priceTakeProfit,
19553
+ priceStopLoss: data.signal?.priceStopLoss,
19554
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19555
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19556
+ _partial: data.signal?._partial,
19557
+ totalExecuted: data.signal?.totalExecuted,
19558
+ openTime: data.signal?.pendingAt,
19559
+ scheduledAt: data.signal?.scheduledAt,
19560
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19561
+ pnl: data.pnl.pnlPercentage,
19562
+ pnlPriceOpen: data.pnl.priceOpen,
19563
+ pnlPriceClose: data.pnl.priceClose,
19564
+ closeReason: data.closeReason,
19565
+ closeTime: data.closeTimestamp,
19566
+ duration: durationMin,
19567
+ }, { ...searchOptions, signalId: data.signal?.id });
19568
+ }
19569
+ };
19570
+ this.subscribe = functoolsKit.singleshot(() => {
19571
+ this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_SUBSCRIBE);
19572
+ const unsubscribe = signalBacktestEmitter.subscribe(this.tick);
19573
+ return () => {
19574
+ this.subscribe.clear();
19575
+ unsubscribe();
19576
+ };
19577
+ });
19578
+ this.unsubscribe = async () => {
19579
+ this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_UNSUBSCRIBE);
19580
+ if (this.subscribe.hasValue()) {
19581
+ const lastSubscription = this.subscribe();
19582
+ lastSubscription();
19583
+ }
19584
+ };
19585
+ }
19586
+ }
19587
+
19588
+ const LIVE_REPORT_METHOD_NAME_SUBSCRIBE = "LiveReportService.subscribe";
19589
+ const LIVE_REPORT_METHOD_NAME_UNSUBSCRIBE = "LiveReportService.unsubscribe";
19590
+ const LIVE_REPORT_METHOD_NAME_TICK = "LiveReportService.tick";
19591
+ class LiveReportService {
19592
+ constructor() {
19593
+ this.loggerService = inject(TYPES.loggerService);
19594
+ this.tick = async (data) => {
19595
+ this.loggerService.log(LIVE_REPORT_METHOD_NAME_TICK, { data });
19596
+ const baseEvent = {
19597
+ timestamp: Date.now(),
19598
+ action: data.action,
19599
+ symbol: data.symbol,
19600
+ strategyName: data.strategyName,
19601
+ exchangeName: data.exchangeName,
19602
+ frameName: data.frameName,
19603
+ backtest: false,
19604
+ currentPrice: data.currentPrice,
19605
+ };
19606
+ const searchOptions = {
19607
+ symbol: data.symbol,
19608
+ strategyName: data.strategyName,
19609
+ exchangeName: data.exchangeName,
19610
+ frameName: data.frameName,
19611
+ signalId: data.action === "idle" ? "" : data.signal?.id,
19612
+ walkerName: "",
19613
+ };
19614
+ if (data.action === "idle") {
19615
+ await Report.writeData("live", baseEvent, searchOptions);
19616
+ }
19617
+ else if (data.action === "opened") {
19618
+ await Report.writeData("live", {
19619
+ ...baseEvent,
19620
+ signalId: data.signal?.id,
19621
+ position: data.signal?.position,
19622
+ note: data.signal?.note,
19623
+ priceOpen: data.signal?.priceOpen,
19624
+ priceTakeProfit: data.signal?.priceTakeProfit,
19625
+ priceStopLoss: data.signal?.priceStopLoss,
19626
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19627
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19628
+ totalExecuted: data.signal?.totalExecuted,
19629
+ openTime: data.signal?.pendingAt,
19630
+ scheduledAt: data.signal?.scheduledAt,
19631
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19632
+ }, { ...searchOptions, signalId: data.signal?.id });
19633
+ }
19634
+ else if (data.action === "active") {
19635
+ await Report.writeData("live", {
19636
+ ...baseEvent,
19637
+ signalId: data.signal?.id,
19638
+ position: data.signal?.position,
19639
+ note: data.signal?.note,
19640
+ priceOpen: data.signal?.priceOpen,
19641
+ priceTakeProfit: data.signal?.priceTakeProfit,
19642
+ priceStopLoss: data.signal?.priceStopLoss,
19643
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19644
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19645
+ _partial: data.signal?._partial,
19646
+ totalExecuted: data.signal?.totalExecuted,
19647
+ openTime: data.signal?.pendingAt,
19648
+ scheduledAt: data.signal?.scheduledAt,
19649
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19650
+ percentTp: data.percentTp,
19651
+ percentSl: data.percentSl,
19652
+ pnl: data.pnl.pnlPercentage,
19653
+ pnlPriceOpen: data.pnl.priceOpen,
19654
+ pnlPriceClose: data.pnl.priceClose,
19655
+ }, { ...searchOptions, signalId: data.signal?.id });
19656
+ }
19657
+ else if (data.action === "closed") {
19658
+ const durationMs = data.closeTimestamp - data.signal?.pendingAt;
19659
+ const durationMin = Math.round(durationMs / 60000);
19660
+ await Report.writeData("live", {
19661
+ ...baseEvent,
19662
+ signalId: data.signal?.id,
19663
+ position: data.signal?.position,
19664
+ note: data.signal?.note,
19665
+ priceOpen: data.signal?.priceOpen,
19666
+ priceTakeProfit: data.signal?.priceTakeProfit,
19667
+ priceStopLoss: data.signal?.priceStopLoss,
19668
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19669
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19670
+ _partial: data.signal?._partial,
19671
+ totalExecuted: data.signal?.totalExecuted,
19672
+ openTime: data.signal?.pendingAt,
19673
+ scheduledAt: data.signal?.scheduledAt,
19674
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19675
+ pnl: data.pnl.pnlPercentage,
19676
+ pnlPriceOpen: data.pnl.priceOpen,
19677
+ pnlPriceClose: data.pnl.priceClose,
19678
+ closeReason: data.closeReason,
19679
+ duration: durationMin,
19680
+ closeTime: data.closeTimestamp,
19681
+ }, { ...searchOptions, signalId: data.signal?.id });
19682
+ }
19683
+ };
19684
+ this.subscribe = functoolsKit.singleshot(() => {
19685
+ this.loggerService.log(LIVE_REPORT_METHOD_NAME_SUBSCRIBE);
19686
+ const unsubscribe = signalLiveEmitter.subscribe(this.tick);
19687
+ return () => {
19688
+ this.subscribe.clear();
19689
+ unsubscribe();
19690
+ };
19691
+ });
19692
+ this.unsubscribe = async () => {
19693
+ this.loggerService.log(LIVE_REPORT_METHOD_NAME_UNSUBSCRIBE);
19694
+ if (this.subscribe.hasValue()) {
19695
+ const lastSubscription = this.subscribe();
19696
+ lastSubscription();
19697
+ }
19698
+ };
19699
+ }
19700
+ }
19701
+
19702
+ const SCHEDULE_REPORT_METHOD_NAME_SUBSCRIBE = "ScheduleReportService.subscribe";
19703
+ const SCHEDULE_REPORT_METHOD_NAME_UNSUBSCRIBE = "ScheduleReportService.unsubscribe";
19704
+ const SCHEDULE_REPORT_METHOD_NAME_TICK = "ScheduleReportService.tick";
19705
+ class ScheduleReportService {
19706
+ constructor() {
19707
+ this.loggerService = inject(TYPES.loggerService);
19708
+ this.tick = async (data) => {
19709
+ this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_TICK, { data });
19710
+ const baseEvent = {
19711
+ symbol: data.symbol,
19712
+ strategyName: data.strategyName,
19713
+ exchangeName: data.exchangeName,
19714
+ frameName: data.frameName,
19715
+ backtest: data.backtest,
19716
+ currentPrice: data.currentPrice,
19717
+ };
19718
+ const searchOptions = {
19719
+ symbol: data.symbol,
19720
+ strategyName: data.strategyName,
19721
+ exchangeName: data.exchangeName,
19722
+ frameName: data.frameName,
19723
+ signalId: data.signal?.id,
19724
+ walkerName: "",
19725
+ };
19726
+ if (data.action === "scheduled") {
19727
+ await Report.writeData("schedule", {
19728
+ timestamp: data.signal?.scheduledAt,
19729
+ action: "scheduled",
19730
+ ...baseEvent,
19731
+ signalId: data.signal?.id,
19732
+ position: data.signal?.position,
19733
+ note: data.signal?.note,
19734
+ priceOpen: data.signal?.priceOpen,
19735
+ priceTakeProfit: data.signal?.priceTakeProfit,
19736
+ priceStopLoss: data.signal?.priceStopLoss,
19737
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19738
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19739
+ totalExecuted: data.signal?.totalExecuted,
19740
+ pendingAt: data.signal?.pendingAt,
19741
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19742
+ }, searchOptions);
19743
+ }
19744
+ else if (data.action === "opened") {
19745
+ if (data.signal?.scheduledAt !== data.signal?.pendingAt) {
19746
+ const durationMs = data.signal?.pendingAt - data.signal?.scheduledAt;
19747
+ const durationMin = Math.round(durationMs / 60000);
19748
+ await Report.writeData("schedule", {
19749
+ timestamp: data.signal?.pendingAt,
19750
+ action: "opened",
19751
+ ...baseEvent,
19752
+ signalId: data.signal?.id,
19753
+ position: data.signal?.position,
19754
+ note: data.signal?.note,
19755
+ priceOpen: data.signal?.priceOpen,
19756
+ priceTakeProfit: data.signal?.priceTakeProfit,
19757
+ priceStopLoss: data.signal?.priceStopLoss,
19758
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19759
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19760
+ totalExecuted: data.signal?.totalExecuted,
19761
+ scheduledAt: data.signal?.scheduledAt,
19762
+ pendingAt: data.signal?.pendingAt,
19763
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19764
+ duration: durationMin,
19765
+ }, searchOptions);
19766
+ }
19767
+ }
19768
+ else if (data.action === "cancelled") {
19769
+ const durationMs = data.closeTimestamp - data.signal?.scheduledAt;
19770
+ const durationMin = Math.round(durationMs / 60000);
19771
+ await Report.writeData("schedule", {
19772
+ timestamp: data.closeTimestamp,
19773
+ action: "cancelled",
19774
+ ...baseEvent,
19775
+ signalId: data.signal?.id,
19776
+ position: data.signal?.position,
19777
+ note: data.signal?.note,
19778
+ priceOpen: data.signal?.priceOpen,
19779
+ priceTakeProfit: data.signal?.priceTakeProfit,
19780
+ priceStopLoss: data.signal?.priceStopLoss,
19781
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
19782
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
19783
+ totalExecuted: data.signal?.totalExecuted,
19784
+ scheduledAt: data.signal?.scheduledAt,
19785
+ pendingAt: data.signal?.pendingAt,
19786
+ minuteEstimatedTime: data.signal?.minuteEstimatedTime,
19787
+ closeTime: data.closeTimestamp,
19788
+ duration: durationMin,
19789
+ cancelReason: data.reason,
19790
+ cancelId: data.cancelId,
19791
+ }, searchOptions);
19792
+ }
19793
+ };
19794
+ this.subscribe = functoolsKit.singleshot(() => {
19795
+ this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_SUBSCRIBE);
19796
+ const unsubscribe = signalEmitter.subscribe(this.tick);
19797
+ return () => {
19798
+ this.subscribe.clear();
19799
+ unsubscribe();
19800
+ };
19801
+ });
19802
+ this.unsubscribe = async () => {
19803
+ this.loggerService.log(SCHEDULE_REPORT_METHOD_NAME_UNSUBSCRIBE);
19804
+ if (this.subscribe.hasValue()) {
19805
+ const lastSubscription = this.subscribe();
19806
+ lastSubscription();
19807
+ }
19808
+ };
19809
+ }
19810
+ }
19811
+
19812
+ const PERFORMANCE_REPORT_METHOD_NAME_SUBSCRIBE = "PerformanceReportService.subscribe";
19813
+ const PERFORMANCE_REPORT_METHOD_NAME_UNSUBSCRIBE = "PerformanceReportService.unsubscribe";
19814
+ const PERFORMANCE_REPORT_METHOD_NAME_TRACK = "PerformanceReportService.track";
19815
+ class PerformanceReportService {
19816
+ constructor() {
19817
+ this.loggerService = inject(TYPES.loggerService);
19818
+ this.track = async (event) => {
19819
+ this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_TRACK, { event });
19820
+ await Report.writeData("performance", {
19821
+ timestamp: event.timestamp,
19822
+ metricType: event.metricType,
19823
+ duration: event.duration,
19824
+ symbol: event.symbol,
19825
+ strategyName: event.strategyName,
19826
+ exchangeName: event.exchangeName,
19827
+ frameName: event.frameName,
19828
+ backtest: event.backtest,
19829
+ previousTimestamp: event.previousTimestamp,
19830
+ }, {
19831
+ symbol: event.symbol,
19832
+ strategyName: event.strategyName,
19833
+ exchangeName: event.exchangeName,
19834
+ frameName: event.frameName,
19835
+ signalId: "",
19836
+ walkerName: "",
19837
+ });
19838
+ };
19839
+ this.subscribe = functoolsKit.singleshot(() => {
19840
+ this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_SUBSCRIBE);
19841
+ const unsubscribe = performanceEmitter.subscribe(this.track);
19842
+ return () => {
19843
+ this.subscribe.clear();
19844
+ unsubscribe();
19845
+ };
19846
+ });
19847
+ this.unsubscribe = async () => {
19848
+ this.loggerService.log(PERFORMANCE_REPORT_METHOD_NAME_UNSUBSCRIBE);
19849
+ if (this.subscribe.hasValue()) {
19850
+ const lastSubscription = this.subscribe();
19851
+ lastSubscription();
19852
+ }
19853
+ };
19854
+ }
19855
+ }
19856
+
19857
+ const WALKER_REPORT_METHOD_NAME_SUBSCRIBE = "WalkerReportService.subscribe";
19858
+ const WALKER_REPORT_METHOD_NAME_UNSUBSCRIBE = "WalkerReportService.unsubscribe";
19859
+ const WALKER_REPORT_METHOD_NAME_TICK = "WalkerReportService.tick";
19860
+ class WalkerReportService {
19861
+ constructor() {
19862
+ this.loggerService = inject(TYPES.loggerService);
19863
+ this.tick = async (data) => {
19864
+ this.loggerService.log(WALKER_REPORT_METHOD_NAME_TICK, { data });
19865
+ await Report.writeData("walker", {
19866
+ timestamp: Date.now(),
19867
+ walkerName: data.walkerName,
19868
+ symbol: data.symbol,
19869
+ exchangeName: data.exchangeName,
19870
+ frameName: data.frameName,
19871
+ strategyName: data.strategyName,
19872
+ metric: data.metric,
19873
+ metricValue: data.metricValue,
19874
+ strategiesTested: data.strategiesTested,
19875
+ totalStrategies: data.totalStrategies,
19876
+ bestStrategy: data.bestStrategy,
19877
+ bestMetric: data.bestMetric,
19878
+ totalSignals: data.stats.totalSignals,
19879
+ winCount: data.stats.winCount,
19880
+ lossCount: data.stats.lossCount,
19881
+ winRate: data.stats.winRate,
19882
+ avgPnl: data.stats.avgPnl,
19883
+ totalPnl: data.stats.totalPnl,
19884
+ stdDev: data.stats.stdDev,
19885
+ sharpeRatio: data.stats.sharpeRatio,
19886
+ annualizedSharpeRatio: data.stats.annualizedSharpeRatio,
19887
+ certaintyRatio: data.stats.certaintyRatio,
19888
+ expectedYearlyReturns: data.stats.expectedYearlyReturns,
19889
+ }, {
19890
+ symbol: data.symbol,
19891
+ strategyName: data.strategyName,
19892
+ exchangeName: data.exchangeName,
19893
+ frameName: data.frameName,
19894
+ signalId: "",
19895
+ walkerName: data.walkerName,
19896
+ });
19897
+ };
19898
+ this.subscribe = functoolsKit.singleshot(() => {
19899
+ this.loggerService.log(WALKER_REPORT_METHOD_NAME_SUBSCRIBE);
19900
+ const unsubscribe = walkerEmitter.subscribe(this.tick);
19901
+ return () => {
19902
+ this.subscribe.clear();
19903
+ unsubscribe();
19904
+ };
19905
+ });
19906
+ this.unsubscribe = async () => {
19907
+ this.loggerService.log(WALKER_REPORT_METHOD_NAME_UNSUBSCRIBE);
19908
+ if (this.subscribe.hasValue()) {
19909
+ const lastSubscription = this.subscribe();
19910
+ lastSubscription();
19911
+ }
19912
+ };
19913
+ }
19914
+ }
19915
+
19916
+ const HEAT_REPORT_METHOD_NAME_SUBSCRIBE = "HeatReportService.subscribe";
19917
+ const HEAT_REPORT_METHOD_NAME_UNSUBSCRIBE = "HeatReportService.unsubscribe";
19918
+ const HEAT_REPORT_METHOD_NAME_TICK = "HeatReportService.tick";
19919
+ class HeatReportService {
19920
+ constructor() {
19921
+ this.loggerService = inject(TYPES.loggerService);
19922
+ this.tick = async (data) => {
19923
+ this.loggerService.log(HEAT_REPORT_METHOD_NAME_TICK, { data });
19924
+ if (data.action !== "closed") {
19925
+ return;
19926
+ }
19927
+ await Report.writeData("heat", {
19928
+ timestamp: Date.now(),
19929
+ action: data.action,
19930
+ symbol: data.symbol,
19931
+ strategyName: data.strategyName,
19932
+ exchangeName: data.exchangeName,
19933
+ frameName: data.frameName,
19934
+ backtest: data.backtest,
19935
+ signalId: data.signal?.id,
19936
+ position: data.signal?.position,
19937
+ pnl: data.pnl.pnlPercentage,
19938
+ closeReason: data.closeReason,
19939
+ openTime: data.signal?.pendingAt,
19940
+ closeTime: data.closeTimestamp,
19941
+ }, {
19942
+ symbol: data.symbol,
19943
+ strategyName: data.strategyName,
19944
+ exchangeName: data.exchangeName,
19945
+ frameName: data.frameName,
19946
+ signalId: data.signal?.id,
19947
+ walkerName: "",
19948
+ });
19949
+ };
19950
+ this.subscribe = functoolsKit.singleshot(() => {
19951
+ this.loggerService.log(HEAT_REPORT_METHOD_NAME_SUBSCRIBE);
19952
+ const unsubscribe = signalEmitter.subscribe(this.tick);
19953
+ return () => {
19954
+ this.subscribe.clear();
19955
+ unsubscribe();
19956
+ };
19957
+ });
19958
+ this.unsubscribe = async () => {
19959
+ this.loggerService.log(HEAT_REPORT_METHOD_NAME_UNSUBSCRIBE);
19960
+ if (this.subscribe.hasValue()) {
19961
+ const lastSubscription = this.subscribe();
19962
+ lastSubscription();
19963
+ }
19964
+ };
19965
+ }
19966
+ }
19967
+
19968
+ const PARTIAL_REPORT_METHOD_NAME_SUBSCRIBE = "PartialReportService.subscribe";
19969
+ const PARTIAL_REPORT_METHOD_NAME_UNSUBSCRIBE = "PartialReportService.unsubscribe";
19970
+ const PARTIAL_REPORT_METHOD_NAME_TICK_PROFIT = "PartialReportService.tickProfit";
19971
+ const PARTIAL_REPORT_METHOD_NAME_TICK_LOSS = "PartialReportService.tickLoss";
19972
+ class PartialReportService {
19973
+ constructor() {
19974
+ this.loggerService = inject(TYPES.loggerService);
19975
+ this.tickProfit = async (data) => {
19976
+ this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_TICK_PROFIT, { data });
19977
+ await Report.writeData("partial", {
19978
+ timestamp: data.timestamp,
19979
+ action: "profit",
19980
+ symbol: data.symbol,
19981
+ strategyName: data.data.strategyName,
19982
+ exchangeName: data.exchangeName,
19983
+ frameName: data.frameName,
19984
+ backtest: data.backtest,
19985
+ signalId: data.data.id,
19986
+ position: data.data.position,
19987
+ currentPrice: data.currentPrice,
19988
+ level: data.level,
19989
+ priceOpen: data.data.priceOpen,
19990
+ priceTakeProfit: data.data.priceTakeProfit,
19991
+ priceStopLoss: data.data.priceStopLoss,
19992
+ _partial: data.data._partial,
19993
+ note: data.data.note,
19994
+ pendingAt: data.data.pendingAt,
19995
+ scheduledAt: data.data.scheduledAt,
19996
+ minuteEstimatedTime: data.data.minuteEstimatedTime,
19997
+ }, {
19998
+ symbol: data.symbol,
19999
+ strategyName: data.data.strategyName,
20000
+ exchangeName: data.exchangeName,
20001
+ frameName: data.frameName,
20002
+ signalId: data.data.id,
20003
+ walkerName: "",
20004
+ });
17665
20005
  };
17666
- /**
17667
- * Saves symbol-strategy report to disk.
17668
- * Creates directory if it doesn't exist.
17669
- * Delegates to ReportStorage.dump().
17670
- *
17671
- * @param symbol - Trading pair symbol to save report for
17672
- * @param strategyName - Strategy name to save report for
17673
- * @param exchangeName - Exchange name
17674
- * @param frameName - Frame name
17675
- * @param backtest - True if backtest mode, false if live mode
17676
- * @param path - Directory path to save report (default: "./dump/risk")
17677
- * @param columns - Column configuration for formatting the table
17678
- *
17679
- * @example
17680
- * ```typescript
17681
- * const service = new RiskMarkdownService();
17682
- *
17683
- * // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
17684
- * await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false);
17685
- *
17686
- * // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
17687
- * await service.dump("BTCUSDT", "my-strategy", "binance", "1h", false, "./custom/path");
17688
- * ```
17689
- */
17690
- this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) => {
17691
- this.loggerService.log("riskMarkdownService dump", {
17692
- symbol,
17693
- strategyName,
17694
- exchangeName,
17695
- frameName,
17696
- backtest,
17697
- path,
20006
+ this.tickLoss = async (data) => {
20007
+ this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_TICK_LOSS, { data });
20008
+ await Report.writeData("partial", {
20009
+ timestamp: data.timestamp,
20010
+ action: "loss",
20011
+ symbol: data.symbol,
20012
+ strategyName: data.data.strategyName,
20013
+ exchangeName: data.exchangeName,
20014
+ frameName: data.frameName,
20015
+ backtest: data.backtest,
20016
+ signalId: data.data.id,
20017
+ position: data.data.position,
20018
+ currentPrice: data.currentPrice,
20019
+ level: data.level,
20020
+ priceOpen: data.data.priceOpen,
20021
+ priceTakeProfit: data.data.priceTakeProfit,
20022
+ priceStopLoss: data.data.priceStopLoss,
20023
+ _partial: data.data._partial,
20024
+ note: data.data.note,
20025
+ pendingAt: data.data.pendingAt,
20026
+ scheduledAt: data.data.scheduledAt,
20027
+ minuteEstimatedTime: data.data.minuteEstimatedTime,
20028
+ }, {
20029
+ symbol: data.symbol,
20030
+ strategyName: data.data.strategyName,
20031
+ exchangeName: data.exchangeName,
20032
+ frameName: data.frameName,
20033
+ signalId: data.data.id,
20034
+ walkerName: "",
17698
20035
  });
17699
- const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
17700
- await storage.dump(symbol, strategyName, path, columns);
17701
20036
  };
17702
- /**
17703
- * Clears accumulated event data from storage.
17704
- * If payload is provided, clears only that specific symbol-strategy-exchange-frame-backtest combination's data.
17705
- * If nothing is provided, clears all data.
17706
- *
17707
- * @param payload - Optional payload with symbol, strategyName, exchangeName, frameName, backtest
17708
- *
17709
- * @example
17710
- * ```typescript
17711
- * const service = new RiskMarkdownService();
17712
- *
17713
- * // Clear specific combination
17714
- * await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy", exchangeName: "binance", frameName: "1h", backtest: false });
17715
- *
17716
- * // Clear all data
17717
- * await service.clear();
17718
- * ```
17719
- */
17720
- this.clear = async (payload) => {
17721
- this.loggerService.log("riskMarkdownService clear", {
17722
- payload,
17723
- });
17724
- if (payload) {
17725
- const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
17726
- this.getStorage.clear(key);
17727
- }
17728
- else {
17729
- this.getStorage.clear();
20037
+ this.subscribe = functoolsKit.singleshot(() => {
20038
+ this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_SUBSCRIBE);
20039
+ const unProfit = partialProfitSubject.subscribe(this.tickProfit);
20040
+ const unLoss = partialLossSubject.subscribe(this.tickLoss);
20041
+ return () => {
20042
+ this.subscribe.clear();
20043
+ unProfit();
20044
+ unLoss();
20045
+ };
20046
+ });
20047
+ this.unsubscribe = async () => {
20048
+ this.loggerService.log(PARTIAL_REPORT_METHOD_NAME_UNSUBSCRIBE);
20049
+ if (this.subscribe.hasValue()) {
20050
+ const lastSubscription = this.subscribe();
20051
+ lastSubscription();
17730
20052
  }
17731
20053
  };
17732
- /**
17733
- * Initializes the service by subscribing to risk rejection events.
17734
- * Uses singleshot to ensure initialization happens only once.
17735
- * Automatically called on first use.
17736
- *
17737
- * @example
17738
- * ```typescript
17739
- * const service = new RiskMarkdownService();
17740
- * await service.init(); // Subscribe to rejection events
17741
- * ```
17742
- */
17743
- this.init = functoolsKit.singleshot(async () => {
17744
- this.loggerService.log("riskMarkdownService init");
17745
- this.unsubscribe = riskSubject.subscribe(this.tickRejection);
17746
- });
17747
20054
  }
17748
20055
  }
17749
20056
 
17750
- /**
17751
- * Service for validating column configurations to ensure consistency with ColumnModel interface
17752
- * and prevent invalid column definitions.
17753
- *
17754
- * Performs comprehensive validation on all column definitions in COLUMN_CONFIG:
17755
- * - **Required fields**: All columns must have key, label, format, and isVisible properties
17756
- * - **Unique keys**: All key values must be unique within each column collection
17757
- * - **Function validation**: format and isVisible must be callable functions
17758
- * - **Data types**: key and label must be non-empty strings
17759
- *
17760
- * @throws {Error} If any validation fails, throws with detailed breakdown of all errors
17761
- *
17762
- * @example
17763
- * ```typescript
17764
- * const validator = new ColumnValidationService();
17765
- * validator.validate(); // Throws if column configuration is invalid
17766
- * ```
17767
- *
17768
- * @example Validation failure output:
17769
- * ```
17770
- * Column configuration validation failed:
17771
- * 1. backtest_columns[0]: Missing required field "format"
17772
- * 2. heat_columns: Duplicate key "symbol" at indexes 1, 5
17773
- * 3. live_columns[3].isVisible must be a function, got "boolean"
17774
- * ```
17775
- */
17776
- class ColumnValidationService {
20057
+ const BREAKEVEN_REPORT_METHOD_NAME_SUBSCRIBE = "BreakevenReportService.subscribe";
20058
+ const BREAKEVEN_REPORT_METHOD_NAME_UNSUBSCRIBE = "BreakevenReportService.unsubscribe";
20059
+ const BREAKEVEN_REPORT_METHOD_NAME_TICK = "BreakevenReportService.tickBreakeven";
20060
+ class BreakevenReportService {
17777
20061
  constructor() {
17778
- /**
17779
- * @private
17780
- * @readonly
17781
- * Injected logger service instance
17782
- */
17783
20062
  this.loggerService = inject(TYPES.loggerService);
17784
- /**
17785
- * Validates all column configurations in COLUMN_CONFIG for structural correctness.
17786
- *
17787
- * Checks:
17788
- * 1. All required fields (key, label, format, isVisible) are present in each column
17789
- * 2. key and label are non-empty strings
17790
- * 3. format and isVisible are functions (not other types)
17791
- * 4. All keys are unique within each column collection
17792
- *
17793
- * @throws Error if configuration is invalid
17794
- */
17795
- this.validate = () => {
17796
- this.loggerService.log("columnValidationService validate");
17797
- const errors = [];
17798
- // Iterate through all column collections in COLUMN_CONFIG
17799
- for (const [configKey, columns] of Object.entries(COLUMN_CONFIG)) {
17800
- if (!Array.isArray(columns)) {
17801
- errors.push(`${configKey} is not an array, got ${typeof columns}`);
17802
- continue;
17803
- }
17804
- // Track keys for uniqueness check
17805
- const keyMap = new Map();
17806
- // Validate each column in the collection
17807
- columns.forEach((column, index) => {
17808
- if (!column || typeof column !== "object") {
17809
- errors.push(`${configKey}[${index}]: Column must be an object, got ${typeof column}`);
17810
- return;
17811
- }
17812
- // Check for all required fields
17813
- const requiredFields = ["key", "label", "format", "isVisible"];
17814
- for (const field of requiredFields) {
17815
- if (!(field in column)) {
17816
- errors.push(`${configKey}[${index}]: Missing required field "${field}"`);
17817
- }
17818
- }
17819
- // Validate key and label are non-empty strings
17820
- if (typeof column.key !== "string" || column.key.trim() === "") {
17821
- errors.push(`${configKey}[${index}].key must be a non-empty string, got ${typeof column.key === "string" ? `"${column.key}"` : typeof column.key}`);
17822
- }
17823
- else {
17824
- // Track key for uniqueness check
17825
- if (!keyMap.has(column.key)) {
17826
- keyMap.set(column.key, []);
17827
- }
17828
- keyMap.get(column.key).push(index);
17829
- }
17830
- if (typeof column.label !== "string" || column.label.trim() === "") {
17831
- errors.push(`${configKey}[${index}].label must be a non-empty string, got ${typeof column.label === "string" ? `"${column.label}"` : typeof column.label}`);
17832
- }
17833
- // Validate format is a function
17834
- if (typeof column.format !== "function") {
17835
- errors.push(`${configKey}[${index}].format must be a function, got "${typeof column.format}"`);
17836
- }
17837
- // Validate isVisible is a function
17838
- if (typeof column.isVisible !== "function") {
17839
- errors.push(`${configKey}[${index}].isVisible must be a function, got "${typeof column.isVisible}"`);
17840
- }
17841
- });
17842
- // Check for duplicate keys
17843
- for (const [key, indexes] of keyMap.entries()) {
17844
- if (indexes.length > 1) {
17845
- errors.push(`${configKey}: Duplicate key "${key}" at indexes ${indexes.join(", ")}`);
17846
- }
17847
- }
20063
+ this.tickBreakeven = async (data) => {
20064
+ this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_TICK, { data });
20065
+ await Report.writeData("breakeven", {
20066
+ timestamp: data.timestamp,
20067
+ symbol: data.symbol,
20068
+ strategyName: data.data.strategyName,
20069
+ exchangeName: data.exchangeName,
20070
+ frameName: data.frameName,
20071
+ backtest: data.backtest,
20072
+ signalId: data.data.id,
20073
+ position: data.data.position,
20074
+ currentPrice: data.currentPrice,
20075
+ priceOpen: data.data.priceOpen,
20076
+ priceTakeProfit: data.data.priceTakeProfit,
20077
+ priceStopLoss: data.data.priceStopLoss,
20078
+ _partial: data.data._partial,
20079
+ note: data.data.note,
20080
+ pendingAt: data.data.pendingAt,
20081
+ scheduledAt: data.data.scheduledAt,
20082
+ minuteEstimatedTime: data.data.minuteEstimatedTime,
20083
+ }, {
20084
+ symbol: data.symbol,
20085
+ strategyName: data.data.strategyName,
20086
+ exchangeName: data.exchangeName,
20087
+ frameName: data.frameName,
20088
+ signalId: data.data.id,
20089
+ walkerName: "",
20090
+ });
20091
+ };
20092
+ this.subscribe = functoolsKit.singleshot(() => {
20093
+ this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_SUBSCRIBE);
20094
+ const unsubscribe = breakevenSubject.subscribe(this.tickBreakeven);
20095
+ return () => {
20096
+ this.subscribe.clear();
20097
+ unsubscribe();
20098
+ };
20099
+ });
20100
+ this.unsubscribe = async () => {
20101
+ this.loggerService.log(BREAKEVEN_REPORT_METHOD_NAME_UNSUBSCRIBE);
20102
+ if (this.subscribe.hasValue()) {
20103
+ const lastSubscription = this.subscribe();
20104
+ lastSubscription();
17848
20105
  }
17849
- // Throw aggregated errors if any
17850
- if (errors.length > 0) {
17851
- const errorMessage = `Column configuration validation failed:\n${errors
17852
- .map((e, i) => ` ${i + 1}. ${e}`)
17853
- .join("\n")}`;
17854
- this.loggerService.warn(errorMessage);
17855
- throw new Error(errorMessage);
20106
+ };
20107
+ }
20108
+ }
20109
+
20110
+ const RISK_REPORT_METHOD_NAME_SUBSCRIBE = "RiskReportService.subscribe";
20111
+ const RISK_REPORT_METHOD_NAME_UNSUBSCRIBE = "RiskReportService.unsubscribe";
20112
+ const RISK_REPORT_METHOD_NAME_TICK = "RiskReportService.tickRejection";
20113
+ class RiskReportService {
20114
+ constructor() {
20115
+ this.loggerService = inject(TYPES.loggerService);
20116
+ this.tickRejection = async (data) => {
20117
+ this.loggerService.log(RISK_REPORT_METHOD_NAME_TICK, { data });
20118
+ await Report.writeData("risk", {
20119
+ timestamp: data.timestamp,
20120
+ symbol: data.symbol,
20121
+ strategyName: data.strategyName,
20122
+ exchangeName: data.exchangeName,
20123
+ frameName: data.frameName,
20124
+ backtest: data.backtest,
20125
+ currentPrice: data.currentPrice,
20126
+ activePositionCount: data.activePositionCount,
20127
+ rejectionId: data.rejectionId,
20128
+ rejectionNote: data.rejectionNote,
20129
+ pendingSignal: data.pendingSignal,
20130
+ signalId: data.pendingSignal?.id,
20131
+ position: data.pendingSignal?.position,
20132
+ priceOpen: data.pendingSignal?.priceOpen,
20133
+ priceTakeProfit: data.pendingSignal?.priceTakeProfit,
20134
+ priceStopLoss: data.pendingSignal?.priceStopLoss,
20135
+ originalPriceTakeProfit: data.pendingSignal?.originalPriceTakeProfit,
20136
+ originalPriceStopLoss: data.pendingSignal?.originalPriceStopLoss,
20137
+ totalExecuted: data.pendingSignal?.totalExecuted,
20138
+ note: data.pendingSignal?.note,
20139
+ minuteEstimatedTime: data.pendingSignal?.minuteEstimatedTime,
20140
+ }, {
20141
+ symbol: data.symbol,
20142
+ strategyName: data.strategyName,
20143
+ exchangeName: data.exchangeName,
20144
+ frameName: data.frameName,
20145
+ signalId: "",
20146
+ walkerName: "",
20147
+ });
20148
+ };
20149
+ this.subscribe = functoolsKit.singleshot(() => {
20150
+ this.loggerService.log(RISK_REPORT_METHOD_NAME_SUBSCRIBE);
20151
+ const unsubscribe = riskSubject.subscribe(this.tickRejection);
20152
+ return () => {
20153
+ this.subscribe.clear();
20154
+ unsubscribe();
20155
+ };
20156
+ });
20157
+ this.unsubscribe = async () => {
20158
+ this.loggerService.log(RISK_REPORT_METHOD_NAME_UNSUBSCRIBE);
20159
+ if (this.subscribe.hasValue()) {
20160
+ const lastSubscription = this.subscribe();
20161
+ lastSubscription();
17856
20162
  }
17857
- this.loggerService.log("columnValidationService validation passed");
17858
20163
  };
17859
20164
  }
17860
20165
  }
@@ -17924,6 +20229,17 @@ class ColumnValidationService {
17924
20229
  provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
17925
20230
  provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
17926
20231
  }
20232
+ {
20233
+ provide(TYPES.backtestReportService, () => new BacktestReportService());
20234
+ provide(TYPES.liveReportService, () => new LiveReportService());
20235
+ provide(TYPES.scheduleReportService, () => new ScheduleReportService());
20236
+ provide(TYPES.performanceReportService, () => new PerformanceReportService());
20237
+ provide(TYPES.walkerReportService, () => new WalkerReportService());
20238
+ provide(TYPES.heatReportService, () => new HeatReportService());
20239
+ provide(TYPES.partialReportService, () => new PartialReportService());
20240
+ provide(TYPES.breakevenReportService, () => new BreakevenReportService());
20241
+ provide(TYPES.riskReportService, () => new RiskReportService());
20242
+ }
17927
20243
  {
17928
20244
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
17929
20245
  provide(TYPES.strategyValidationService, () => new StrategyValidationService());
@@ -18004,6 +20320,17 @@ const markdownServices = {
18004
20320
  outlineMarkdownService: inject(TYPES.outlineMarkdownService),
18005
20321
  riskMarkdownService: inject(TYPES.riskMarkdownService),
18006
20322
  };
20323
+ const reportServices = {
20324
+ backtestReportService: inject(TYPES.backtestReportService),
20325
+ liveReportService: inject(TYPES.liveReportService),
20326
+ scheduleReportService: inject(TYPES.scheduleReportService),
20327
+ performanceReportService: inject(TYPES.performanceReportService),
20328
+ walkerReportService: inject(TYPES.walkerReportService),
20329
+ heatReportService: inject(TYPES.heatReportService),
20330
+ partialReportService: inject(TYPES.partialReportService),
20331
+ breakevenReportService: inject(TYPES.breakevenReportService),
20332
+ riskReportService: inject(TYPES.riskReportService),
20333
+ };
18007
20334
  const validationServices = {
18008
20335
  exchangeValidationService: inject(TYPES.exchangeValidationService),
18009
20336
  strategyValidationService: inject(TYPES.strategyValidationService),
@@ -18029,6 +20356,7 @@ const backtest = {
18029
20356
  ...logicPrivateServices,
18030
20357
  ...logicPublicServices,
18031
20358
  ...markdownServices,
20359
+ ...reportServices,
18032
20360
  ...validationServices,
18033
20361
  ...templateServices,
18034
20362
  };
@@ -18496,7 +20824,7 @@ async function cancel(symbol, cancelId) {
18496
20824
  *
18497
20825
  * @param symbol - Trading pair symbol
18498
20826
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
18499
- * @returns Promise that resolves when state is updated
20827
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
18500
20828
  *
18501
20829
  * @throws Error if currentPrice is not in profit direction:
18502
20830
  * - LONG: currentPrice must be > priceOpen
@@ -18507,7 +20835,10 @@ async function cancel(symbol, cancelId) {
18507
20835
  * import { partialProfit } from "backtest-kit";
18508
20836
  *
18509
20837
  * // Close 30% of LONG position at profit
18510
- * await partialProfit("BTCUSDT", 30, 45000);
20838
+ * const success = await partialProfit("BTCUSDT", 30);
20839
+ * if (success) {
20840
+ * console.log('Partial profit executed');
20841
+ * }
18511
20842
  * ```
18512
20843
  */
18513
20844
  async function partialProfit(symbol, percentToClose) {
@@ -18524,7 +20855,7 @@ async function partialProfit(symbol, percentToClose) {
18524
20855
  const currentPrice = await getAveragePrice(symbol);
18525
20856
  const { backtest: isBacktest } = bt.executionContextService.context;
18526
20857
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
18527
- await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
20858
+ return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
18528
20859
  }
18529
20860
  /**
18530
20861
  * Executes partial close at loss level (moving toward SL).
@@ -18536,7 +20867,7 @@ async function partialProfit(symbol, percentToClose) {
18536
20867
  *
18537
20868
  * @param symbol - Trading pair symbol
18538
20869
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
18539
- * @returns Promise that resolves when state is updated
20870
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
18540
20871
  *
18541
20872
  * @throws Error if currentPrice is not in loss direction:
18542
20873
  * - LONG: currentPrice must be < priceOpen
@@ -18547,7 +20878,10 @@ async function partialProfit(symbol, percentToClose) {
18547
20878
  * import { partialLoss } from "backtest-kit";
18548
20879
  *
18549
20880
  * // Close 40% of LONG position at loss
18550
- * await partialLoss("BTCUSDT", 40, 38000);
20881
+ * const success = await partialLoss("BTCUSDT", 40);
20882
+ * if (success) {
20883
+ * console.log('Partial loss executed');
20884
+ * }
18551
20885
  * ```
18552
20886
  */
18553
20887
  async function partialLoss(symbol, percentToClose) {
@@ -18564,7 +20898,7 @@ async function partialLoss(symbol, percentToClose) {
18564
20898
  const currentPrice = await getAveragePrice(symbol);
18565
20899
  const { backtest: isBacktest } = bt.executionContextService.context;
18566
20900
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
18567
- await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
20901
+ return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
18568
20902
  }
18569
20903
  /**
18570
20904
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -21167,7 +23501,7 @@ class BacktestUtils {
21167
23501
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
21168
23502
  * @param currentPrice - Current market price for this partial close
21169
23503
  * @param context - Execution context with strategyName, exchangeName, and frameName
21170
- * @returns Promise that resolves when state is updated
23504
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
21171
23505
  *
21172
23506
  * @throws Error if currentPrice is not in profit direction:
21173
23507
  * - LONG: currentPrice must be > priceOpen
@@ -21176,11 +23510,14 @@ class BacktestUtils {
21176
23510
  * @example
21177
23511
  * ```typescript
21178
23512
  * // Close 30% of LONG position at profit
21179
- * await Backtest.partialProfit("BTCUSDT", 30, 45000, {
23513
+ * const success = await Backtest.partialProfit("BTCUSDT", 30, 45000, {
21180
23514
  * exchangeName: "binance",
21181
23515
  * frameName: "frame1",
21182
23516
  * strategyName: "my-strategy"
21183
23517
  * });
23518
+ * if (success) {
23519
+ * console.log('Partial profit executed');
23520
+ * }
21184
23521
  * ```
21185
23522
  */
21186
23523
  this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
@@ -21199,7 +23536,7 @@ class BacktestUtils {
21199
23536
  riskList &&
21200
23537
  riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT));
21201
23538
  }
21202
- await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
23539
+ return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
21203
23540
  };
21204
23541
  /**
21205
23542
  * Executes partial close at loss level (moving toward SL).
@@ -21211,7 +23548,7 @@ class BacktestUtils {
21211
23548
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
21212
23549
  * @param currentPrice - Current market price for this partial close
21213
23550
  * @param context - Execution context with strategyName, exchangeName, and frameName
21214
- * @returns Promise that resolves when state is updated
23551
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
21215
23552
  *
21216
23553
  * @throws Error if currentPrice is not in loss direction:
21217
23554
  * - LONG: currentPrice must be < priceOpen
@@ -21220,11 +23557,14 @@ class BacktestUtils {
21220
23557
  * @example
21221
23558
  * ```typescript
21222
23559
  * // Close 40% of LONG position at loss
21223
- * await Backtest.partialLoss("BTCUSDT", 40, 38000, {
23560
+ * const success = await Backtest.partialLoss("BTCUSDT", 40, 38000, {
21224
23561
  * exchangeName: "binance",
21225
23562
  * frameName: "frame1",
21226
23563
  * strategyName: "my-strategy"
21227
23564
  * });
23565
+ * if (success) {
23566
+ * console.log('Partial loss executed');
23567
+ * }
21228
23568
  * ```
21229
23569
  */
21230
23570
  this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
@@ -21243,7 +23583,7 @@ class BacktestUtils {
21243
23583
  riskList &&
21244
23584
  riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS));
21245
23585
  }
21246
- await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
23586
+ return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
21247
23587
  };
21248
23588
  /**
21249
23589
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -22086,7 +24426,7 @@ class LiveUtils {
22086
24426
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
22087
24427
  * @param currentPrice - Current market price for this partial close
22088
24428
  * @param context - Execution context with strategyName and exchangeName
22089
- * @returns Promise that resolves when state is updated
24429
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
22090
24430
  *
22091
24431
  * @throws Error if currentPrice is not in profit direction:
22092
24432
  * - LONG: currentPrice must be > priceOpen
@@ -22095,10 +24435,13 @@ class LiveUtils {
22095
24435
  * @example
22096
24436
  * ```typescript
22097
24437
  * // Close 30% of LONG position at profit
22098
- * await Live.partialProfit("BTCUSDT", 30, 45000, {
24438
+ * const success = await Live.partialProfit("BTCUSDT", 30, 45000, {
22099
24439
  * exchangeName: "binance",
22100
24440
  * strategyName: "my-strategy"
22101
24441
  * });
24442
+ * if (success) {
24443
+ * console.log('Partial profit executed');
24444
+ * }
22102
24445
  * ```
22103
24446
  */
22104
24447
  this.partialProfit = async (symbol, percentToClose, currentPrice, context) => {
@@ -22115,7 +24458,7 @@ class LiveUtils {
22115
24458
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
22116
24459
  riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT));
22117
24460
  }
22118
- await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
24461
+ return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
22119
24462
  strategyName: context.strategyName,
22120
24463
  exchangeName: context.exchangeName,
22121
24464
  frameName: "",
@@ -22131,7 +24474,7 @@ class LiveUtils {
22131
24474
  * @param percentToClose - Percentage of position to close (0-100, absolute value)
22132
24475
  * @param currentPrice - Current market price for this partial close
22133
24476
  * @param context - Execution context with strategyName and exchangeName
22134
- * @returns Promise that resolves when state is updated
24477
+ * @returns Promise<boolean> - true if partial close executed, false if skipped
22135
24478
  *
22136
24479
  * @throws Error if currentPrice is not in loss direction:
22137
24480
  * - LONG: currentPrice must be < priceOpen
@@ -22140,10 +24483,13 @@ class LiveUtils {
22140
24483
  * @example
22141
24484
  * ```typescript
22142
24485
  * // Close 40% of LONG position at loss
22143
- * await Live.partialLoss("BTCUSDT", 40, 38000, {
24486
+ * const success = await Live.partialLoss("BTCUSDT", 40, 38000, {
22144
24487
  * exchangeName: "binance",
22145
24488
  * strategyName: "my-strategy"
22146
24489
  * });
24490
+ * if (success) {
24491
+ * console.log('Partial loss executed');
24492
+ * }
22147
24493
  * ```
22148
24494
  */
22149
24495
  this.partialLoss = async (symbol, percentToClose, currentPrice, context) => {
@@ -22160,7 +24506,7 @@ class LiveUtils {
22160
24506
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
22161
24507
  riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS));
22162
24508
  }
22163
- await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
24509
+ return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
22164
24510
  strategyName: context.strategyName,
22165
24511
  exchangeName: context.exchangeName,
22166
24512
  frameName: "",
@@ -24027,11 +26373,29 @@ class ExchangeInstance {
24027
26373
  }
24028
26374
  const when = new Date(Date.now());
24029
26375
  const since = new Date(when.getTime() - adjust * 60 * 1000);
24030
- const data = await this._schema.getCandles(symbol, interval, since, limit);
26376
+ let allData = [];
26377
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26378
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26379
+ let remaining = limit;
26380
+ let currentSince = new Date(since.getTime());
26381
+ while (remaining > 0) {
26382
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26383
+ const chunkData = await this._schema.getCandles(symbol, interval, currentSince, chunkLimit);
26384
+ allData.push(...chunkData);
26385
+ remaining -= chunkLimit;
26386
+ if (remaining > 0) {
26387
+ // Move currentSince forward by the number of candles fetched
26388
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
26389
+ }
26390
+ }
26391
+ }
26392
+ else {
26393
+ allData = await this._schema.getCandles(symbol, interval, since, limit);
26394
+ }
24031
26395
  // Filter candles to strictly match the requested range
24032
26396
  const whenTimestamp = when.getTime();
24033
26397
  const sinceTimestamp = since.getTime();
24034
- const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
26398
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
24035
26399
  if (filteredData.length < limit) {
24036
26400
  bt.loggerService.warn(`ExchangeInstance Expected ${limit} candles, got ${filteredData.length}`);
24037
26401
  }
@@ -25164,6 +27528,9 @@ exports.Exchange = Exchange;
25164
27528
  exports.ExecutionContextService = ExecutionContextService;
25165
27529
  exports.Heat = Heat;
25166
27530
  exports.Live = Live;
27531
+ exports.Markdown = Markdown;
27532
+ exports.MarkdownFileBase = MarkdownFileBase;
27533
+ exports.MarkdownFolderBase = MarkdownFolderBase;
25167
27534
  exports.MethodContextService = MethodContextService;
25168
27535
  exports.Notification = Notification;
25169
27536
  exports.Optimizer = Optimizer;
@@ -25176,6 +27543,8 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
25176
27543
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
25177
27544
  exports.PersistSignalAdapter = PersistSignalAdapter;
25178
27545
  exports.PositionSize = PositionSize;
27546
+ exports.Report = Report;
27547
+ exports.ReportBase = ReportBase;
25179
27548
  exports.Risk = Risk;
25180
27549
  exports.Schedule = Schedule;
25181
27550
  exports.Walker = Walker;