backtest-kit 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +841 -6
  2. package/build/index.cjs +3418 -200
  3. package/build/index.mjs +3408 -201
  4. package/package.json +3 -2
  5. package/types.d.ts +2266 -119
package/build/index.cjs CHANGED
@@ -118,6 +118,8 @@ const connectionServices$1 = {
118
118
  frameConnectionService: Symbol('frameConnectionService'),
119
119
  sizingConnectionService: Symbol('sizingConnectionService'),
120
120
  riskConnectionService: Symbol('riskConnectionService'),
121
+ optimizerConnectionService: Symbol('optimizerConnectionService'),
122
+ partialConnectionService: Symbol('partialConnectionService'),
121
123
  };
122
124
  const schemaServices$1 = {
123
125
  exchangeSchemaService: Symbol('exchangeSchemaService'),
@@ -126,6 +128,7 @@ const schemaServices$1 = {
126
128
  walkerSchemaService: Symbol('walkerSchemaService'),
127
129
  sizingSchemaService: Symbol('sizingSchemaService'),
128
130
  riskSchemaService: Symbol('riskSchemaService'),
131
+ optimizerSchemaService: Symbol('optimizerSchemaService'),
129
132
  };
130
133
  const globalServices$1 = {
131
134
  exchangeGlobalService: Symbol('exchangeGlobalService'),
@@ -133,6 +136,8 @@ const globalServices$1 = {
133
136
  frameGlobalService: Symbol('frameGlobalService'),
134
137
  sizingGlobalService: Symbol('sizingGlobalService'),
135
138
  riskGlobalService: Symbol('riskGlobalService'),
139
+ optimizerGlobalService: Symbol('optimizerGlobalService'),
140
+ partialGlobalService: Symbol('partialGlobalService'),
136
141
  };
137
142
  const commandServices$1 = {
138
143
  liveCommandService: Symbol('liveCommandService'),
@@ -156,6 +161,7 @@ const markdownServices$1 = {
156
161
  performanceMarkdownService: Symbol('performanceMarkdownService'),
157
162
  walkerMarkdownService: Symbol('walkerMarkdownService'),
158
163
  heatMarkdownService: Symbol('heatMarkdownService'),
164
+ partialMarkdownService: Symbol('partialMarkdownService'),
159
165
  };
160
166
  const validationServices$1 = {
161
167
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -164,6 +170,10 @@ const validationServices$1 = {
164
170
  walkerValidationService: Symbol('walkerValidationService'),
165
171
  sizingValidationService: Symbol('sizingValidationService'),
166
172
  riskValidationService: Symbol('riskValidationService'),
173
+ optimizerValidationService: Symbol('optimizerValidationService'),
174
+ };
175
+ const templateServices$1 = {
176
+ optimizerTemplateService: Symbol('optimizerTemplateService'),
167
177
  };
168
178
  const TYPES = {
169
179
  ...baseServices$1,
@@ -176,6 +186,7 @@ const TYPES = {
176
186
  ...logicPublicServices$1,
177
187
  ...markdownServices$1,
178
188
  ...validationServices$1,
189
+ ...templateServices$1,
179
190
  };
180
191
 
181
192
  /**
@@ -877,6 +888,12 @@ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
877
888
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
878
889
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
879
890
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
891
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
892
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
893
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
894
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
895
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
896
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
880
897
  const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
881
898
  const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
882
899
  const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
@@ -942,7 +959,7 @@ const PersistBase = functoolsKit.makeExtendable(class {
942
959
  * Creates new persistence instance.
943
960
  *
944
961
  * @param entityName - Unique entity type identifier
945
- * @param baseDir - Base directory for all entities (default: ./logs/data)
962
+ * @param baseDir - Base directory for all entities (default: ./dump/data)
946
963
  */
947
964
  constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
948
965
  this.entityName = entityName;
@@ -1201,7 +1218,7 @@ class PersistSignalUtils {
1201
1218
  this.PersistSignalFactory = PersistBase;
1202
1219
  this.getSignalStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
1203
1220
  strategyName,
1204
- `./logs/data/signal/`,
1221
+ `./dump/data/signal/`,
1205
1222
  ]));
1206
1223
  /**
1207
1224
  * Reads persisted signal data for a strategy and symbol.
@@ -1297,7 +1314,7 @@ class PersistRiskUtils {
1297
1314
  this.PersistRiskFactory = PersistBase;
1298
1315
  this.getRiskStorage = functoolsKit.memoize(([riskName]) => `${riskName}`, (riskName) => Reflect.construct(this.PersistRiskFactory, [
1299
1316
  riskName,
1300
- `./logs/data/risk/`,
1317
+ `./dump/data/risk/`,
1301
1318
  ]));
1302
1319
  /**
1303
1320
  * Reads persisted active positions for a risk profile.
@@ -1374,6 +1391,192 @@ class PersistRiskUtils {
1374
1391
  * ```
1375
1392
  */
1376
1393
  const PersistRiskAdapter = new PersistRiskUtils();
1394
+ /**
1395
+ * Utility class for managing scheduled signal persistence.
1396
+ *
1397
+ * Features:
1398
+ * - Memoized storage instances per strategy
1399
+ * - Custom adapter support
1400
+ * - Atomic read/write operations for scheduled signals
1401
+ * - Crash-safe scheduled signal state management
1402
+ *
1403
+ * Used by ClientStrategy for live mode persistence of scheduled signals (_scheduledSignal).
1404
+ */
1405
+ class PersistScheduleUtils {
1406
+ constructor() {
1407
+ this.PersistScheduleFactory = PersistBase;
1408
+ this.getScheduleStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistScheduleFactory, [
1409
+ strategyName,
1410
+ `./dump/data/schedule/`,
1411
+ ]));
1412
+ /**
1413
+ * Reads persisted scheduled signal data for a strategy and symbol.
1414
+ *
1415
+ * Called by ClientStrategy.waitForInit() to restore scheduled signal state.
1416
+ * Returns null if no scheduled signal exists.
1417
+ *
1418
+ * @param strategyName - Strategy identifier
1419
+ * @param symbol - Trading pair symbol
1420
+ * @returns Promise resolving to scheduled signal or null
1421
+ */
1422
+ this.readScheduleData = async (strategyName, symbol) => {
1423
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1424
+ const isInitial = !this.getScheduleStorage.has(strategyName);
1425
+ const stateStorage = this.getScheduleStorage(strategyName);
1426
+ await stateStorage.waitForInit(isInitial);
1427
+ if (await stateStorage.hasValue(symbol)) {
1428
+ return await stateStorage.readValue(symbol);
1429
+ }
1430
+ return null;
1431
+ };
1432
+ /**
1433
+ * Writes scheduled signal data to disk with atomic file writes.
1434
+ *
1435
+ * Called by ClientStrategy.setScheduledSignal() to persist state.
1436
+ * Uses atomic writes to prevent corruption on crashes.
1437
+ *
1438
+ * @param scheduledSignalRow - Scheduled signal data (null to clear)
1439
+ * @param strategyName - Strategy identifier
1440
+ * @param symbol - Trading pair symbol
1441
+ * @returns Promise that resolves when write is complete
1442
+ */
1443
+ this.writeScheduleData = async (scheduledSignalRow, strategyName, symbol) => {
1444
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1445
+ const isInitial = !this.getScheduleStorage.has(strategyName);
1446
+ const stateStorage = this.getScheduleStorage(strategyName);
1447
+ await stateStorage.waitForInit(isInitial);
1448
+ await stateStorage.writeValue(symbol, scheduledSignalRow);
1449
+ };
1450
+ }
1451
+ /**
1452
+ * Registers a custom persistence adapter.
1453
+ *
1454
+ * @param Ctor - Custom PersistBase constructor
1455
+ *
1456
+ * @example
1457
+ * ```typescript
1458
+ * class RedisPersist extends PersistBase {
1459
+ * async readValue(id) { return JSON.parse(await redis.get(id)); }
1460
+ * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1461
+ * }
1462
+ * PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
1463
+ * ```
1464
+ */
1465
+ usePersistScheduleAdapter(Ctor) {
1466
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1467
+ this.PersistScheduleFactory = Ctor;
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Global singleton instance of PersistScheduleUtils.
1472
+ * Used by ClientStrategy for scheduled signal persistence.
1473
+ *
1474
+ * @example
1475
+ * ```typescript
1476
+ * // Custom adapter
1477
+ * PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
1478
+ *
1479
+ * // Read scheduled signal
1480
+ * const scheduled = await PersistScheduleAdapter.readScheduleData("my-strategy", "BTCUSDT");
1481
+ *
1482
+ * // Write scheduled signal
1483
+ * await PersistScheduleAdapter.writeScheduleData(scheduled, "my-strategy", "BTCUSDT");
1484
+ * ```
1485
+ */
1486
+ const PersistScheduleAdapter = new PersistScheduleUtils();
1487
+ /**
1488
+ * Utility class for managing partial profit/loss levels persistence.
1489
+ *
1490
+ * Features:
1491
+ * - Memoized storage instances per symbol
1492
+ * - Custom adapter support
1493
+ * - Atomic read/write operations for partial data
1494
+ * - Crash-safe partial state management
1495
+ *
1496
+ * Used by ClientPartial for live mode persistence of profit/loss levels.
1497
+ */
1498
+ class PersistPartialUtils {
1499
+ constructor() {
1500
+ this.PersistPartialFactory = PersistBase;
1501
+ this.getPartialStorage = functoolsKit.memoize(([symbol]) => `${symbol}`, (symbol) => Reflect.construct(this.PersistPartialFactory, [
1502
+ symbol,
1503
+ `./dump/data/partial/`,
1504
+ ]));
1505
+ /**
1506
+ * Reads persisted partial data for a symbol.
1507
+ *
1508
+ * Called by ClientPartial.waitForInit() to restore state.
1509
+ * Returns empty object if no partial data exists.
1510
+ *
1511
+ * @param symbol - Trading pair symbol
1512
+ * @returns Promise resolving to partial data record
1513
+ */
1514
+ this.readPartialData = async (symbol) => {
1515
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1516
+ const isInitial = !this.getPartialStorage.has(symbol);
1517
+ const stateStorage = this.getPartialStorage(symbol);
1518
+ await stateStorage.waitForInit(isInitial);
1519
+ const PARTIAL_STORAGE_KEY = "levels";
1520
+ if (await stateStorage.hasValue(PARTIAL_STORAGE_KEY)) {
1521
+ return await stateStorage.readValue(PARTIAL_STORAGE_KEY);
1522
+ }
1523
+ return {};
1524
+ };
1525
+ /**
1526
+ * Writes partial data to disk with atomic file writes.
1527
+ *
1528
+ * Called by ClientPartial after profit/loss level changes to persist state.
1529
+ * Uses atomic writes to prevent corruption on crashes.
1530
+ *
1531
+ * @param partialData - Record of signal IDs to partial data
1532
+ * @param symbol - Trading pair symbol
1533
+ * @returns Promise that resolves when write is complete
1534
+ */
1535
+ this.writePartialData = async (partialData, symbol) => {
1536
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1537
+ const isInitial = !this.getPartialStorage.has(symbol);
1538
+ const stateStorage = this.getPartialStorage(symbol);
1539
+ await stateStorage.waitForInit(isInitial);
1540
+ const PARTIAL_STORAGE_KEY = "levels";
1541
+ await stateStorage.writeValue(PARTIAL_STORAGE_KEY, partialData);
1542
+ };
1543
+ }
1544
+ /**
1545
+ * Registers a custom persistence adapter.
1546
+ *
1547
+ * @param Ctor - Custom PersistBase constructor
1548
+ *
1549
+ * @example
1550
+ * ```typescript
1551
+ * class RedisPersist extends PersistBase {
1552
+ * async readValue(id) { return JSON.parse(await redis.get(id)); }
1553
+ * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1554
+ * }
1555
+ * PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
1556
+ * ```
1557
+ */
1558
+ usePersistPartialAdapter(Ctor) {
1559
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1560
+ this.PersistPartialFactory = Ctor;
1561
+ }
1562
+ }
1563
+ /**
1564
+ * Global singleton instance of PersistPartialUtils.
1565
+ * Used by ClientPartial for partial profit/loss levels persistence.
1566
+ *
1567
+ * @example
1568
+ * ```typescript
1569
+ * // Custom adapter
1570
+ * PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
1571
+ *
1572
+ * // Read partial data
1573
+ * const partialData = await PersistPartialAdapter.readPartialData("BTCUSDT");
1574
+ *
1575
+ * // Write partial data
1576
+ * await PersistPartialAdapter.writePartialData(partialData, "BTCUSDT");
1577
+ * ```
1578
+ */
1579
+ const PersistPartialAdapter = new PersistPartialUtils();
1377
1580
 
1378
1581
  /**
1379
1582
  * Global signal emitter for all trading events (live + backtest).
@@ -1414,7 +1617,12 @@ const doneWalkerSubject = new functoolsKit.Subject();
1414
1617
  * Progress emitter for backtest execution progress.
1415
1618
  * Emits progress updates during backtest execution.
1416
1619
  */
1417
- const progressEmitter = new functoolsKit.Subject();
1620
+ const progressBacktestEmitter = new functoolsKit.Subject();
1621
+ /**
1622
+ * Progress emitter for walker execution progress.
1623
+ * Emits progress updates during walker execution.
1624
+ */
1625
+ const progressWalkerEmitter = new functoolsKit.Subject();
1418
1626
  /**
1419
1627
  * Performance emitter for execution metrics.
1420
1628
  * Emits performance metrics for profiling and bottleneck detection.
@@ -1430,11 +1638,26 @@ const walkerEmitter = new functoolsKit.Subject();
1430
1638
  * Emits when all strategies have been tested and final results are available.
1431
1639
  */
1432
1640
  const walkerCompleteSubject = new functoolsKit.Subject();
1641
+ /**
1642
+ * Walker stop emitter for walker cancellation events.
1643
+ * Emits when a walker comparison is stopped/cancelled.
1644
+ */
1645
+ const walkerStopSubject = new functoolsKit.Subject();
1433
1646
  /**
1434
1647
  * Validation emitter for risk validation errors.
1435
1648
  * Emits when risk validation functions throw errors during signal checking.
1436
1649
  */
1437
1650
  const validationSubject = new functoolsKit.Subject();
1651
+ /**
1652
+ * Partial profit emitter for profit level milestones.
1653
+ * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
1654
+ */
1655
+ const partialProfitSubject = new functoolsKit.Subject();
1656
+ /**
1657
+ * Partial loss emitter for loss level milestones.
1658
+ * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1659
+ */
1660
+ const partialLossSubject = new functoolsKit.Subject();
1438
1661
 
1439
1662
  var emitters = /*#__PURE__*/Object.freeze({
1440
1663
  __proto__: null,
@@ -1442,14 +1665,18 @@ var emitters = /*#__PURE__*/Object.freeze({
1442
1665
  doneLiveSubject: doneLiveSubject,
1443
1666
  doneWalkerSubject: doneWalkerSubject,
1444
1667
  errorEmitter: errorEmitter,
1668
+ partialLossSubject: partialLossSubject,
1669
+ partialProfitSubject: partialProfitSubject,
1445
1670
  performanceEmitter: performanceEmitter,
1446
- progressEmitter: progressEmitter,
1671
+ progressBacktestEmitter: progressBacktestEmitter,
1672
+ progressWalkerEmitter: progressWalkerEmitter,
1447
1673
  signalBacktestEmitter: signalBacktestEmitter,
1448
1674
  signalEmitter: signalEmitter,
1449
1675
  signalLiveEmitter: signalLiveEmitter,
1450
1676
  validationSubject: validationSubject,
1451
1677
  walkerCompleteSubject: walkerCompleteSubject,
1452
- walkerEmitter: walkerEmitter
1678
+ walkerEmitter: walkerEmitter,
1679
+ walkerStopSubject: walkerStopSubject
1453
1680
  });
1454
1681
 
1455
1682
  const INTERVAL_MINUTES$1 = {
@@ -1462,6 +1689,25 @@ const INTERVAL_MINUTES$1 = {
1462
1689
  };
1463
1690
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1464
1691
  const errors = [];
1692
+ // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
1693
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
1694
+ errors.push('id is required and must be a non-empty string');
1695
+ }
1696
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
1697
+ errors.push('exchangeName is required');
1698
+ }
1699
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
1700
+ errors.push('strategyName is required');
1701
+ }
1702
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
1703
+ errors.push('symbol is required and must be a non-empty string');
1704
+ }
1705
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
1706
+ errors.push('_isScheduled is required');
1707
+ }
1708
+ if (signal.position === undefined || signal.position === null) {
1709
+ errors.push('position is required and must be "long" or "short"');
1710
+ }
1465
1711
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1466
1712
  if (!isFinite(currentPrice)) {
1467
1713
  errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
@@ -1711,26 +1957,42 @@ const GET_AVG_PRICE_FN = (candles) => {
1711
1957
  ? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
1712
1958
  : sumPriceVolume / totalVolume;
1713
1959
  };
1714
- const WAIT_FOR_INIT_FN$1 = async (self) => {
1960
+ const WAIT_FOR_INIT_FN$2 = async (self) => {
1715
1961
  self.params.logger.debug("ClientStrategy waitForInit");
1716
1962
  if (self.params.execution.context.backtest) {
1717
1963
  return;
1718
1964
  }
1965
+ // Restore pending signal
1719
1966
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
1720
- if (!pendingSignal) {
1721
- return;
1722
- }
1723
- if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
1724
- return;
1725
- }
1726
- if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
1727
- return;
1967
+ if (pendingSignal) {
1968
+ if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
1969
+ return;
1970
+ }
1971
+ if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
1972
+ return;
1973
+ }
1974
+ self._pendingSignal = pendingSignal;
1975
+ // Call onActive callback for restored signal
1976
+ if (self.params.callbacks?.onActive) {
1977
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1978
+ self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
1979
+ }
1728
1980
  }
1729
- self._pendingSignal = pendingSignal;
1730
- // Call onActive callback for restored signal
1731
- if (self.params.callbacks?.onActive) {
1732
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1733
- self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
1981
+ // Restore scheduled signal
1982
+ const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.strategyName, self.params.execution.context.symbol);
1983
+ if (scheduledSignal) {
1984
+ if (scheduledSignal.exchangeName !== self.params.method.context.exchangeName) {
1985
+ return;
1986
+ }
1987
+ if (scheduledSignal.strategyName !== self.params.method.context.strategyName) {
1988
+ return;
1989
+ }
1990
+ self._scheduledSignal = scheduledSignal;
1991
+ // Call onSchedule callback for restored scheduled signal
1992
+ if (self.params.callbacks?.onSchedule) {
1993
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1994
+ self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
1995
+ }
1734
1996
  }
1735
1997
  };
1736
1998
  const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
@@ -1747,7 +2009,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
1747
2009
  elapsedMinutes: Math.floor(elapsedTime / 60000),
1748
2010
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
1749
2011
  });
1750
- self._scheduledSignal = null;
2012
+ await self.setScheduledSignal(null);
1751
2013
  if (self.params.callbacks?.onCancel) {
1752
2014
  self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
1753
2015
  }
@@ -1802,7 +2064,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
1802
2064
  averagePrice: currentPrice,
1803
2065
  priceStopLoss: scheduled.priceStopLoss,
1804
2066
  });
1805
- self._scheduledSignal = null;
2067
+ await self.setScheduledSignal(null);
1806
2068
  const result = {
1807
2069
  action: "idle",
1808
2070
  signal: null,
@@ -1823,7 +2085,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
1823
2085
  symbol: self.params.execution.context.symbol,
1824
2086
  signalId: scheduled.id,
1825
2087
  });
1826
- self._scheduledSignal = null;
2088
+ await self.setScheduledSignal(null);
1827
2089
  return null;
1828
2090
  }
1829
2091
  // В LIVE режиме activationTimestamp - это текущее время при tick()
@@ -1851,10 +2113,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
1851
2113
  symbol: self.params.execution.context.symbol,
1852
2114
  signalId: scheduled.id,
1853
2115
  });
1854
- self._scheduledSignal = null;
2116
+ await self.setScheduledSignal(null);
1855
2117
  return null;
1856
2118
  }
1857
- self._scheduledSignal = null;
2119
+ await self.setScheduledSignal(null);
1858
2120
  // КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
1859
2121
  const activatedSignal = {
1860
2122
  ...scheduled,
@@ -1992,6 +2254,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
1992
2254
  if (self.params.callbacks?.onClose) {
1993
2255
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
1994
2256
  }
2257
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2258
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
1995
2259
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
1996
2260
  strategyName: self.params.method.context.strategyName,
1997
2261
  riskName: self.params.riskName,
@@ -2014,6 +2278,34 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2014
2278
  return result;
2015
2279
  };
2016
2280
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2281
+ // Calculate revenue percentage for partial fill/loss callbacks
2282
+ {
2283
+ let revenuePercent = 0;
2284
+ if (signal.position === "long") {
2285
+ // For long: positive if current > open, negative if current < open
2286
+ revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
2287
+ }
2288
+ else if (signal.position === "short") {
2289
+ // For short: positive if current < open, negative if current > open
2290
+ revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
2291
+ }
2292
+ // Call onPartialProfit if revenue is positive (but not reached TP yet)
2293
+ if (revenuePercent > 0) {
2294
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2295
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2296
+ if (self.params.callbacks?.onPartialProfit) {
2297
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2298
+ }
2299
+ }
2300
+ // Call onPartialLoss if revenue is negative (but not hit SL yet)
2301
+ if (revenuePercent < 0) {
2302
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2303
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2304
+ if (self.params.callbacks?.onPartialLoss) {
2305
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2306
+ }
2307
+ }
2308
+ }
2017
2309
  const result = {
2018
2310
  action: "active",
2019
2311
  signal: signal,
@@ -2052,7 +2344,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2052
2344
  averagePrice,
2053
2345
  priceStopLoss: scheduled.priceStopLoss,
2054
2346
  });
2055
- self._scheduledSignal = null;
2347
+ await self.setScheduledSignal(null);
2056
2348
  if (self.params.callbacks?.onCancel) {
2057
2349
  self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
2058
2350
  }
@@ -2077,7 +2369,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2077
2369
  symbol: self.params.execution.context.symbol,
2078
2370
  signalId: scheduled.id,
2079
2371
  });
2080
- self._scheduledSignal = null;
2372
+ await self.setScheduledSignal(null);
2081
2373
  return false;
2082
2374
  }
2083
2375
  // В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
@@ -2102,10 +2394,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2102
2394
  symbol: self.params.execution.context.symbol,
2103
2395
  signalId: scheduled.id,
2104
2396
  });
2105
- self._scheduledSignal = null;
2397
+ await self.setScheduledSignal(null);
2106
2398
  return false;
2107
2399
  }
2108
- self._scheduledSignal = null;
2400
+ await self.setScheduledSignal(null);
2109
2401
  // КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
2110
2402
  const activatedSignal = {
2111
2403
  ...scheduled,
@@ -2141,6 +2433,8 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2141
2433
  if (self.params.callbacks?.onClose) {
2142
2434
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2143
2435
  }
2436
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2437
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
2144
2438
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2145
2439
  strategyName: self.params.method.context.strategyName,
2146
2440
  riskName: self.params.riskName,
@@ -2282,6 +2576,33 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2282
2576
  }
2283
2577
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2284
2578
  }
2579
+ // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
2580
+ // Calculate revenue percentage
2581
+ {
2582
+ let revenuePercent = 0;
2583
+ if (signal.position === "long") {
2584
+ revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
2585
+ }
2586
+ else if (signal.position === "short") {
2587
+ revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
2588
+ }
2589
+ // Call onPartialProfit if revenue is positive (but not reached TP yet)
2590
+ if (revenuePercent > 0) {
2591
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2592
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2593
+ if (self.params.callbacks?.onPartialProfit) {
2594
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2595
+ }
2596
+ }
2597
+ // Call onPartialLoss if revenue is negative (but not hit SL yet)
2598
+ if (revenuePercent < 0) {
2599
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2600
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2601
+ if (self.params.callbacks?.onPartialLoss) {
2602
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2603
+ }
2604
+ }
2605
+ }
2285
2606
  }
2286
2607
  return null;
2287
2608
  };
@@ -2328,7 +2649,7 @@ class ClientStrategy {
2328
2649
  *
2329
2650
  * @returns Promise that resolves when initialization is complete
2330
2651
  */
2331
- this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
2652
+ this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$2(this));
2332
2653
  }
2333
2654
  /**
2334
2655
  * Updates pending signal and persists to disk in live mode.
@@ -2354,6 +2675,25 @@ class ClientStrategy {
2354
2675
  }
2355
2676
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
2356
2677
  }
2678
+ /**
2679
+ * Updates scheduled signal and persists to disk in live mode.
2680
+ *
2681
+ * Centralized method for all scheduled signal state changes.
2682
+ * Uses atomic file writes to prevent corruption.
2683
+ *
2684
+ * @param scheduledSignal - New scheduled signal state (null to clear)
2685
+ * @returns Promise that resolves when update is complete
2686
+ */
2687
+ async setScheduledSignal(scheduledSignal) {
2688
+ this.params.logger.debug("ClientStrategy setScheduledSignal", {
2689
+ scheduledSignal,
2690
+ });
2691
+ this._scheduledSignal = scheduledSignal;
2692
+ if (this.params.execution.context.backtest) {
2693
+ return;
2694
+ }
2695
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.strategyName, this.params.execution.context.symbol);
2696
+ }
2357
2697
  /**
2358
2698
  * Retrieves the current pending signal.
2359
2699
  * If no signal is pending, returns null.
@@ -2428,7 +2768,7 @@ class ClientStrategy {
2428
2768
  const signal = await GET_SIGNAL_FN(this);
2429
2769
  if (signal) {
2430
2770
  if (signal._isScheduled === true) {
2431
- this._scheduledSignal = signal;
2771
+ await this.setScheduledSignal(signal);
2432
2772
  return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
2433
2773
  }
2434
2774
  await this.setPendingSignal(signal);
@@ -2604,7 +2944,7 @@ class ClientStrategy {
2604
2944
  this._isStopped = true;
2605
2945
  // Clear scheduled signal if exists
2606
2946
  if (this._scheduledSignal) {
2607
- this._scheduledSignal = null;
2947
+ await this.setScheduledSignal(null);
2608
2948
  }
2609
2949
  }
2610
2950
  }
@@ -2643,6 +2983,7 @@ class StrategyConnectionService {
2643
2983
  this.riskConnectionService = inject(TYPES.riskConnectionService);
2644
2984
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
2645
2985
  this.methodContextService = inject(TYPES.methodContextService);
2986
+ this.partialConnectionService = inject(TYPES.partialConnectionService);
2646
2987
  /**
2647
2988
  * Retrieves memoized ClientStrategy instance for given strategy name.
2648
2989
  *
@@ -2659,6 +3000,7 @@ class StrategyConnectionService {
2659
3000
  execution: this.executionContextService,
2660
3001
  method: this.methodContextService,
2661
3002
  logger: this.loggerService,
3003
+ partial: this.partialConnectionService,
2662
3004
  exchange: this.exchangeConnectionService,
2663
3005
  risk: riskName ? this.riskConnectionService.getRisk(riskName) : NOOP_RISK,
2664
3006
  riskName,
@@ -2671,11 +3013,14 @@ class StrategyConnectionService {
2671
3013
  * Retrieves the currently active pending signal for the strategy.
2672
3014
  * If no active signal exists, returns null.
2673
3015
  * Used internally for monitoring TP/SL and time expiration.
3016
+ *
3017
+ * @param strategyName - Name of strategy to get pending signal for
3018
+ *
2674
3019
  * @returns Promise resolving to pending signal or null
2675
3020
  */
2676
- this.getPendingSignal = async () => {
3021
+ this.getPendingSignal = async (strategyName) => {
2677
3022
  this.loggerService.log("strategyConnectionService getPendingSignal");
2678
- const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
3023
+ const strategy = await this.getStrategy(strategyName);
2679
3024
  return await strategy.getPendingSignal();
2680
3025
  };
2681
3026
  /**
@@ -3142,7 +3487,7 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
3142
3487
  * Uses singleshot pattern to ensure it only runs once.
3143
3488
  * This function is exported for use in tests or other modules.
3144
3489
  */
3145
- const WAIT_FOR_INIT_FN = async (self) => {
3490
+ const WAIT_FOR_INIT_FN$1 = async (self) => {
3146
3491
  self.params.logger.debug("ClientRisk waitForInit");
3147
3492
  const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName);
3148
3493
  self._activePositions = new Map(persistedPositions);
@@ -3173,7 +3518,7 @@ class ClientRisk {
3173
3518
  * Uses singleshot pattern to ensure initialization happens exactly once.
3174
3519
  * Skips persistence in backtest mode.
3175
3520
  */
3176
- this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN(this));
3521
+ this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
3177
3522
  /**
3178
3523
  * Checks if a signal should be allowed based on risk limits.
3179
3524
  *
@@ -3590,20 +3935,12 @@ class StrategyGlobalService {
3590
3935
  * @param backtest - Whether running in backtest mode
3591
3936
  * @returns Promise resolving to pending signal or null
3592
3937
  */
3593
- this.getPendingSignal = async (symbol, when, backtest) => {
3938
+ this.getPendingSignal = async (strategyName) => {
3594
3939
  this.loggerService.log("strategyGlobalService getPendingSignal", {
3595
- symbol,
3596
- when,
3597
- backtest,
3940
+ strategyName,
3598
3941
  });
3599
3942
  await this.validate(this.methodContextService.context.strategyName);
3600
- return await ExecutionContextService.runInContext(async () => {
3601
- return await this.strategyConnectionService.getPendingSignal();
3602
- }, {
3603
- symbol,
3604
- when,
3605
- backtest,
3606
- });
3943
+ return await this.strategyConnectionService.getPendingSignal(strategyName);
3607
3944
  };
3608
3945
  /**
3609
3946
  * Checks signal status at a specific timestamp.
@@ -4356,7 +4693,7 @@ class BacktestLogicPrivateService {
4356
4693
  const when = timeframes[i];
4357
4694
  // Emit progress event if context is available
4358
4695
  {
4359
- await progressEmitter.next({
4696
+ await progressBacktestEmitter.next({
4360
4697
  exchangeName: this.methodContextService.context.exchangeName,
4361
4698
  strategyName: this.methodContextService.context.strategyName,
4362
4699
  symbol,
@@ -4490,7 +4827,7 @@ class BacktestLogicPrivateService {
4490
4827
  }
4491
4828
  // Emit final progress event (100%)
4492
4829
  {
4493
- await progressEmitter.next({
4830
+ await progressBacktestEmitter.next({
4494
4831
  exchangeName: this.methodContextService.context.exchangeName,
4495
4832
  strategyName: this.methodContextService.context.strategyName,
4496
4833
  symbol,
@@ -4607,6 +4944,7 @@ class LiveLogicPrivateService {
4607
4944
  }
4608
4945
  }
4609
4946
 
4947
+ const CANCEL_SYMBOL = Symbol("CANCEL_SYMBOL");
4610
4948
  /**
4611
4949
  * Private service for walker orchestration (strategy comparison).
4612
4950
  *
@@ -4664,6 +5002,10 @@ class WalkerLogicPrivateService {
4664
5002
  let strategiesTested = 0;
4665
5003
  let bestMetric = null;
4666
5004
  let bestStrategy = null;
5005
+ const listenStop = walkerStopSubject
5006
+ .filter((walkerName) => walkerName === context.walkerName)
5007
+ .map(() => CANCEL_SYMBOL)
5008
+ .toPromise();
4667
5009
  // Run backtest for each strategy
4668
5010
  for (const strategyName of strategies) {
4669
5011
  // Call onStrategyStart callback if provided
@@ -4679,7 +5021,16 @@ class WalkerLogicPrivateService {
4679
5021
  exchangeName: context.exchangeName,
4680
5022
  frameName: context.frameName,
4681
5023
  });
4682
- await functoolsKit.resolveDocuments(iterator);
5024
+ const result = await Promise.race([
5025
+ await functoolsKit.resolveDocuments(iterator),
5026
+ listenStop,
5027
+ ]);
5028
+ if (result === CANCEL_SYMBOL) {
5029
+ this.loggerService.info("walkerLogicPrivateService received stop signal, cancelling walker", {
5030
+ context,
5031
+ });
5032
+ break;
5033
+ }
4683
5034
  this.loggerService.info("walkerLogicPrivateService backtest complete", {
4684
5035
  strategyName,
4685
5036
  symbol,
@@ -4717,6 +5068,16 @@ class WalkerLogicPrivateService {
4717
5068
  strategiesTested,
4718
5069
  totalStrategies: strategies.length,
4719
5070
  };
5071
+ // Emit progress event
5072
+ await progressWalkerEmitter.next({
5073
+ walkerName: context.walkerName,
5074
+ exchangeName: context.exchangeName,
5075
+ frameName: context.frameName,
5076
+ symbol,
5077
+ totalStrategies: strategies.length,
5078
+ processedStrategies: strategiesTested,
5079
+ progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
5080
+ });
4720
5081
  // Call onStrategyComplete callback if provided
4721
5082
  if (walkerSchema.callbacks?.onStrategyComplete) {
4722
5083
  walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
@@ -5062,7 +5423,7 @@ function isUnsafe$3(value) {
5062
5423
  }
5063
5424
  return false;
5064
5425
  }
5065
- const columns$3 = [
5426
+ const columns$4 = [
5066
5427
  {
5067
5428
  key: "signalId",
5068
5429
  label: "Signal ID",
@@ -5140,7 +5501,7 @@ const columns$3 = [
5140
5501
  * Storage class for accumulating closed signals per strategy.
5141
5502
  * Maintains a list of all closed signals and provides methods to generate reports.
5142
5503
  */
5143
- let ReportStorage$3 = class ReportStorage {
5504
+ let ReportStorage$4 = class ReportStorage {
5144
5505
  constructor() {
5145
5506
  /** Internal list of all closed signals for this strategy */
5146
5507
  this._signalList = [];
@@ -5230,9 +5591,9 @@ let ReportStorage$3 = class ReportStorage {
5230
5591
  if (stats.totalSignals === 0) {
5231
5592
  return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
5232
5593
  }
5233
- const header = columns$3.map((col) => col.label);
5234
- const separator = columns$3.map(() => "---");
5235
- const rows = this._signalList.map((closedSignal) => columns$3.map((col) => col.format(closedSignal)));
5594
+ const header = columns$4.map((col) => col.label);
5595
+ const separator = columns$4.map(() => "---");
5596
+ const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5236
5597
  const tableData = [header, separator, ...rows];
5237
5598
  const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5238
5599
  return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
@@ -5241,9 +5602,9 @@ let ReportStorage$3 = class ReportStorage {
5241
5602
  * Saves strategy report to disk.
5242
5603
  *
5243
5604
  * @param strategyName - Strategy name
5244
- * @param path - Directory path to save report (default: "./logs/backtest")
5605
+ * @param path - Directory path to save report (default: "./dump/backtest")
5245
5606
  */
5246
- async dump(strategyName, path$1 = "./logs/backtest") {
5607
+ async dump(strategyName, path$1 = "./dump/backtest") {
5247
5608
  const markdown = await this.getReport(strategyName);
5248
5609
  try {
5249
5610
  const dir = path.join(process.cwd(), path$1);
@@ -5293,7 +5654,7 @@ class BacktestMarkdownService {
5293
5654
  * Memoized function to get or create ReportStorage for a strategy.
5294
5655
  * Each strategy gets its own isolated storage instance.
5295
5656
  */
5296
- this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
5657
+ this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$4());
5297
5658
  /**
5298
5659
  * Processes tick events and accumulates closed signals.
5299
5660
  * Should be called from IStrategyCallbacks.onTick.
@@ -5371,20 +5732,20 @@ class BacktestMarkdownService {
5371
5732
  * Delegates to ReportStorage.dump().
5372
5733
  *
5373
5734
  * @param strategyName - Strategy name to save report for
5374
- * @param path - Directory path to save report (default: "./logs/backtest")
5735
+ * @param path - Directory path to save report (default: "./dump/backtest")
5375
5736
  *
5376
5737
  * @example
5377
5738
  * ```typescript
5378
5739
  * const service = new BacktestMarkdownService();
5379
5740
  *
5380
- * // Save to default path: ./logs/backtest/my-strategy.md
5741
+ * // Save to default path: ./dump/backtest/my-strategy.md
5381
5742
  * await service.dump("my-strategy");
5382
5743
  *
5383
5744
  * // Save to custom path: ./custom/path/my-strategy.md
5384
5745
  * await service.dump("my-strategy", "./custom/path");
5385
5746
  * ```
5386
5747
  */
5387
- this.dump = async (strategyName, path = "./logs/backtest") => {
5748
+ this.dump = async (strategyName, path = "./dump/backtest") => {
5388
5749
  this.loggerService.log("backtestMarkdownService dump", {
5389
5750
  strategyName,
5390
5751
  path,
@@ -5452,7 +5813,7 @@ function isUnsafe$2(value) {
5452
5813
  }
5453
5814
  return false;
5454
5815
  }
5455
- const columns$2 = [
5816
+ const columns$3 = [
5456
5817
  {
5457
5818
  key: "timestamp",
5458
5819
  label: "Timestamp",
@@ -5526,12 +5887,12 @@ const columns$2 = [
5526
5887
  },
5527
5888
  ];
5528
5889
  /** Maximum number of events to store in live trading reports */
5529
- const MAX_EVENTS$2 = 250;
5890
+ const MAX_EVENTS$3 = 250;
5530
5891
  /**
5531
5892
  * Storage class for accumulating all tick events per strategy.
5532
5893
  * Maintains a chronological list of all events (idle, opened, active, closed).
5533
5894
  */
5534
- let ReportStorage$2 = class ReportStorage {
5895
+ let ReportStorage$3 = class ReportStorage {
5535
5896
  constructor() {
5536
5897
  /** Internal list of all tick events for this strategy */
5537
5898
  this._eventList = [];
@@ -5559,7 +5920,7 @@ let ReportStorage$2 = class ReportStorage {
5559
5920
  }
5560
5921
  {
5561
5922
  this._eventList.push(newEvent);
5562
- if (this._eventList.length > MAX_EVENTS$2) {
5923
+ if (this._eventList.length > MAX_EVENTS$3) {
5563
5924
  this._eventList.shift();
5564
5925
  }
5565
5926
  }
@@ -5583,7 +5944,7 @@ let ReportStorage$2 = class ReportStorage {
5583
5944
  stopLoss: data.signal.priceStopLoss,
5584
5945
  });
5585
5946
  // Trim queue if exceeded MAX_EVENTS
5586
- if (this._eventList.length > MAX_EVENTS$2) {
5947
+ if (this._eventList.length > MAX_EVENTS$3) {
5587
5948
  this._eventList.shift();
5588
5949
  }
5589
5950
  }
@@ -5615,7 +5976,7 @@ let ReportStorage$2 = class ReportStorage {
5615
5976
  else {
5616
5977
  this._eventList.push(newEvent);
5617
5978
  // Trim queue if exceeded MAX_EVENTS
5618
- if (this._eventList.length > MAX_EVENTS$2) {
5979
+ if (this._eventList.length > MAX_EVENTS$3) {
5619
5980
  this._eventList.shift();
5620
5981
  }
5621
5982
  }
@@ -5653,7 +6014,7 @@ let ReportStorage$2 = class ReportStorage {
5653
6014
  else {
5654
6015
  this._eventList.push(newEvent);
5655
6016
  // Trim queue if exceeded MAX_EVENTS
5656
- if (this._eventList.length > MAX_EVENTS$2) {
6017
+ if (this._eventList.length > MAX_EVENTS$3) {
5657
6018
  this._eventList.shift();
5658
6019
  }
5659
6020
  }
@@ -5750,9 +6111,9 @@ let ReportStorage$2 = class ReportStorage {
5750
6111
  if (stats.totalEvents === 0) {
5751
6112
  return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
5752
6113
  }
5753
- const header = columns$2.map((col) => col.label);
5754
- const separator = columns$2.map(() => "---");
5755
- const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6114
+ const header = columns$3.map((col) => col.label);
6115
+ const separator = columns$3.map(() => "---");
6116
+ const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
5756
6117
  const tableData = [header, separator, ...rows];
5757
6118
  const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5758
6119
  return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
@@ -5761,9 +6122,9 @@ let ReportStorage$2 = class ReportStorage {
5761
6122
  * Saves strategy report to disk.
5762
6123
  *
5763
6124
  * @param strategyName - Strategy name
5764
- * @param path - Directory path to save report (default: "./logs/live")
6125
+ * @param path - Directory path to save report (default: "./dump/live")
5765
6126
  */
5766
- async dump(strategyName, path$1 = "./logs/live") {
6127
+ async dump(strategyName, path$1 = "./dump/live") {
5767
6128
  const markdown = await this.getReport(strategyName);
5768
6129
  try {
5769
6130
  const dir = path.join(process.cwd(), path$1);
@@ -5816,7 +6177,7 @@ class LiveMarkdownService {
5816
6177
  * Memoized function to get or create ReportStorage for a strategy.
5817
6178
  * Each strategy gets its own isolated storage instance.
5818
6179
  */
5819
- this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
6180
+ this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
5820
6181
  /**
5821
6182
  * Processes tick events and accumulates all event types.
5822
6183
  * Should be called from IStrategyCallbacks.onTick.
@@ -5904,20 +6265,20 @@ class LiveMarkdownService {
5904
6265
  * Delegates to ReportStorage.dump().
5905
6266
  *
5906
6267
  * @param strategyName - Strategy name to save report for
5907
- * @param path - Directory path to save report (default: "./logs/live")
6268
+ * @param path - Directory path to save report (default: "./dump/live")
5908
6269
  *
5909
6270
  * @example
5910
6271
  * ```typescript
5911
6272
  * const service = new LiveMarkdownService();
5912
6273
  *
5913
- * // Save to default path: ./logs/live/my-strategy.md
6274
+ * // Save to default path: ./dump/live/my-strategy.md
5914
6275
  * await service.dump("my-strategy");
5915
6276
  *
5916
6277
  * // Save to custom path: ./custom/path/my-strategy.md
5917
6278
  * await service.dump("my-strategy", "./custom/path");
5918
6279
  * ```
5919
6280
  */
5920
- this.dump = async (strategyName, path = "./logs/live") => {
6281
+ this.dump = async (strategyName, path = "./dump/live") => {
5921
6282
  this.loggerService.log("liveMarkdownService dump", {
5922
6283
  strategyName,
5923
6284
  path,
@@ -5967,7 +6328,7 @@ class LiveMarkdownService {
5967
6328
  }
5968
6329
  }
5969
6330
 
5970
- const columns$1 = [
6331
+ const columns$2 = [
5971
6332
  {
5972
6333
  key: "timestamp",
5973
6334
  label: "Timestamp",
@@ -6025,12 +6386,12 @@ const columns$1 = [
6025
6386
  },
6026
6387
  ];
6027
6388
  /** Maximum number of events to store in schedule reports */
6028
- const MAX_EVENTS$1 = 250;
6389
+ const MAX_EVENTS$2 = 250;
6029
6390
  /**
6030
6391
  * Storage class for accumulating scheduled signal events per strategy.
6031
6392
  * Maintains a chronological list of scheduled and cancelled events.
6032
6393
  */
6033
- let ReportStorage$1 = class ReportStorage {
6394
+ let ReportStorage$2 = class ReportStorage {
6034
6395
  constructor() {
6035
6396
  /** Internal list of all scheduled events for this strategy */
6036
6397
  this._eventList = [];
@@ -6054,7 +6415,7 @@ let ReportStorage$1 = class ReportStorage {
6054
6415
  stopLoss: data.signal.priceStopLoss,
6055
6416
  });
6056
6417
  // Trim queue if exceeded MAX_EVENTS
6057
- if (this._eventList.length > MAX_EVENTS$1) {
6418
+ if (this._eventList.length > MAX_EVENTS$2) {
6058
6419
  this._eventList.shift();
6059
6420
  }
6060
6421
  }
@@ -6090,7 +6451,7 @@ let ReportStorage$1 = class ReportStorage {
6090
6451
  else {
6091
6452
  this._eventList.push(newEvent);
6092
6453
  // Trim queue if exceeded MAX_EVENTS
6093
- if (this._eventList.length > MAX_EVENTS$1) {
6454
+ if (this._eventList.length > MAX_EVENTS$2) {
6094
6455
  this._eventList.shift();
6095
6456
  }
6096
6457
  }
@@ -6142,9 +6503,9 @@ let ReportStorage$1 = class ReportStorage {
6142
6503
  if (stats.totalEvents === 0) {
6143
6504
  return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
6144
6505
  }
6145
- const header = columns$1.map((col) => col.label);
6146
- const separator = columns$1.map(() => "---");
6147
- const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
6506
+ const header = columns$2.map((col) => col.label);
6507
+ const separator = columns$2.map(() => "---");
6508
+ const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6148
6509
  const tableData = [header, separator, ...rows];
6149
6510
  const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
6150
6511
  return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
@@ -6153,9 +6514,9 @@ let ReportStorage$1 = class ReportStorage {
6153
6514
  * Saves strategy report to disk.
6154
6515
  *
6155
6516
  * @param strategyName - Strategy name
6156
- * @param path - Directory path to save report (default: "./logs/schedule")
6517
+ * @param path - Directory path to save report (default: "./dump/schedule")
6157
6518
  */
6158
- async dump(strategyName, path$1 = "./logs/schedule") {
6519
+ async dump(strategyName, path$1 = "./dump/schedule") {
6159
6520
  const markdown = await this.getReport(strategyName);
6160
6521
  try {
6161
6522
  const dir = path.join(process.cwd(), path$1);
@@ -6199,7 +6560,7 @@ class ScheduleMarkdownService {
6199
6560
  * Memoized function to get or create ReportStorage for a strategy.
6200
6561
  * Each strategy gets its own isolated storage instance.
6201
6562
  */
6202
- this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
6563
+ this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
6203
6564
  /**
6204
6565
  * Processes tick events and accumulates scheduled/cancelled events.
6205
6566
  * Should be called from signalLiveEmitter subscription.
@@ -6274,20 +6635,20 @@ class ScheduleMarkdownService {
6274
6635
  * Delegates to ReportStorage.dump().
6275
6636
  *
6276
6637
  * @param strategyName - Strategy name to save report for
6277
- * @param path - Directory path to save report (default: "./logs/schedule")
6638
+ * @param path - Directory path to save report (default: "./dump/schedule")
6278
6639
  *
6279
6640
  * @example
6280
6641
  * ```typescript
6281
6642
  * const service = new ScheduleMarkdownService();
6282
6643
  *
6283
- * // Save to default path: ./logs/schedule/my-strategy.md
6644
+ * // Save to default path: ./dump/schedule/my-strategy.md
6284
6645
  * await service.dump("my-strategy");
6285
6646
  *
6286
6647
  * // Save to custom path: ./custom/path/my-strategy.md
6287
6648
  * await service.dump("my-strategy", "./custom/path");
6288
6649
  * ```
6289
6650
  */
6290
- this.dump = async (strategyName, path = "./logs/schedule") => {
6651
+ this.dump = async (strategyName, path = "./dump/schedule") => {
6291
6652
  this.loggerService.log("scheduleMarkdownService dump", {
6292
6653
  strategyName,
6293
6654
  path,
@@ -6347,7 +6708,7 @@ function percentile(sortedArray, p) {
6347
6708
  return sortedArray[Math.max(0, index)];
6348
6709
  }
6349
6710
  /** Maximum number of performance events to store per strategy */
6350
- const MAX_EVENTS = 10000;
6711
+ const MAX_EVENTS$1 = 10000;
6351
6712
  /**
6352
6713
  * Storage class for accumulating performance metrics per strategy.
6353
6714
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -6365,7 +6726,7 @@ class PerformanceStorage {
6365
6726
  addEvent(event) {
6366
6727
  this._events.push(event);
6367
6728
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
6368
- if (this._events.length > MAX_EVENTS) {
6729
+ if (this._events.length > MAX_EVENTS$1) {
6369
6730
  this._events.shift();
6370
6731
  }
6371
6732
  }
@@ -6504,7 +6865,7 @@ class PerformanceStorage {
6504
6865
  * @param strategyName - Strategy name
6505
6866
  * @param path - Directory path to save report
6506
6867
  */
6507
- async dump(strategyName, path$1 = "./logs/performance") {
6868
+ async dump(strategyName, path$1 = "./dump/performance") {
6508
6869
  const markdown = await this.getReport(strategyName);
6509
6870
  try {
6510
6871
  const dir = path.join(process.cwd(), path$1);
@@ -6617,14 +6978,14 @@ class PerformanceMarkdownService {
6617
6978
  *
6618
6979
  * @example
6619
6980
  * ```typescript
6620
- * // Save to default path: ./logs/performance/my-strategy.md
6981
+ * // Save to default path: ./dump/performance/my-strategy.md
6621
6982
  * await performanceService.dump("my-strategy");
6622
6983
  *
6623
6984
  * // Save to custom path
6624
6985
  * await performanceService.dump("my-strategy", "./custom/path");
6625
6986
  * ```
6626
6987
  */
6627
- this.dump = async (strategyName, path = "./logs/performance") => {
6988
+ this.dump = async (strategyName, path = "./dump/performance") => {
6628
6989
  this.loggerService.log("performanceMarkdownService dump", {
6629
6990
  strategyName,
6630
6991
  path,
@@ -6686,7 +7047,7 @@ function formatMetric(value) {
6686
7047
  * Storage class for accumulating walker results.
6687
7048
  * Maintains a list of all strategy results and provides methods to generate reports.
6688
7049
  */
6689
- class ReportStorage {
7050
+ let ReportStorage$1 = class ReportStorage {
6690
7051
  constructor(walkerName) {
6691
7052
  this.walkerName = walkerName;
6692
7053
  /** Walker metadata (set from first addResult call) */
@@ -6754,9 +7115,9 @@ class ReportStorage {
6754
7115
  * @param symbol - Trading symbol
6755
7116
  * @param metric - Metric being optimized
6756
7117
  * @param context - Context with exchangeName and frameName
6757
- * @param path - Directory path to save report (default: "./logs/walker")
7118
+ * @param path - Directory path to save report (default: "./dump/walker")
6758
7119
  */
6759
- async dump(symbol, metric, context, path$1 = "./logs/walker") {
7120
+ async dump(symbol, metric, context, path$1 = "./dump/walker") {
6760
7121
  const markdown = await this.getReport(symbol, metric, context);
6761
7122
  try {
6762
7123
  const dir = path.join(process.cwd(), path$1);
@@ -6770,7 +7131,7 @@ class ReportStorage {
6770
7131
  console.error(`Failed to save walker report:`, error);
6771
7132
  }
6772
7133
  }
6773
- }
7134
+ };
6774
7135
  /**
6775
7136
  * Service for generating and saving walker markdown reports.
6776
7137
  *
@@ -6795,7 +7156,7 @@ class WalkerMarkdownService {
6795
7156
  * Memoized function to get or create ReportStorage for a walker.
6796
7157
  * Each walker gets its own isolated storage instance.
6797
7158
  */
6798
- this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage(walkerName));
7159
+ this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
6799
7160
  /**
6800
7161
  * Processes walker progress events and accumulates strategy results.
6801
7162
  * Should be called from walkerEmitter.
@@ -6878,20 +7239,20 @@ class WalkerMarkdownService {
6878
7239
  * @param symbol - Trading symbol
6879
7240
  * @param metric - Metric being optimized
6880
7241
  * @param context - Context with exchangeName and frameName
6881
- * @param path - Directory path to save report (default: "./logs/walker")
7242
+ * @param path - Directory path to save report (default: "./dump/walker")
6882
7243
  *
6883
7244
  * @example
6884
7245
  * ```typescript
6885
7246
  * const service = new WalkerMarkdownService();
6886
7247
  *
6887
- * // Save to default path: ./logs/walker/my-walker.md
7248
+ * // Save to default path: ./dump/walker/my-walker.md
6888
7249
  * await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
6889
7250
  *
6890
7251
  * // Save to custom path: ./custom/path/my-walker.md
6891
7252
  * await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" }, "./custom/path");
6892
7253
  * ```
6893
7254
  */
6894
- this.dump = async (walkerName, symbol, metric, context, path = "./logs/walker") => {
7255
+ this.dump = async (walkerName, symbol, metric, context, path = "./dump/walker") => {
6895
7256
  this.loggerService.log("walkerMarkdownService dump", {
6896
7257
  walkerName,
6897
7258
  symbol,
@@ -6966,7 +7327,7 @@ function isUnsafe(value) {
6966
7327
  }
6967
7328
  return false;
6968
7329
  }
6969
- const columns = [
7330
+ const columns$1 = [
6970
7331
  {
6971
7332
  key: "symbol",
6972
7333
  label: "Symbol",
@@ -7262,9 +7623,9 @@ class HeatmapStorage {
7262
7623
  if (data.symbols.length === 0) {
7263
7624
  return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
7264
7625
  }
7265
- const header = columns.map((col) => col.label);
7266
- const separator = columns.map(() => "---");
7267
- const rows = data.symbols.map((row) => columns.map((col) => col.format(row)));
7626
+ const header = columns$1.map((col) => col.label);
7627
+ const separator = columns$1.map(() => "---");
7628
+ const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
7268
7629
  const tableData = [header, separator, ...rows];
7269
7630
  const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7270
7631
  return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
@@ -7273,9 +7634,9 @@ class HeatmapStorage {
7273
7634
  * Saves heatmap report to disk.
7274
7635
  *
7275
7636
  * @param strategyName - Strategy name for filename
7276
- * @param path - Directory path to save report (default: "./logs/heatmap")
7637
+ * @param path - Directory path to save report (default: "./dump/heatmap")
7277
7638
  */
7278
- async dump(strategyName, path$1 = "./logs/heatmap") {
7639
+ async dump(strategyName, path$1 = "./dump/heatmap") {
7279
7640
  const markdown = await this.getReport(strategyName);
7280
7641
  try {
7281
7642
  const dir = path.join(process.cwd(), path$1);
@@ -7406,20 +7767,20 @@ class HeatMarkdownService {
7406
7767
  * Default filename: {strategyName}.md
7407
7768
  *
7408
7769
  * @param strategyName - Strategy name to save heatmap report for
7409
- * @param path - Optional directory path to save report (default: "./logs/heatmap")
7770
+ * @param path - Optional directory path to save report (default: "./dump/heatmap")
7410
7771
  *
7411
7772
  * @example
7412
7773
  * ```typescript
7413
7774
  * const service = new HeatMarkdownService();
7414
7775
  *
7415
- * // Save to default path: ./logs/heatmap/my-strategy.md
7776
+ * // Save to default path: ./dump/heatmap/my-strategy.md
7416
7777
  * await service.dump("my-strategy");
7417
7778
  *
7418
7779
  * // Save to custom path: ./reports/my-strategy.md
7419
7780
  * await service.dump("my-strategy", "./reports");
7420
7781
  * ```
7421
7782
  */
7422
- this.dump = async (strategyName, path = "./logs/heatmap") => {
7783
+ this.dump = async (strategyName, path = "./dump/heatmap") => {
7423
7784
  this.loggerService.log(HEATMAP_METHOD_NAME_DUMP, {
7424
7785
  strategyName,
7425
7786
  path,
@@ -7851,34 +8212,2297 @@ class RiskValidationService {
7851
8212
  }
7852
8213
  }
7853
8214
 
7854
- {
7855
- provide(TYPES.loggerService, () => new LoggerService());
7856
- }
7857
- {
7858
- provide(TYPES.executionContextService, () => new ExecutionContextService());
7859
- provide(TYPES.methodContextService, () => new MethodContextService());
7860
- }
7861
- {
7862
- provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
7863
- provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
7864
- provide(TYPES.frameConnectionService, () => new FrameConnectionService());
7865
- provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
7866
- provide(TYPES.riskConnectionService, () => new RiskConnectionService());
7867
- }
7868
- {
7869
- provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
7870
- provide(TYPES.strategySchemaService, () => new StrategySchemaService());
7871
- provide(TYPES.frameSchemaService, () => new FrameSchemaService());
7872
- provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
7873
- provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
7874
- provide(TYPES.riskSchemaService, () => new RiskSchemaService());
7875
- }
7876
- {
7877
- provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
8215
+ /**
8216
+ * Default template service for generating optimizer code snippets.
8217
+ * Implements all IOptimizerTemplate methods with Ollama LLM integration.
8218
+ *
8219
+ * Features:
8220
+ * - Multi-timeframe analysis (1m, 5m, 15m, 1h)
8221
+ * - JSON structured output for signals
8222
+ * - Debug logging to ./dump/strategy
8223
+ * - CCXT exchange integration
8224
+ * - Walker-based strategy comparison
8225
+ *
8226
+ * Can be partially overridden in optimizer schema configuration.
8227
+ */
8228
+ class OptimizerTemplateService {
8229
+ constructor() {
8230
+ this.loggerService = inject(TYPES.loggerService);
8231
+ /**
8232
+ * Generates the top banner with imports and constants.
8233
+ *
8234
+ * @param symbol - Trading pair symbol
8235
+ * @returns Shebang, imports, and WARN_KB constant
8236
+ */
8237
+ this.getTopBanner = async (symbol) => {
8238
+ this.loggerService.log("optimizerTemplateService getTopBanner", {
8239
+ symbol,
8240
+ });
8241
+ return [
8242
+ "#!/usr/bin/env node",
8243
+ "",
8244
+ `import { Ollama } from "ollama";`,
8245
+ `import ccxt from "ccxt";`,
8246
+ `import {`,
8247
+ ` addExchange,`,
8248
+ ` addStrategy,`,
8249
+ ` addFrame,`,
8250
+ ` addWalker,`,
8251
+ ` Walker,`,
8252
+ ` Backtest,`,
8253
+ ` getCandles,`,
8254
+ ` listenSignalBacktest,`,
8255
+ ` listenWalkerComplete,`,
8256
+ ` listenDoneBacktest,`,
8257
+ ` listenBacktestProgress,`,
8258
+ ` listenWalkerProgress,`,
8259
+ ` listenError,`,
8260
+ `} from "backtest-kit";`,
8261
+ `import { promises as fs } from "fs";`,
8262
+ `import { v4 as uuid } from "uuid";`,
8263
+ `import path from "path";`,
8264
+ ``,
8265
+ `const WARN_KB = 100;`
8266
+ ].join("\n");
8267
+ };
8268
+ /**
8269
+ * Generates default user message for LLM conversation.
8270
+ * Simple prompt to read and acknowledge data.
8271
+ *
8272
+ * @param symbol - Trading pair symbol
8273
+ * @param data - Fetched data array
8274
+ * @param name - Source name
8275
+ * @returns User message with JSON data
8276
+ */
8277
+ this.getUserMessage = async (symbol, data, name) => {
8278
+ this.loggerService.log("optimizerTemplateService getUserMessage", {
8279
+ symbol,
8280
+ data,
8281
+ name,
8282
+ });
8283
+ return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
8284
+ };
8285
+ /**
8286
+ * Generates default assistant message for LLM conversation.
8287
+ * Simple acknowledgment response.
8288
+ *
8289
+ * @param symbol - Trading pair symbol
8290
+ * @param data - Fetched data array
8291
+ * @param name - Source name
8292
+ * @returns Assistant acknowledgment message
8293
+ */
8294
+ this.getAssistantMessage = async (symbol, data, name) => {
8295
+ this.loggerService.log("optimizerTemplateService getAssistantMessage", {
8296
+ symbol,
8297
+ data,
8298
+ name,
8299
+ });
8300
+ return "ОК";
8301
+ };
8302
+ /**
8303
+ * Generates Walker configuration code.
8304
+ * Compares multiple strategies on test frame.
8305
+ *
8306
+ * @param walkerName - Unique walker identifier
8307
+ * @param exchangeName - Exchange to use for backtesting
8308
+ * @param frameName - Test frame name
8309
+ * @param strategies - Array of strategy names to compare
8310
+ * @returns Generated addWalker() call
8311
+ */
8312
+ this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
8313
+ this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
8314
+ walkerName,
8315
+ exchangeName,
8316
+ frameName,
8317
+ strategies,
8318
+ });
8319
+ // Escape special characters to prevent code injection
8320
+ const escapedWalkerName = String(walkerName)
8321
+ .replace(/\\/g, '\\\\')
8322
+ .replace(/"/g, '\\"');
8323
+ const escapedExchangeName = String(exchangeName)
8324
+ .replace(/\\/g, '\\\\')
8325
+ .replace(/"/g, '\\"');
8326
+ const escapedFrameName = String(frameName)
8327
+ .replace(/\\/g, '\\\\')
8328
+ .replace(/"/g, '\\"');
8329
+ const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
8330
+ return [
8331
+ `addWalker({`,
8332
+ ` walkerName: "${escapedWalkerName}",`,
8333
+ ` exchangeName: "${escapedExchangeName}",`,
8334
+ ` frameName: "${escapedFrameName}",`,
8335
+ ` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
8336
+ `});`
8337
+ ].join("\n");
8338
+ };
8339
+ /**
8340
+ * Generates Strategy configuration with LLM integration.
8341
+ * Includes multi-timeframe analysis and signal generation.
8342
+ *
8343
+ * @param strategyName - Unique strategy identifier
8344
+ * @param interval - Signal throttling interval (e.g., "5m")
8345
+ * @param prompt - Strategy logic from getPrompt()
8346
+ * @returns Generated addStrategy() call with getSignal() function
8347
+ */
8348
+ this.getStrategyTemplate = async (strategyName, interval, prompt) => {
8349
+ this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
8350
+ strategyName,
8351
+ interval,
8352
+ prompt,
8353
+ });
8354
+ // Escape special characters to prevent code injection
8355
+ const escapedStrategyName = String(strategyName)
8356
+ .replace(/\\/g, '\\\\')
8357
+ .replace(/"/g, '\\"');
8358
+ const escapedInterval = String(interval)
8359
+ .replace(/\\/g, '\\\\')
8360
+ .replace(/"/g, '\\"');
8361
+ const escapedPrompt = String(prompt)
8362
+ .replace(/\\/g, '\\\\')
8363
+ .replace(/`/g, '\\`')
8364
+ .replace(/\$/g, '\\$');
8365
+ return [
8366
+ `addStrategy({`,
8367
+ ` strategyName: "${escapedStrategyName}",`,
8368
+ ` interval: "${escapedInterval}",`,
8369
+ ` getSignal: async (symbol) => {`,
8370
+ ` const messages = [];`,
8371
+ ``,
8372
+ ` // Загружаем данные всех таймфреймов`,
8373
+ ` const microTermCandles = await getCandles(symbol, "1m", 30);`,
8374
+ ` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
8375
+ ` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
8376
+ ` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
8377
+ ``,
8378
+ ` function formatCandles(candles, timeframe) {`,
8379
+ ` return candles.map((c) =>`,
8380
+ ` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
8381
+ ` ).join("\\n");`,
8382
+ ` }`,
8383
+ ``,
8384
+ ` // Сообщение 1: Среднесрочный тренд`,
8385
+ ` messages.push(`,
8386
+ ` {`,
8387
+ ` role: "user",`,
8388
+ ` content: [`,
8389
+ ` \`\${symbol}\`,`,
8390
+ ` "Проанализируй свечи 1h:",`,
8391
+ ` "",`,
8392
+ ` formatCandles(mediumTermCandles, "1h")`,
8393
+ ` ].join("\\n"),`,
8394
+ ` },`,
8395
+ ` {`,
8396
+ ` role: "assistant",`,
8397
+ ` content: "Тренд 1h проанализирован",`,
8398
+ ` }`,
8399
+ ` );`,
8400
+ ``,
8401
+ ` // Сообщение 2: Краткосрочный тренд`,
8402
+ ` messages.push(`,
8403
+ ` {`,
8404
+ ` role: "user",`,
8405
+ ` content: [`,
8406
+ ` "Проанализируй свечи 15m:",`,
8407
+ ` "",`,
8408
+ ` formatCandles(shortTermCandles, "15m")`,
8409
+ ` ].join("\\n"),`,
8410
+ ` },`,
8411
+ ` {`,
8412
+ ` role: "assistant",`,
8413
+ ` content: "Тренд 15m проанализирован",`,
8414
+ ` }`,
8415
+ ` );`,
8416
+ ``,
8417
+ ` // Сообщение 3: Основной таймфрейм`,
8418
+ ` messages.push(`,
8419
+ ` {`,
8420
+ ` role: "user",`,
8421
+ ` content: [`,
8422
+ ` "Проанализируй свечи 5m:",`,
8423
+ ` "",`,
8424
+ ` formatCandles(mainTermCandles, "5m")`,
8425
+ ` ].join("\\n")`,
8426
+ ` },`,
8427
+ ` {`,
8428
+ ` role: "assistant",`,
8429
+ ` content: "Таймфрейм 5m проанализирован",`,
8430
+ ` }`,
8431
+ ` );`,
8432
+ ``,
8433
+ ` // Сообщение 4: Микро-структура`,
8434
+ ` messages.push(`,
8435
+ ` {`,
8436
+ ` role: "user",`,
8437
+ ` content: [`,
8438
+ ` "Проанализируй свечи 1m:",`,
8439
+ ` "",`,
8440
+ ` formatCandles(microTermCandles, "1m")`,
8441
+ ` ].join("\\n")`,
8442
+ ` },`,
8443
+ ` {`,
8444
+ ` role: "assistant",`,
8445
+ ` content: "Микроструктура 1m проанализирована",`,
8446
+ ` }`,
8447
+ ` );`,
8448
+ ``,
8449
+ ` // Сообщение 5: Запрос сигнала`,
8450
+ ` messages.push(`,
8451
+ ` {`,
8452
+ ` role: "user",`,
8453
+ ` content: [`,
8454
+ ` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
8455
+ ` "",`,
8456
+ ` \`${escapedPrompt}\`,`,
8457
+ ` "",`,
8458
+ ` "Если сигналы противоречивы или тренд слабый то position: wait"`,
8459
+ ` ].join("\\n"),`,
8460
+ ` }`,
8461
+ ` );`,
8462
+ ``,
8463
+ ` const resultId = uuid();`,
8464
+ ``,
8465
+ ` const result = await json(messages);`,
8466
+ ``,
8467
+ ` await dumpJson(resultId, messages, result);`,
8468
+ ``,
8469
+ ` return result;`,
8470
+ ` },`,
8471
+ `});`
8472
+ ].join("\n");
8473
+ };
8474
+ /**
8475
+ * Generates Exchange configuration code.
8476
+ * Uses CCXT Binance with standard formatters.
8477
+ *
8478
+ * @param symbol - Trading pair symbol (unused, for consistency)
8479
+ * @param exchangeName - Unique exchange identifier
8480
+ * @returns Generated addExchange() call with CCXT integration
8481
+ */
8482
+ this.getExchangeTemplate = async (symbol, exchangeName) => {
8483
+ this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
8484
+ exchangeName,
8485
+ symbol,
8486
+ });
8487
+ // Escape special characters to prevent code injection
8488
+ const escapedExchangeName = String(exchangeName)
8489
+ .replace(/\\/g, '\\\\')
8490
+ .replace(/"/g, '\\"');
8491
+ return [
8492
+ `addExchange({`,
8493
+ ` exchangeName: "${escapedExchangeName}",`,
8494
+ ` getCandles: async (symbol, interval, since, limit) => {`,
8495
+ ` const exchange = new ccxt.binance();`,
8496
+ ` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
8497
+ ` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
8498
+ ` timestamp, open, high, low, close, volume`,
8499
+ ` }));`,
8500
+ ` },`,
8501
+ ` formatPrice: async (symbol, price) => price.toFixed(2),`,
8502
+ ` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
8503
+ `});`
8504
+ ].join("\n");
8505
+ };
8506
+ /**
8507
+ * Generates Frame (timeframe) configuration code.
8508
+ *
8509
+ * @param symbol - Trading pair symbol (unused, for consistency)
8510
+ * @param frameName - Unique frame identifier
8511
+ * @param interval - Candle interval (e.g., "1m")
8512
+ * @param startDate - Frame start date
8513
+ * @param endDate - Frame end date
8514
+ * @returns Generated addFrame() call
8515
+ */
8516
+ this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
8517
+ this.loggerService.log("optimizerTemplateService getFrameTemplate", {
8518
+ symbol,
8519
+ frameName,
8520
+ interval,
8521
+ startDate,
8522
+ endDate,
8523
+ });
8524
+ // Escape special characters to prevent code injection
8525
+ const escapedFrameName = String(frameName)
8526
+ .replace(/\\/g, '\\\\')
8527
+ .replace(/"/g, '\\"');
8528
+ const escapedInterval = String(interval)
8529
+ .replace(/\\/g, '\\\\')
8530
+ .replace(/"/g, '\\"');
8531
+ return [
8532
+ `addFrame({`,
8533
+ ` frameName: "${escapedFrameName}",`,
8534
+ ` interval: "${escapedInterval}",`,
8535
+ ` startDate: new Date("${startDate.toISOString()}"),`,
8536
+ ` endDate: new Date("${endDate.toISOString()}"),`,
8537
+ `});`
8538
+ ].join("\n");
8539
+ };
8540
+ /**
8541
+ * Generates launcher code to run Walker with event listeners.
8542
+ * Includes progress tracking and completion handlers.
8543
+ *
8544
+ * @param symbol - Trading pair symbol
8545
+ * @param walkerName - Walker name to launch
8546
+ * @returns Generated Walker.background() call with listeners
8547
+ */
8548
+ this.getLauncherTemplate = async (symbol, walkerName) => {
8549
+ this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
8550
+ symbol,
8551
+ walkerName,
8552
+ });
8553
+ // Escape special characters to prevent code injection
8554
+ const escapedSymbol = String(symbol)
8555
+ .replace(/\\/g, '\\\\')
8556
+ .replace(/"/g, '\\"');
8557
+ const escapedWalkerName = String(walkerName)
8558
+ .replace(/\\/g, '\\\\')
8559
+ .replace(/"/g, '\\"');
8560
+ return [
8561
+ `Walker.background("${escapedSymbol}", {`,
8562
+ ` walkerName: "${escapedWalkerName}"`,
8563
+ `});`,
8564
+ ``,
8565
+ `listenSignalBacktest((event) => {`,
8566
+ ` console.log(event);`,
8567
+ `});`,
8568
+ ``,
8569
+ `listenBacktestProgress((event) => {`,
8570
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
8571
+ ` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
8572
+ `});`,
8573
+ ``,
8574
+ `listenWalkerProgress((event) => {`,
8575
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
8576
+ ` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
8577
+ ` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
8578
+ `});`,
8579
+ ``,
8580
+ `listenWalkerComplete((results) => {`,
8581
+ ` console.log("Walker completed:", results.bestStrategy);`,
8582
+ ` Walker.dump("${escapedSymbol}", results.walkerName);`,
8583
+ `});`,
8584
+ ``,
8585
+ `listenDoneBacktest((event) => {`,
8586
+ ` console.log("Backtest completed:", event.symbol);`,
8587
+ ` Backtest.dump(event.strategyName);`,
8588
+ `});`,
8589
+ ``,
8590
+ `listenError((error) => {`,
8591
+ ` console.error("Error occurred:", error);`,
8592
+ `});`
8593
+ ].join("\n");
8594
+ };
8595
+ /**
8596
+ * Generates dumpJson() helper function for debug output.
8597
+ * Saves LLM conversations and results to ./dump/strategy/{resultId}/
8598
+ *
8599
+ * @param symbol - Trading pair symbol (unused, for consistency)
8600
+ * @returns Generated async dumpJson() function
8601
+ */
8602
+ this.getJsonDumpTemplate = async (symbol) => {
8603
+ this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
8604
+ symbol,
8605
+ });
8606
+ return [
8607
+ `async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
8608
+ ` // Extract system messages and system reminders from existing data`,
8609
+ ` const systemMessages = history.filter((m) => m.role === "system");`,
8610
+ ` const userMessages = history.filter((m) => m.role === "user");`,
8611
+ ` const subfolderPath = path.join(outputDir, resultId);`,
8612
+ ``,
8613
+ ` try {`,
8614
+ ` await fs.access(subfolderPath);`,
8615
+ ` return;`,
8616
+ ` } catch {`,
8617
+ ` await fs.mkdir(subfolderPath, { recursive: true });`,
8618
+ ` }`,
8619
+ ``,
8620
+ ` {`,
8621
+ ` let summary = "# Outline Result Summary\\n";`,
8622
+ ``,
8623
+ ` {`,
8624
+ ` summary += "\\n";`,
8625
+ ` summary += \`**ResultId**: \${resultId}\\n\`;`,
8626
+ ` summary += "\\n";`,
8627
+ ` }`,
8628
+ ``,
8629
+ ` if (result) {`,
8630
+ ` summary += "## Output Data\\n\\n";`,
8631
+ ` summary += "\`\`\`json\\n";`,
8632
+ ` summary += JSON.stringify(result, null, 2);`,
8633
+ ` summary += "\\n\`\`\`\\n\\n";`,
8634
+ ` }`,
8635
+ ``,
8636
+ ` // Add system messages to summary`,
8637
+ ` if (systemMessages.length > 0) {`,
8638
+ ` summary += "## System Messages\\n\\n";`,
8639
+ ` systemMessages.forEach((msg, idx) => {`,
8640
+ ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
8641
+ ` summary += msg.content;`,
8642
+ ` summary += "\\n";`,
8643
+ ` });`,
8644
+ ` }`,
8645
+ ``,
8646
+ ` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
8647
+ ` await fs.writeFile(summaryFile, summary, "utf8");`,
8648
+ ` }`,
8649
+ ``,
8650
+ ` {`,
8651
+ ` await Promise.all(`,
8652
+ ` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
8653
+ ` const messageNum = String(idx + 1).padStart(2, "0");`,
8654
+ ` const contentFileName = \`\${messageNum}_user_message.md\`;`,
8655
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
8656
+ ``,
8657
+ ` {`,
8658
+ ` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
8659
+ ` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
8660
+ ` if (messageSizeKb > WARN_KB) {`,
8661
+ ` console.warn(`,
8662
+ ` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
8663
+ ` );`,
8664
+ ` }`,
8665
+ ` }`,
8666
+ ``,
8667
+ ` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
8668
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
8669
+ ` content += message.content;`,
8670
+ ` content += "\\n";`,
8671
+ ``,
8672
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
8673
+ ` })`,
8674
+ ` );`,
8675
+ ` }`,
8676
+ ``,
8677
+ ` {`,
8678
+ ` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
8679
+ ` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
8680
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
8681
+ ``,
8682
+ ` let content = "# Full Outline Result\\n\\n";`,
8683
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
8684
+ ``,
8685
+ ` if (result) {`,
8686
+ ` content += "## Output Data\\n\\n";`,
8687
+ ` content += "\`\`\`json\\n";`,
8688
+ ` content += JSON.stringify(result, null, 2);`,
8689
+ ` content += "\\n\`\`\`\\n";`,
8690
+ ` }`,
8691
+ ``,
8692
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
8693
+ ` }`,
8694
+ `}`
8695
+ ].join("\n");
8696
+ };
8697
+ /**
8698
+ * Generates text() helper for LLM text generation.
8699
+ * Uses Ollama gpt-oss:20b model for market analysis.
8700
+ *
8701
+ * @param symbol - Trading pair symbol (used in prompt)
8702
+ * @returns Generated async text() function
8703
+ */
8704
+ this.getTextTemplate = async (symbol) => {
8705
+ this.loggerService.log("optimizerTemplateService getTextTemplate", {
8706
+ symbol,
8707
+ });
8708
+ // Escape special characters in symbol to prevent code injection
8709
+ const escapedSymbol = String(symbol)
8710
+ .replace(/\\/g, '\\\\')
8711
+ .replace(/`/g, '\\`')
8712
+ .replace(/\$/g, '\\$')
8713
+ .toUpperCase();
8714
+ return [
8715
+ `async function text(messages) {`,
8716
+ ` const ollama = new Ollama({`,
8717
+ ` host: "https://ollama.com",`,
8718
+ ` headers: {`,
8719
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
8720
+ ` },`,
8721
+ ` });`,
8722
+ ``,
8723
+ ` const response = await ollama.chat({`,
8724
+ ` model: "gpt-oss:20b",`,
8725
+ ` messages: [`,
8726
+ ` {`,
8727
+ ` role: "system",`,
8728
+ ` content: [`,
8729
+ ` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
8730
+ ` "только отчёт готовый для копипасты целиком",`,
8731
+ ` "",`,
8732
+ ` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
8733
+ ` ].join("\\n"),`,
8734
+ ` },`,
8735
+ ` ...messages,`,
8736
+ ` {`,
8737
+ ` role: "user",`,
8738
+ ` content: [`,
8739
+ ` "На каких условиях мне купить ${escapedSymbol}?",`,
8740
+ ` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
8741
+ ` "Какой RR ставить для позиций?",`,
8742
+ ` "Предпочтительны LONG или SHORT позиции?",`,
8743
+ ` "",`,
8744
+ ` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
8745
+ ` ].join("\\n")`,
8746
+ ` }`,
8747
+ ` ]`,
8748
+ ` });`,
8749
+ ``,
8750
+ ` const content = response.message.content.trim();`,
8751
+ ` return content`,
8752
+ ` .replace(/\\\\/g, '\\\\\\\\')`,
8753
+ ` .replace(/\`/g, '\\\\\`')`,
8754
+ ` .replace(/\\$/g, '\\\\$')`,
8755
+ ` .replace(/"/g, '\\\\"')`,
8756
+ ` .replace(/'/g, "\\\\'");`,
8757
+ `}`
8758
+ ].join("\n");
8759
+ };
8760
+ /**
8761
+ * Generates json() helper for structured LLM output.
8762
+ * Uses Ollama with JSON schema for trading signals.
8763
+ *
8764
+ * Signal schema:
8765
+ * - position: "wait" | "long" | "short"
8766
+ * - note: strategy explanation
8767
+ * - priceOpen: entry price
8768
+ * - priceTakeProfit: target price
8769
+ * - priceStopLoss: stop price
8770
+ * - minuteEstimatedTime: expected duration (max 360 min)
8771
+ *
8772
+ * @param symbol - Trading pair symbol (unused, for consistency)
8773
+ * @returns Generated async json() function with signal schema
8774
+ */
8775
+ this.getJsonTemplate = async (symbol) => {
8776
+ this.loggerService.log("optimizerTemplateService getJsonTemplate", {
8777
+ symbol,
8778
+ });
8779
+ return [
8780
+ `async function json(messages) {`,
8781
+ ` const ollama = new Ollama({`,
8782
+ ` host: "https://ollama.com",`,
8783
+ ` headers: {`,
8784
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
8785
+ ` },`,
8786
+ ` });`,
8787
+ ``,
8788
+ ` const response = await ollama.chat({`,
8789
+ ` model: "gpt-oss:20b",`,
8790
+ ` messages: [`,
8791
+ ` {`,
8792
+ ` role: "system",`,
8793
+ ` content: [`,
8794
+ ` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
8795
+ ` "",`,
8796
+ ` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
8797
+ ` "",`,
8798
+ ` "1. ТИПЫ ПОЗИЦИЙ:",`,
8799
+ ` " - position='wait': нет четкого сигнала, жди лучших условий",`,
8800
+ ` " - position='long': бычий сигнал, цена будет расти",`,
8801
+ ` " - position='short': медвежий сигнал, цена будет падать",`,
8802
+ ` "",`,
8803
+ ` "2. ЦЕНА ВХОДА (priceOpen):",`,
8804
+ ` " - Может быть текущей рыночной ценой для немедленного входа",`,
8805
+ ` " - Может быть отложенной ценой для входа при достижении уровня",`,
8806
+ ` " - Укажи оптимальную цену входа согласно технического анализа",`,
8807
+ ` "",`,
8808
+ ` "3. УРОВНИ ВЫХОДА:",`,
8809
+ ` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
8810
+ ` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
8811
+ ` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
8812
+ ` "",`,
8813
+ ` "4. ВРЕМЕННЫЕ РАМКИ:",`,
8814
+ ` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
8815
+ ` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
8816
+ ` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
8817
+ ` ].join("\\n"),`,
8818
+ ` },`,
8819
+ ` ...messages,`,
8820
+ ` ],`,
8821
+ ` format: {`,
8822
+ ` type: "object",`,
8823
+ ` properties: {`,
8824
+ ` position: {`,
8825
+ ` type: "string",`,
8826
+ ` enum: ["wait", "long", "short"],`,
8827
+ ` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
8828
+ ` },`,
8829
+ ` note: {`,
8830
+ ` type: "string",`,
8831
+ ` description: "Professional trading recommendation with price levels",`,
8832
+ ` },`,
8833
+ ` priceOpen: {`,
8834
+ ` type: "number",`,
8835
+ ` description: "Entry price (current market price or limit order price)",`,
8836
+ ` },`,
8837
+ ` priceTakeProfit: {`,
8838
+ ` type: "number",`,
8839
+ ` description: "Take profit target price",`,
8840
+ ` },`,
8841
+ ` priceStopLoss: {`,
8842
+ ` type: "number",`,
8843
+ ` description: "Stop loss exit price",`,
8844
+ ` },`,
8845
+ ` minuteEstimatedTime: {`,
8846
+ ` type: "number",`,
8847
+ ` description: "Expected time to reach TP in minutes (max 360)",`,
8848
+ ` },`,
8849
+ ` },`,
8850
+ ` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
8851
+ ` },`,
8852
+ ` });`,
8853
+ ``,
8854
+ ` const jsonResponse = JSON.parse(response.message.content.trim());`,
8855
+ ` return jsonResponse;`,
8856
+ `}`
8857
+ ].join("\n");
8858
+ };
8859
+ }
8860
+ }
8861
+
8862
+ /**
8863
+ * Service for managing optimizer schema registration and retrieval.
8864
+ * Provides validation and registry management for optimizer configurations.
8865
+ *
8866
+ * Uses ToolRegistry for immutable schema storage.
8867
+ */
8868
+ class OptimizerSchemaService {
8869
+ constructor() {
8870
+ this.loggerService = inject(TYPES.loggerService);
8871
+ this._registry = new functoolsKit.ToolRegistry("optimizerSchema");
8872
+ /**
8873
+ * Registers a new optimizer schema.
8874
+ * Validates required fields before registration.
8875
+ *
8876
+ * @param key - Unique optimizer name
8877
+ * @param value - Optimizer schema configuration
8878
+ * @throws Error if schema validation fails
8879
+ */
8880
+ this.register = (key, value) => {
8881
+ this.loggerService.log(`optimizerSchemaService register`, { key });
8882
+ this.validateShallow(value);
8883
+ this._registry = this._registry.register(key, value);
8884
+ };
8885
+ /**
8886
+ * Validates optimizer schema structure.
8887
+ * Checks required fields: optimizerName, rangeTrain, source, getPrompt.
8888
+ *
8889
+ * @param optimizerSchema - Schema to validate
8890
+ * @throws Error if validation fails
8891
+ */
8892
+ this.validateShallow = (optimizerSchema) => {
8893
+ this.loggerService.log(`optimizerTemplateService validateShallow`, {
8894
+ optimizerSchema,
8895
+ });
8896
+ if (typeof optimizerSchema.optimizerName !== "string") {
8897
+ throw new Error(`optimizer template validation failed: missing optimizerName`);
8898
+ }
8899
+ if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
8900
+ throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
8901
+ }
8902
+ if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
8903
+ throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
8904
+ }
8905
+ if (typeof optimizerSchema.getPrompt !== "function") {
8906
+ throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
8907
+ }
8908
+ };
8909
+ /**
8910
+ * Partially overrides an existing optimizer schema.
8911
+ * Merges provided values with existing schema.
8912
+ *
8913
+ * @param key - Optimizer name to override
8914
+ * @param value - Partial schema values to merge
8915
+ * @returns Updated complete schema
8916
+ * @throws Error if optimizer not found
8917
+ */
8918
+ this.override = (key, value) => {
8919
+ this.loggerService.log(`optimizerSchemaService override`, { key });
8920
+ this._registry = this._registry.override(key, value);
8921
+ return this._registry.get(key);
8922
+ };
8923
+ /**
8924
+ * Retrieves optimizer schema by name.
8925
+ *
8926
+ * @param key - Optimizer name
8927
+ * @returns Complete optimizer schema
8928
+ * @throws Error if optimizer not found
8929
+ */
8930
+ this.get = (key) => {
8931
+ this.loggerService.log(`optimizerSchemaService get`, { key });
8932
+ return this._registry.get(key);
8933
+ };
8934
+ }
8935
+ }
8936
+
8937
+ /**
8938
+ * Service for validating optimizer existence and managing optimizer registry.
8939
+ * Maintains a Map of registered optimizers for validation purposes.
8940
+ *
8941
+ * Uses memoization for efficient repeated validation checks.
8942
+ */
8943
+ class OptimizerValidationService {
8944
+ constructor() {
8945
+ this.loggerService = inject(TYPES.loggerService);
8946
+ this._optimizerMap = new Map();
8947
+ /**
8948
+ * Adds optimizer to validation registry.
8949
+ * Prevents duplicate optimizer names.
8950
+ *
8951
+ * @param optimizerName - Unique optimizer identifier
8952
+ * @param optimizerSchema - Complete optimizer schema
8953
+ * @throws Error if optimizer with same name already exists
8954
+ */
8955
+ this.addOptimizer = (optimizerName, optimizerSchema) => {
8956
+ this.loggerService.log("optimizerValidationService addOptimizer", {
8957
+ optimizerName,
8958
+ optimizerSchema,
8959
+ });
8960
+ if (this._optimizerMap.has(optimizerName)) {
8961
+ throw new Error(`optimizer ${optimizerName} already exist`);
8962
+ }
8963
+ this._optimizerMap.set(optimizerName, optimizerSchema);
8964
+ };
8965
+ /**
8966
+ * Validates that optimizer exists in registry.
8967
+ * Memoized for performance on repeated checks.
8968
+ *
8969
+ * @param optimizerName - Optimizer name to validate
8970
+ * @param source - Source method name for error messages
8971
+ * @throws Error if optimizer not found
8972
+ */
8973
+ this.validate = functoolsKit.memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
8974
+ this.loggerService.log("optimizerValidationService validate", {
8975
+ optimizerName,
8976
+ source,
8977
+ });
8978
+ const optimizer = this._optimizerMap.get(optimizerName);
8979
+ if (!optimizer) {
8980
+ throw new Error(`optimizer ${optimizerName} not found source=${source}`);
8981
+ }
8982
+ return true;
8983
+ });
8984
+ /**
8985
+ * Lists all registered optimizer schemas.
8986
+ *
8987
+ * @returns Array of all optimizer schemas
8988
+ */
8989
+ this.list = async () => {
8990
+ this.loggerService.log("optimizerValidationService list");
8991
+ return Array.from(this._optimizerMap.values());
8992
+ };
8993
+ }
8994
+ }
8995
+
8996
+ const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
8997
+ const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
8998
+ const METHOD_NAME_DUMP = "optimizerGlobalService dump";
8999
+ /**
9000
+ * Global service for optimizer operations with validation.
9001
+ * Entry point for public API, performs validation before delegating to ConnectionService.
9002
+ *
9003
+ * Workflow:
9004
+ * 1. Log operation
9005
+ * 2. Validate optimizer exists
9006
+ * 3. Delegate to OptimizerConnectionService
9007
+ */
9008
+ class OptimizerGlobalService {
9009
+ constructor() {
9010
+ this.loggerService = inject(TYPES.loggerService);
9011
+ this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
9012
+ this.optimizerValidationService = inject(TYPES.optimizerValidationService);
9013
+ /**
9014
+ * Fetches data from all sources and generates strategy metadata.
9015
+ * Validates optimizer existence before execution.
9016
+ *
9017
+ * @param symbol - Trading pair symbol
9018
+ * @param optimizerName - Optimizer identifier
9019
+ * @returns Array of generated strategies with conversation context
9020
+ * @throws Error if optimizer not found
9021
+ */
9022
+ this.getData = async (symbol, optimizerName) => {
9023
+ this.loggerService.log(METHOD_NAME_GET_DATA, {
9024
+ symbol,
9025
+ optimizerName,
9026
+ });
9027
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
9028
+ return await this.optimizerConnectionService.getData(symbol, optimizerName);
9029
+ };
9030
+ /**
9031
+ * Generates complete executable strategy code.
9032
+ * Validates optimizer existence before execution.
9033
+ *
9034
+ * @param symbol - Trading pair symbol
9035
+ * @param optimizerName - Optimizer identifier
9036
+ * @returns Generated TypeScript/JavaScript code as string
9037
+ * @throws Error if optimizer not found
9038
+ */
9039
+ this.getCode = async (symbol, optimizerName) => {
9040
+ this.loggerService.log(METHOD_NAME_GET_CODE, {
9041
+ symbol,
9042
+ optimizerName,
9043
+ });
9044
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
9045
+ return await this.optimizerConnectionService.getCode(symbol, optimizerName);
9046
+ };
9047
+ /**
9048
+ * Generates and saves strategy code to file.
9049
+ * Validates optimizer existence before execution.
9050
+ *
9051
+ * @param symbol - Trading pair symbol
9052
+ * @param optimizerName - Optimizer identifier
9053
+ * @param path - Output directory path (optional)
9054
+ * @throws Error if optimizer not found
9055
+ */
9056
+ this.dump = async (symbol, optimizerName, path) => {
9057
+ this.loggerService.log(METHOD_NAME_DUMP, {
9058
+ symbol,
9059
+ optimizerName,
9060
+ path,
9061
+ });
9062
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
9063
+ return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
9064
+ };
9065
+ }
9066
+ }
9067
+
9068
+ const ITERATION_LIMIT = 25;
9069
+ const DEFAULT_SOURCE_NAME = "unknown";
9070
+ const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
9071
+ /**
9072
+ * Default user message formatter.
9073
+ * Delegates to template's getUserMessage method.
9074
+ *
9075
+ * @param symbol - Trading pair symbol
9076
+ * @param data - Fetched data array
9077
+ * @param name - Source name
9078
+ * @param self - ClientOptimizer instance
9079
+ * @returns Formatted user message content
9080
+ */
9081
+ const DEFAULT_USER_FN = async (symbol, data, name, self) => {
9082
+ return await self.params.template.getUserMessage(symbol, data, name);
9083
+ };
9084
+ /**
9085
+ * Default assistant message formatter.
9086
+ * Delegates to template's getAssistantMessage method.
9087
+ *
9088
+ * @param symbol - Trading pair symbol
9089
+ * @param data - Fetched data array
9090
+ * @param name - Source name
9091
+ * @param self - ClientOptimizer instance
9092
+ * @returns Formatted assistant message content
9093
+ */
9094
+ const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
9095
+ return await self.params.template.getAssistantMessage(symbol, data, name);
9096
+ };
9097
+ /**
9098
+ * Resolves paginated data from source with deduplication.
9099
+ * Uses iterateDocuments to handle pagination automatically.
9100
+ *
9101
+ * @param fetch - Source fetch function
9102
+ * @param filterData - Filter arguments (symbol, dates)
9103
+ * @returns Deduplicated array of all fetched data
9104
+ */
9105
+ const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
9106
+ const iterator = functoolsKit.iterateDocuments({
9107
+ limit: ITERATION_LIMIT,
9108
+ async createRequest({ limit, offset }) {
9109
+ return await fetch({
9110
+ symbol: filterData.symbol,
9111
+ startDate: filterData.startDate,
9112
+ endDate: filterData.endDate,
9113
+ limit,
9114
+ offset,
9115
+ });
9116
+ },
9117
+ });
9118
+ const distinct = functoolsKit.distinctDocuments(iterator, (data) => data.id);
9119
+ return await functoolsKit.resolveDocuments(distinct);
9120
+ };
9121
+ /**
9122
+ * Collects data from all sources and generates strategy metadata.
9123
+ * Iterates through training ranges, fetches data from each source,
9124
+ * builds LLM conversation history, and generates strategy prompts.
9125
+ *
9126
+ * @param symbol - Trading pair symbol
9127
+ * @param self - ClientOptimizer instance
9128
+ * @returns Array of generated strategies with conversation context
9129
+ */
9130
+ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
9131
+ const strategyList = [];
9132
+ for (const { startDate, endDate } of self.params.rangeTrain) {
9133
+ const messageList = [];
9134
+ for (const source of self.params.source) {
9135
+ if (typeof source === "function") {
9136
+ const data = await RESOLVE_PAGINATION_FN(source, {
9137
+ symbol,
9138
+ startDate,
9139
+ endDate,
9140
+ });
9141
+ if (self.params.callbacks?.onSourceData) {
9142
+ await self.params.callbacks.onSourceData(symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
9143
+ }
9144
+ const [userContent, assistantContent] = await Promise.all([
9145
+ DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
9146
+ DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
9147
+ ]);
9148
+ messageList.push({
9149
+ role: "user",
9150
+ content: userContent,
9151
+ }, {
9152
+ role: "assistant",
9153
+ content: assistantContent,
9154
+ });
9155
+ }
9156
+ else {
9157
+ const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
9158
+ const data = await RESOLVE_PAGINATION_FN(fetch, {
9159
+ symbol,
9160
+ startDate,
9161
+ endDate,
9162
+ });
9163
+ if (self.params.callbacks?.onSourceData) {
9164
+ await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
9165
+ }
9166
+ const [userContent, assistantContent] = await Promise.all([
9167
+ user(symbol, data, name, self),
9168
+ assistant(symbol, data, name, self),
9169
+ ]);
9170
+ messageList.push({
9171
+ role: "user",
9172
+ content: userContent,
9173
+ }, {
9174
+ role: "assistant",
9175
+ content: assistantContent,
9176
+ });
9177
+ }
9178
+ const name = "name" in source
9179
+ ? source.name || DEFAULT_SOURCE_NAME
9180
+ : DEFAULT_SOURCE_NAME;
9181
+ strategyList.push({
9182
+ symbol,
9183
+ name,
9184
+ messages: messageList,
9185
+ strategy: await self.params.getPrompt(symbol, messageList),
9186
+ });
9187
+ }
9188
+ }
9189
+ if (self.params.callbacks?.onData) {
9190
+ await self.params.callbacks.onData(symbol, strategyList);
9191
+ }
9192
+ return strategyList;
9193
+ };
9194
+ /**
9195
+ * Generates complete executable strategy code.
9196
+ * Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
9197
+ *
9198
+ * @param symbol - Trading pair symbol
9199
+ * @param self - ClientOptimizer instance
9200
+ * @returns Generated TypeScript/JavaScript code as string
9201
+ */
9202
+ const GET_STRATEGY_CODE_FN = async (symbol, self) => {
9203
+ const strategyData = await self.getData(symbol);
9204
+ const prefix = CREATE_PREFIX_FN();
9205
+ const sections = [];
9206
+ const exchangeName = `${prefix}_exchange`;
9207
+ // 1. Top banner with imports
9208
+ {
9209
+ sections.push(await self.params.template.getTopBanner(symbol));
9210
+ sections.push("");
9211
+ }
9212
+ // 2. JSON dump helper function
9213
+ {
9214
+ sections.push(await self.params.template.getJsonDumpTemplate(symbol));
9215
+ sections.push("");
9216
+ }
9217
+ // 3. Helper functions (text and json)
9218
+ {
9219
+ sections.push(await self.params.template.getTextTemplate(symbol));
9220
+ sections.push("");
9221
+ }
9222
+ {
9223
+ sections.push(await self.params.template.getJsonTemplate(symbol));
9224
+ sections.push("");
9225
+ }
9226
+ // 4. Exchange template (assuming first strategy has exchange info)
9227
+ {
9228
+ sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
9229
+ sections.push("");
9230
+ }
9231
+ // 5. Train frame templates
9232
+ {
9233
+ for (let i = 0; i < self.params.rangeTrain.length; i++) {
9234
+ const range = self.params.rangeTrain[i];
9235
+ const frameName = `${prefix}_train_frame-${i + 1}`;
9236
+ sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
9237
+ range.startDate, range.endDate));
9238
+ sections.push("");
9239
+ }
9240
+ }
9241
+ // 6. Test frame template
9242
+ {
9243
+ const testFrameName = `${prefix}_test_frame`;
9244
+ sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
9245
+ self.params.rangeTest.startDate, self.params.rangeTest.endDate));
9246
+ sections.push("");
9247
+ }
9248
+ // 7. Strategy templates for each generated strategy
9249
+ {
9250
+ for (let i = 0; i < strategyData.length; i++) {
9251
+ const strategy = strategyData[i];
9252
+ const strategyName = `${prefix}_strategy-${i + 1}`;
9253
+ const interval = "5m"; // default interval
9254
+ sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
9255
+ sections.push("");
9256
+ }
9257
+ }
9258
+ // 8. Walker template (uses test frame for validation)
9259
+ {
9260
+ const walkerName = `${prefix}_walker`;
9261
+ const testFrameName = `${prefix}_test_frame`;
9262
+ const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
9263
+ sections.push(await self.params.template.getWalkerTemplate(walkerName, `${prefix}_${exchangeName}`, testFrameName, strategies));
9264
+ sections.push("");
9265
+ }
9266
+ // 9. Launcher template
9267
+ {
9268
+ const walkerName = `${prefix}_walker`;
9269
+ sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
9270
+ sections.push("");
9271
+ }
9272
+ const code = sections.join("\n");
9273
+ if (self.params.callbacks?.onCode) {
9274
+ await self.params.callbacks.onCode(symbol, code);
9275
+ }
9276
+ return code;
9277
+ };
9278
+ /**
9279
+ * Saves generated strategy code to file.
9280
+ * Creates directory if needed, writes .mjs file with generated code.
9281
+ *
9282
+ * @param symbol - Trading pair symbol
9283
+ * @param path - Output directory path
9284
+ * @param self - ClientOptimizer instance
9285
+ */
9286
+ const GET_STRATEGY_DUMP_FN = async (symbol, path$1, self) => {
9287
+ const report = await self.getCode(symbol);
9288
+ try {
9289
+ const dir = path.join(process.cwd(), path$1);
9290
+ await fs.mkdir(dir, { recursive: true });
9291
+ const filename = `${self.params.optimizerName}_${symbol}.mjs`;
9292
+ const filepath = path.join(dir, filename);
9293
+ await fs.writeFile(filepath, report, "utf-8");
9294
+ self.params.logger.info(`Optimizer report saved: ${filepath}`);
9295
+ if (self.params.callbacks?.onDump) {
9296
+ await self.params.callbacks.onDump(symbol, filepath);
9297
+ }
9298
+ }
9299
+ catch (error) {
9300
+ self.params.logger.warn(`Failed to save optimizer report:`, error);
9301
+ throw error;
9302
+ }
9303
+ };
9304
+ /**
9305
+ * Client implementation for optimizer operations.
9306
+ *
9307
+ * Features:
9308
+ * - Data collection from multiple sources with pagination
9309
+ * - LLM conversation history building
9310
+ * - Strategy code generation with templates
9311
+ * - File export with callbacks
9312
+ *
9313
+ * Used by OptimizerConnectionService to create optimizer instances.
9314
+ */
9315
+ class ClientOptimizer {
9316
+ constructor(params) {
9317
+ this.params = params;
9318
+ /**
9319
+ * Fetches data from all sources and generates strategy metadata.
9320
+ * Processes each training range and builds LLM conversation history.
9321
+ *
9322
+ * @param symbol - Trading pair symbol
9323
+ * @returns Array of generated strategies with conversation context
9324
+ */
9325
+ this.getData = async (symbol) => {
9326
+ this.params.logger.debug("ClientOptimizer getData", {
9327
+ symbol,
9328
+ });
9329
+ return await GET_STRATEGY_DATA_FN(symbol, this);
9330
+ };
9331
+ /**
9332
+ * Generates complete executable strategy code.
9333
+ * Includes imports, helpers, strategies, walker, and launcher.
9334
+ *
9335
+ * @param symbol - Trading pair symbol
9336
+ * @returns Generated TypeScript/JavaScript code as string
9337
+ */
9338
+ this.getCode = async (symbol) => {
9339
+ this.params.logger.debug("ClientOptimizer getCode", {
9340
+ symbol,
9341
+ });
9342
+ return await GET_STRATEGY_CODE_FN(symbol, this);
9343
+ };
9344
+ /**
9345
+ * Generates and saves strategy code to file.
9346
+ * Creates directory if needed, writes .mjs file.
9347
+ *
9348
+ * @param symbol - Trading pair symbol
9349
+ * @param path - Output directory path (default: "./")
9350
+ */
9351
+ this.dump = async (symbol, path = "./") => {
9352
+ this.params.logger.debug("ClientOptimizer dump", {
9353
+ symbol,
9354
+ path,
9355
+ });
9356
+ return await GET_STRATEGY_DUMP_FN(symbol, path, this);
9357
+ };
9358
+ }
9359
+ }
9360
+
9361
+ /**
9362
+ * Service for creating and caching optimizer client instances.
9363
+ * Handles dependency injection and template merging.
9364
+ *
9365
+ * Features:
9366
+ * - Memoized optimizer instances (one per optimizerName)
9367
+ * - Template merging (custom + defaults)
9368
+ * - Logger injection
9369
+ * - Delegates to ClientOptimizer for actual operations
9370
+ */
9371
+ class OptimizerConnectionService {
9372
+ constructor() {
9373
+ this.loggerService = inject(TYPES.loggerService);
9374
+ this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
9375
+ this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
9376
+ /**
9377
+ * Creates or retrieves cached optimizer instance.
9378
+ * Memoized by optimizerName for performance.
9379
+ *
9380
+ * Merges custom templates from schema with defaults from OptimizerTemplateService.
9381
+ *
9382
+ * @param optimizerName - Unique optimizer identifier
9383
+ * @returns ClientOptimizer instance with resolved dependencies
9384
+ */
9385
+ this.getOptimizer = functoolsKit.memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
9386
+ const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks } = this.optimizerSchemaService.get(optimizerName);
9387
+ const { getAssistantMessage = this.optimizerTemplateService.getAssistantMessage, getExchangeTemplate = this.optimizerTemplateService.getExchangeTemplate, getFrameTemplate = this.optimizerTemplateService.getFrameTemplate, getJsonDumpTemplate = this.optimizerTemplateService.getJsonDumpTemplate, getJsonTemplate = this.optimizerTemplateService.getJsonTemplate, getLauncherTemplate = this.optimizerTemplateService.getLauncherTemplate, getStrategyTemplate = this.optimizerTemplateService.getStrategyTemplate, getTextTemplate = this.optimizerTemplateService.getTextTemplate, getWalkerTemplate = this.optimizerTemplateService.getWalkerTemplate, getTopBanner = this.optimizerTemplateService.getTopBanner, getUserMessage = this.optimizerTemplateService.getUserMessage, } = rawTemplate;
9388
+ const template = {
9389
+ getAssistantMessage,
9390
+ getExchangeTemplate,
9391
+ getFrameTemplate,
9392
+ getJsonDumpTemplate,
9393
+ getJsonTemplate,
9394
+ getLauncherTemplate,
9395
+ getStrategyTemplate,
9396
+ getTextTemplate,
9397
+ getWalkerTemplate,
9398
+ getTopBanner,
9399
+ getUserMessage,
9400
+ };
9401
+ return new ClientOptimizer({
9402
+ optimizerName,
9403
+ logger: this.loggerService,
9404
+ getPrompt,
9405
+ rangeTest,
9406
+ rangeTrain,
9407
+ source,
9408
+ template,
9409
+ callbacks,
9410
+ });
9411
+ });
9412
+ /**
9413
+ * Fetches data from all sources and generates strategy metadata.
9414
+ *
9415
+ * @param symbol - Trading pair symbol
9416
+ * @param optimizerName - Optimizer identifier
9417
+ * @returns Array of generated strategies with conversation context
9418
+ */
9419
+ this.getData = async (symbol, optimizerName) => {
9420
+ this.loggerService.log("optimizerConnectionService getData", {
9421
+ symbol,
9422
+ optimizerName,
9423
+ });
9424
+ const optimizer = this.getOptimizer(optimizerName);
9425
+ return await optimizer.getData(symbol);
9426
+ };
9427
+ /**
9428
+ * Generates complete executable strategy code.
9429
+ *
9430
+ * @param symbol - Trading pair symbol
9431
+ * @param optimizerName - Optimizer identifier
9432
+ * @returns Generated TypeScript/JavaScript code as string
9433
+ */
9434
+ this.getCode = async (symbol, optimizerName) => {
9435
+ this.loggerService.log("optimizerConnectionService getCode", {
9436
+ symbol,
9437
+ optimizerName,
9438
+ });
9439
+ const optimizer = this.getOptimizer(optimizerName);
9440
+ return await optimizer.getCode(symbol);
9441
+ };
9442
+ /**
9443
+ * Generates and saves strategy code to file.
9444
+ *
9445
+ * @param symbol - Trading pair symbol
9446
+ * @param optimizerName - Optimizer identifier
9447
+ * @param path - Output directory path (optional)
9448
+ */
9449
+ this.dump = async (symbol, optimizerName, path) => {
9450
+ this.loggerService.log("optimizerConnectionService getCode", {
9451
+ symbol,
9452
+ optimizerName,
9453
+ });
9454
+ const optimizer = this.getOptimizer(optimizerName);
9455
+ return await optimizer.dump(symbol, path);
9456
+ };
9457
+ }
9458
+ }
9459
+
9460
+ /**
9461
+ * Symbol marker indicating that partial state needs initialization.
9462
+ * Used as sentinel value for _states before waitForInit() is called.
9463
+ */
9464
+ const NEED_FETCH = Symbol("need_fetch");
9465
+ /**
9466
+ * Array of profit level milestones to track (10%, 20%, ..., 100%).
9467
+ * Each level is checked during profit() method to emit events for newly reached levels.
9468
+ */
9469
+ const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
9470
+ /**
9471
+ * Array of loss level milestones to track (-10%, -20%, ..., -100%).
9472
+ * Each level is checked during loss() method to emit events for newly reached levels.
9473
+ */
9474
+ const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
9475
+ /**
9476
+ * Internal profit handler function for ClientPartial.
9477
+ *
9478
+ * Checks which profit levels have been reached and emits events for new levels only.
9479
+ * Uses Set-based deduplication to prevent duplicate events.
9480
+ *
9481
+ * @param symbol - Trading pair symbol
9482
+ * @param data - Signal row data
9483
+ * @param currentPrice - Current market price
9484
+ * @param revenuePercent - Current profit percentage (positive value)
9485
+ * @param backtest - True if backtest mode
9486
+ * @param when - Event timestamp
9487
+ * @param self - ClientPartial instance reference
9488
+ */
9489
+ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
9490
+ if (self._states === NEED_FETCH) {
9491
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9492
+ }
9493
+ let state = self._states.get(data.id);
9494
+ if (!state) {
9495
+ state = {
9496
+ profitLevels: new Set(),
9497
+ lossLevels: new Set(),
9498
+ };
9499
+ self._states.set(data.id, state);
9500
+ }
9501
+ let shouldPersist = false;
9502
+ for (const level of PROFIT_LEVELS) {
9503
+ if (revenuePercent >= level && !state.profitLevels.has(level)) {
9504
+ state.profitLevels.add(level);
9505
+ shouldPersist = true;
9506
+ self.params.logger.debug("ClientPartial profit level reached", {
9507
+ symbol,
9508
+ signalId: data.id,
9509
+ level,
9510
+ revenuePercent,
9511
+ backtest,
9512
+ });
9513
+ await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
9514
+ }
9515
+ }
9516
+ if (shouldPersist) {
9517
+ await self._persistState(symbol);
9518
+ }
9519
+ };
9520
+ /**
9521
+ * Internal loss handler function for ClientPartial.
9522
+ *
9523
+ * Checks which loss levels have been reached and emits events for new levels only.
9524
+ * Uses Set-based deduplication to prevent duplicate events.
9525
+ * Converts negative lossPercent to absolute value for level comparison.
9526
+ *
9527
+ * @param symbol - Trading pair symbol
9528
+ * @param data - Signal row data
9529
+ * @param currentPrice - Current market price
9530
+ * @param lossPercent - Current loss percentage (negative value)
9531
+ * @param backtest - True if backtest mode
9532
+ * @param when - Event timestamp
9533
+ * @param self - ClientPartial instance reference
9534
+ */
9535
+ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
9536
+ if (self._states === NEED_FETCH) {
9537
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9538
+ }
9539
+ let state = self._states.get(data.id);
9540
+ if (!state) {
9541
+ state = {
9542
+ profitLevels: new Set(),
9543
+ lossLevels: new Set(),
9544
+ };
9545
+ self._states.set(data.id, state);
9546
+ }
9547
+ const absLoss = Math.abs(lossPercent);
9548
+ let shouldPersist = false;
9549
+ for (const level of LOSS_LEVELS) {
9550
+ if (absLoss >= level && !state.lossLevels.has(level)) {
9551
+ state.lossLevels.add(level);
9552
+ shouldPersist = true;
9553
+ self.params.logger.debug("ClientPartial loss level reached", {
9554
+ symbol,
9555
+ signalId: data.id,
9556
+ level,
9557
+ lossPercent,
9558
+ backtest,
9559
+ });
9560
+ await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
9561
+ }
9562
+ }
9563
+ if (shouldPersist) {
9564
+ await self._persistState(symbol);
9565
+ }
9566
+ };
9567
+ /**
9568
+ * Internal initialization function for ClientPartial.
9569
+ *
9570
+ * Loads persisted partial state from disk and restores in-memory Maps.
9571
+ * Converts serialized arrays back to Sets for O(1) lookups.
9572
+ *
9573
+ * @param symbol - Trading pair symbol
9574
+ * @param self - ClientPartial instance reference
9575
+ */
9576
+ const WAIT_FOR_INIT_FN = async (symbol, self) => {
9577
+ self.params.logger.debug("ClientPartial waitForInit", { symbol });
9578
+ if (self._states === NEED_FETCH) {
9579
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9580
+ }
9581
+ const partialData = await PersistPartialAdapter.readPartialData(symbol);
9582
+ for (const [signalId, data] of Object.entries(partialData)) {
9583
+ const state = {
9584
+ profitLevels: new Set(data.profitLevels),
9585
+ lossLevels: new Set(data.lossLevels),
9586
+ };
9587
+ self._states.set(signalId, state);
9588
+ }
9589
+ self.params.logger.info("ClientPartial restored state", {
9590
+ symbol,
9591
+ signalCount: Object.keys(partialData).length,
9592
+ });
9593
+ };
9594
+ /**
9595
+ * Client implementation for partial profit/loss level tracking.
9596
+ *
9597
+ * Features:
9598
+ * - Tracks profit and loss level milestones (10%, 20%, 30%, etc) per signal
9599
+ * - Deduplicates events using Set-based state per signal ID
9600
+ * - Persists state to disk for crash recovery in live mode
9601
+ * - Emits events via onProfit/onLoss callbacks for each newly reached level
9602
+ *
9603
+ * Architecture:
9604
+ * - Created per signal ID by PartialConnectionService (memoized)
9605
+ * - State stored in Map<signalId, IPartialState> with Set<PartialLevel>
9606
+ * - Persistence handled by PersistPartialAdapter (atomic file writes)
9607
+ *
9608
+ * Lifecycle:
9609
+ * 1. Construction: Initialize empty Map
9610
+ * 2. waitForInit(): Load persisted state from disk
9611
+ * 3. profit()/loss(): Check levels, emit events, persist changes
9612
+ * 4. clear(): Remove signal state, persist, clean up memoized instance
9613
+ *
9614
+ * @example
9615
+ * ```typescript
9616
+ * import { ClientPartial } from "./client/ClientPartial";
9617
+ *
9618
+ * const partial = new ClientPartial({
9619
+ * logger: loggerService,
9620
+ * onProfit: async (symbol, data, price, level, backtest, timestamp) => {
9621
+ * console.log(`Signal ${data.id} reached ${level}% profit at ${price}`);
9622
+ * // Emit to partialProfitSubject
9623
+ * },
9624
+ * onLoss: async (symbol, data, price, level, backtest, timestamp) => {
9625
+ * console.log(`Signal ${data.id} reached -${level}% loss at ${price}`);
9626
+ * // Emit to partialLossSubject
9627
+ * }
9628
+ * });
9629
+ *
9630
+ * // Initialize from persisted state
9631
+ * await partial.waitForInit("BTCUSDT");
9632
+ *
9633
+ * // During signal monitoring in ClientStrategy
9634
+ * const signal = { id: "abc123", priceOpen: 50000, position: "long", ... };
9635
+ *
9636
+ * // Price rises to $55000 (10% profit)
9637
+ * await partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
9638
+ * // Emits onProfit callback for 10% level
9639
+ *
9640
+ * // Price rises to $61000 (22% profit)
9641
+ * await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
9642
+ * // Emits onProfit for 20% level only (10% already emitted)
9643
+ *
9644
+ * // Signal closes
9645
+ * await partial.clear("BTCUSDT", signal, 61000);
9646
+ * // State removed, changes persisted
9647
+ * ```
9648
+ */
9649
+ class ClientPartial {
9650
+ /**
9651
+ * Creates new ClientPartial instance.
9652
+ *
9653
+ * @param params - Partial parameters (logger, onProfit, onLoss callbacks)
9654
+ */
9655
+ constructor(params) {
9656
+ this.params = params;
9657
+ /**
9658
+ * Map of signal IDs to partial profit/loss state.
9659
+ * Uses NEED_FETCH sentinel before initialization.
9660
+ *
9661
+ * Each state contains:
9662
+ * - profitLevels: Set of reached profit levels (10, 20, 30, etc)
9663
+ * - lossLevels: Set of reached loss levels (10, 20, 30, etc)
9664
+ */
9665
+ this._states = NEED_FETCH;
9666
+ /**
9667
+ * Initializes partial state by loading from disk.
9668
+ *
9669
+ * Uses singleshot pattern to ensure initialization happens exactly once per symbol.
9670
+ * Reads persisted state from PersistPartialAdapter and restores to _states Map.
9671
+ *
9672
+ * Must be called before profit()/loss()/clear() methods.
9673
+ *
9674
+ * @param symbol - Trading pair symbol
9675
+ * @returns Promise that resolves when initialization is complete
9676
+ *
9677
+ * @example
9678
+ * ```typescript
9679
+ * const partial = new ClientPartial(params);
9680
+ * await partial.waitForInit("BTCUSDT"); // Load persisted state
9681
+ * // Now profit()/loss() can be called
9682
+ * ```
9683
+ */
9684
+ this.waitForInit = functoolsKit.singleshot(async (symbol) => await WAIT_FOR_INIT_FN(symbol, this));
9685
+ this._states = new Map();
9686
+ }
9687
+ /**
9688
+ * Persists current partial state to disk.
9689
+ *
9690
+ * Converts in-memory Maps and Sets to JSON-serializable format:
9691
+ * - Map<signalId, IPartialState> → Record<signalId, IPartialData>
9692
+ * - Set<PartialLevel> → PartialLevel[]
9693
+ *
9694
+ * Called automatically after profit/loss level changes or clear().
9695
+ * Uses atomic file writes via PersistPartialAdapter.
9696
+ *
9697
+ * @param symbol - Trading pair symbol
9698
+ * @returns Promise that resolves when persistence is complete
9699
+ */
9700
+ async _persistState(symbol) {
9701
+ this.params.logger.debug("ClientPartial persistState", { symbol });
9702
+ if (this._states === NEED_FETCH) {
9703
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9704
+ }
9705
+ const partialData = {};
9706
+ for (const [signalId, state] of this._states.entries()) {
9707
+ partialData[signalId] = {
9708
+ profitLevels: Array.from(state.profitLevels),
9709
+ lossLevels: Array.from(state.lossLevels),
9710
+ };
9711
+ }
9712
+ await PersistPartialAdapter.writePartialData(partialData, symbol);
9713
+ }
9714
+ /**
9715
+ * Processes profit state and emits events for newly reached profit levels.
9716
+ *
9717
+ * Called by ClientStrategy during signal monitoring when revenuePercent > 0.
9718
+ * Iterates through PROFIT_LEVELS (10%, 20%, 30%, etc) and checks which
9719
+ * levels have been reached but not yet emitted (Set-based deduplication).
9720
+ *
9721
+ * For each new level:
9722
+ * 1. Adds level to state.profitLevels Set
9723
+ * 2. Logs debug message
9724
+ * 3. Calls params.onProfit callback (emits to partialProfitSubject)
9725
+ *
9726
+ * After all levels processed, persists state to disk if any new levels were found.
9727
+ *
9728
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9729
+ * @param data - Signal row data
9730
+ * @param currentPrice - Current market price
9731
+ * @param revenuePercent - Current profit percentage (positive value)
9732
+ * @param backtest - True if backtest mode, false if live mode
9733
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9734
+ * @returns Promise that resolves when profit processing is complete
9735
+ *
9736
+ * @example
9737
+ * ```typescript
9738
+ * // Signal at $50000, price rises to $61000 (22% profit)
9739
+ * await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
9740
+ * // Emits events for 10% and 20% levels (if not already emitted)
9741
+ * // State persisted to disk
9742
+ * ```
9743
+ */
9744
+ async profit(symbol, data, currentPrice, revenuePercent, backtest, when) {
9745
+ this.params.logger.debug("ClientPartial profit", {
9746
+ symbol,
9747
+ signalId: data.id,
9748
+ currentPrice,
9749
+ revenuePercent,
9750
+ backtest,
9751
+ when,
9752
+ });
9753
+ return await HANDLE_PROFIT_FN(symbol, data, currentPrice, revenuePercent, backtest, when, this);
9754
+ }
9755
+ /**
9756
+ * Processes loss state and emits events for newly reached loss levels.
9757
+ *
9758
+ * Called by ClientStrategy during signal monitoring when revenuePercent < 0.
9759
+ * Converts negative lossPercent to absolute value and iterates through
9760
+ * LOSS_LEVELS (10%, 20%, 30%, etc) to check which levels have been reached
9761
+ * but not yet emitted (Set-based deduplication).
9762
+ *
9763
+ * For each new level:
9764
+ * 1. Adds level to state.lossLevels Set
9765
+ * 2. Logs debug message
9766
+ * 3. Calls params.onLoss callback (emits to partialLossSubject)
9767
+ *
9768
+ * After all levels processed, persists state to disk if any new levels were found.
9769
+ *
9770
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9771
+ * @param data - Signal row data
9772
+ * @param currentPrice - Current market price
9773
+ * @param lossPercent - Current loss percentage (negative value)
9774
+ * @param backtest - True if backtest mode, false if live mode
9775
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9776
+ * @returns Promise that resolves when loss processing is complete
9777
+ *
9778
+ * @example
9779
+ * ```typescript
9780
+ * // Signal at $50000, price drops to $39000 (-22% loss)
9781
+ * await partial.loss("BTCUSDT", signal, 39000, -22.0, false, new Date());
9782
+ * // Emits events for 10% and 20% loss levels (if not already emitted)
9783
+ * // State persisted to disk
9784
+ * ```
9785
+ */
9786
+ async loss(symbol, data, currentPrice, lossPercent, backtest, when) {
9787
+ this.params.logger.debug("ClientPartial loss", {
9788
+ symbol,
9789
+ signalId: data.id,
9790
+ currentPrice,
9791
+ lossPercent,
9792
+ backtest,
9793
+ when,
9794
+ });
9795
+ return await HANDLE_LOSS_FN(symbol, data, currentPrice, lossPercent, backtest, when, this);
9796
+ }
9797
+ /**
9798
+ * Clears partial profit/loss state for a signal when it closes.
9799
+ *
9800
+ * Called by ClientStrategy when signal completes (TP/SL/time_expired).
9801
+ * Removes signal's state from _states Map and persists changes to disk.
9802
+ *
9803
+ * After clear() completes:
9804
+ * - Signal state removed from memory (_states.delete)
9805
+ * - Changes persisted to disk (PersistPartialAdapter.writePartialData)
9806
+ * - Memoized ClientPartial instance cleared in PartialConnectionService
9807
+ *
9808
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9809
+ * @param data - Signal row data
9810
+ * @param priceClose - Final closing price
9811
+ * @returns Promise that resolves when clear is complete
9812
+ * @throws Error if ClientPartial not initialized (waitForInit not called)
9813
+ *
9814
+ * @example
9815
+ * ```typescript
9816
+ * // Signal closes at take profit
9817
+ * await partial.clear("BTCUSDT", signal, 52000);
9818
+ * // State removed: _states.delete(signal.id)
9819
+ * // Persisted: ./dump/data/partial/BTCUSDT/levels.json updated
9820
+ * // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
9821
+ * ```
9822
+ */
9823
+ async clear(symbol, data, priceClose) {
9824
+ this.params.logger.log("ClientPartial clear", {
9825
+ symbol,
9826
+ data,
9827
+ priceClose,
9828
+ });
9829
+ if (this._states === NEED_FETCH) {
9830
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9831
+ }
9832
+ this._states.delete(data.id);
9833
+ await this._persistState(symbol);
9834
+ }
9835
+ }
9836
+
9837
+ /**
9838
+ * Callback function for emitting profit events to partialProfitSubject.
9839
+ *
9840
+ * Called by ClientPartial when a new profit level is reached.
9841
+ * Emits PartialProfitContract event to all subscribers.
9842
+ *
9843
+ * @param symbol - Trading pair symbol
9844
+ * @param data - Signal row data
9845
+ * @param currentPrice - Current market price
9846
+ * @param level - Profit level reached
9847
+ * @param backtest - True if backtest mode
9848
+ * @param timestamp - Event timestamp in milliseconds
9849
+ */
9850
+ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
9851
+ symbol,
9852
+ data,
9853
+ currentPrice,
9854
+ level,
9855
+ backtest,
9856
+ timestamp,
9857
+ });
9858
+ /**
9859
+ * Callback function for emitting loss events to partialLossSubject.
9860
+ *
9861
+ * Called by ClientPartial when a new loss level is reached.
9862
+ * Emits PartialLossContract event to all subscribers.
9863
+ *
9864
+ * @param symbol - Trading pair symbol
9865
+ * @param data - Signal row data
9866
+ * @param currentPrice - Current market price
9867
+ * @param level - Loss level reached
9868
+ * @param backtest - True if backtest mode
9869
+ * @param timestamp - Event timestamp in milliseconds
9870
+ */
9871
+ const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
9872
+ symbol,
9873
+ data,
9874
+ currentPrice,
9875
+ level,
9876
+ backtest,
9877
+ timestamp,
9878
+ });
9879
+ /**
9880
+ * Connection service for partial profit/loss tracking.
9881
+ *
9882
+ * Provides memoized ClientPartial instances per signal ID.
9883
+ * Acts as factory and lifetime manager for ClientPartial objects.
9884
+ *
9885
+ * Features:
9886
+ * - Creates one ClientPartial instance per signal ID (memoized)
9887
+ * - Configures instances with logger and event emitter callbacks
9888
+ * - Delegates profit/loss/clear operations to appropriate ClientPartial
9889
+ * - Cleans up memoized instances when signals are cleared
9890
+ *
9891
+ * Architecture:
9892
+ * - Injected into ClientStrategy via PartialGlobalService
9893
+ * - Uses memoize from functools-kit for instance caching
9894
+ * - Emits events to partialProfitSubject/partialLossSubject
9895
+ *
9896
+ * @example
9897
+ * ```typescript
9898
+ * // Service injected via DI
9899
+ * const service = inject<PartialConnectionService>(TYPES.partialConnectionService);
9900
+ *
9901
+ * // Called by ClientStrategy during signal monitoring
9902
+ * await service.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
9903
+ * // Creates or reuses ClientPartial for signal.id
9904
+ * // Delegates to ClientPartial.profit()
9905
+ *
9906
+ * // When signal closes
9907
+ * await service.clear("BTCUSDT", signal, 52000);
9908
+ * // Clears signal state and removes memoized instance
9909
+ * ```
9910
+ */
9911
+ class PartialConnectionService {
9912
+ constructor() {
9913
+ /**
9914
+ * Logger service injected from DI container.
9915
+ */
9916
+ this.loggerService = inject(TYPES.loggerService);
9917
+ /**
9918
+ * Memoized factory function for ClientPartial instances.
9919
+ *
9920
+ * Creates one ClientPartial per signal ID with configured callbacks.
9921
+ * Instances are cached until clear() is called.
9922
+ *
9923
+ * Key format: signalId
9924
+ * Value: ClientPartial instance with logger and event emitters
9925
+ */
9926
+ this.getPartial = functoolsKit.memoize(([signalId]) => `${signalId}`, () => {
9927
+ return new ClientPartial({
9928
+ logger: this.loggerService,
9929
+ onProfit: COMMIT_PROFIT_FN,
9930
+ onLoss: COMMIT_LOSS_FN,
9931
+ });
9932
+ });
9933
+ /**
9934
+ * Processes profit state and emits events for newly reached profit levels.
9935
+ *
9936
+ * Retrieves or creates ClientPartial for signal ID, initializes it if needed,
9937
+ * then delegates to ClientPartial.profit() method.
9938
+ *
9939
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9940
+ * @param data - Signal row data
9941
+ * @param currentPrice - Current market price
9942
+ * @param revenuePercent - Current profit percentage (positive value)
9943
+ * @param backtest - True if backtest mode, false if live mode
9944
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9945
+ * @returns Promise that resolves when profit processing is complete
9946
+ */
9947
+ this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
9948
+ this.loggerService.log("partialConnectionService profit", {
9949
+ symbol,
9950
+ data,
9951
+ currentPrice,
9952
+ revenuePercent,
9953
+ backtest,
9954
+ when,
9955
+ });
9956
+ const partial = this.getPartial(data.id);
9957
+ await partial.waitForInit(symbol);
9958
+ return await partial.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
9959
+ };
9960
+ /**
9961
+ * Processes loss state and emits events for newly reached loss levels.
9962
+ *
9963
+ * Retrieves or creates ClientPartial for signal ID, initializes it if needed,
9964
+ * then delegates to ClientPartial.loss() method.
9965
+ *
9966
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9967
+ * @param data - Signal row data
9968
+ * @param currentPrice - Current market price
9969
+ * @param lossPercent - Current loss percentage (negative value)
9970
+ * @param backtest - True if backtest mode, false if live mode
9971
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9972
+ * @returns Promise that resolves when loss processing is complete
9973
+ */
9974
+ this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
9975
+ this.loggerService.log("partialConnectionService loss", {
9976
+ symbol,
9977
+ data,
9978
+ currentPrice,
9979
+ lossPercent,
9980
+ backtest,
9981
+ when,
9982
+ });
9983
+ const partial = this.getPartial(data.id);
9984
+ await partial.waitForInit(symbol);
9985
+ return await partial.loss(symbol, data, currentPrice, lossPercent, backtest, when);
9986
+ };
9987
+ /**
9988
+ * Clears partial profit/loss state when signal closes.
9989
+ *
9990
+ * Retrieves ClientPartial for signal ID, initializes if needed,
9991
+ * delegates clear operation, then removes memoized instance.
9992
+ *
9993
+ * Sequence:
9994
+ * 1. Get ClientPartial from memoize cache
9995
+ * 2. Ensure initialization (waitForInit)
9996
+ * 3. Call ClientPartial.clear() - removes state, persists to disk
9997
+ * 4. Clear memoized instance - prevents memory leaks
9998
+ *
9999
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10000
+ * @param data - Signal row data
10001
+ * @param priceClose - Final closing price
10002
+ * @returns Promise that resolves when clear is complete
10003
+ */
10004
+ this.clear = async (symbol, data, priceClose) => {
10005
+ this.loggerService.log("partialConnectionService profit", {
10006
+ symbol,
10007
+ data,
10008
+ priceClose,
10009
+ });
10010
+ const partial = this.getPartial(data.id);
10011
+ await partial.waitForInit(symbol);
10012
+ await partial.clear(symbol, data, priceClose);
10013
+ this.getPartial.clear(data.id);
10014
+ };
10015
+ }
10016
+ }
10017
+
10018
+ const columns = [
10019
+ {
10020
+ key: "action",
10021
+ label: "Action",
10022
+ format: (data) => data.action.toUpperCase(),
10023
+ },
10024
+ {
10025
+ key: "symbol",
10026
+ label: "Symbol",
10027
+ format: (data) => data.symbol,
10028
+ },
10029
+ {
10030
+ key: "signalId",
10031
+ label: "Signal ID",
10032
+ format: (data) => data.signalId,
10033
+ },
10034
+ {
10035
+ key: "position",
10036
+ label: "Position",
10037
+ format: (data) => data.position.toUpperCase(),
10038
+ },
10039
+ {
10040
+ key: "level",
10041
+ label: "Level %",
10042
+ format: (data) => data.action === "profit" ? `+${data.level}%` : `-${data.level}%`,
10043
+ },
10044
+ {
10045
+ key: "currentPrice",
10046
+ label: "Current Price",
10047
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
10048
+ },
10049
+ {
10050
+ key: "timestamp",
10051
+ label: "Timestamp",
10052
+ format: (data) => new Date(data.timestamp).toISOString(),
10053
+ },
10054
+ {
10055
+ key: "mode",
10056
+ label: "Mode",
10057
+ format: (data) => (data.backtest ? "Backtest" : "Live"),
10058
+ },
10059
+ ];
10060
+ /** Maximum number of events to store in partial reports */
10061
+ const MAX_EVENTS = 250;
10062
+ /**
10063
+ * Storage class for accumulating partial profit/loss events per symbol.
10064
+ * Maintains a chronological list of profit and loss level events.
10065
+ */
10066
+ class ReportStorage {
10067
+ constructor() {
10068
+ /** Internal list of all partial events for this symbol */
10069
+ this._eventList = [];
10070
+ }
10071
+ /**
10072
+ * Adds a profit event to the storage.
10073
+ *
10074
+ * @param symbol - Trading pair symbol
10075
+ * @param data - Signal row data
10076
+ * @param currentPrice - Current market price
10077
+ * @param level - Profit level reached
10078
+ * @param backtest - True if backtest mode
10079
+ */
10080
+ addProfitEvent(symbol, data, currentPrice, level, backtest, timestamp) {
10081
+ this._eventList.push({
10082
+ timestamp,
10083
+ action: "profit",
10084
+ symbol,
10085
+ signalId: data.id,
10086
+ position: data.position,
10087
+ currentPrice,
10088
+ level,
10089
+ backtest,
10090
+ });
10091
+ // Trim queue if exceeded MAX_EVENTS
10092
+ if (this._eventList.length > MAX_EVENTS) {
10093
+ this._eventList.shift();
10094
+ }
10095
+ }
10096
+ /**
10097
+ * Adds a loss event to the storage.
10098
+ *
10099
+ * @param symbol - Trading pair symbol
10100
+ * @param data - Signal row data
10101
+ * @param currentPrice - Current market price
10102
+ * @param level - Loss level reached
10103
+ * @param backtest - True if backtest mode
10104
+ */
10105
+ addLossEvent(symbol, data, currentPrice, level, backtest, timestamp) {
10106
+ this._eventList.push({
10107
+ timestamp,
10108
+ action: "loss",
10109
+ symbol,
10110
+ signalId: data.id,
10111
+ position: data.position,
10112
+ currentPrice,
10113
+ level,
10114
+ backtest,
10115
+ });
10116
+ // Trim queue if exceeded MAX_EVENTS
10117
+ if (this._eventList.length > MAX_EVENTS) {
10118
+ this._eventList.shift();
10119
+ }
10120
+ }
10121
+ /**
10122
+ * Calculates statistical data from partial profit/loss events (Controller).
10123
+ *
10124
+ * @returns Statistical data (empty object if no events)
10125
+ */
10126
+ async getData() {
10127
+ if (this._eventList.length === 0) {
10128
+ return {
10129
+ eventList: [],
10130
+ totalEvents: 0,
10131
+ totalProfit: 0,
10132
+ totalLoss: 0,
10133
+ };
10134
+ }
10135
+ const profitEvents = this._eventList.filter((e) => e.action === "profit");
10136
+ const lossEvents = this._eventList.filter((e) => e.action === "loss");
10137
+ return {
10138
+ eventList: this._eventList,
10139
+ totalEvents: this._eventList.length,
10140
+ totalProfit: profitEvents.length,
10141
+ totalLoss: lossEvents.length,
10142
+ };
10143
+ }
10144
+ /**
10145
+ * Generates markdown report with all partial events for a symbol (View).
10146
+ *
10147
+ * @param symbol - Trading pair symbol
10148
+ * @returns Markdown formatted report with all events
10149
+ */
10150
+ async getReport(symbol) {
10151
+ const stats = await this.getData();
10152
+ if (stats.totalEvents === 0) {
10153
+ return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", "No partial profit/loss events recorded yet.");
10154
+ }
10155
+ const header = columns.map((col) => col.label);
10156
+ const separator = columns.map(() => "---");
10157
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10158
+ const tableData = [header, separator, ...rows];
10159
+ const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
10160
+ return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
10161
+ }
10162
+ /**
10163
+ * Saves symbol report to disk.
10164
+ *
10165
+ * @param symbol - Trading pair symbol
10166
+ * @param path - Directory path to save report (default: "./dump/partial")
10167
+ */
10168
+ async dump(symbol, path$1 = "./dump/partial") {
10169
+ const markdown = await this.getReport(symbol);
10170
+ try {
10171
+ const dir = path.join(process.cwd(), path$1);
10172
+ await fs.mkdir(dir, { recursive: true });
10173
+ const filename = `${symbol}.md`;
10174
+ const filepath = path.join(dir, filename);
10175
+ await fs.writeFile(filepath, markdown, "utf-8");
10176
+ console.log(`Partial profit/loss report saved: ${filepath}`);
10177
+ }
10178
+ catch (error) {
10179
+ console.error(`Failed to save markdown report:`, error);
10180
+ }
10181
+ }
10182
+ }
10183
+ /**
10184
+ * Service for generating and saving partial profit/loss markdown reports.
10185
+ *
10186
+ * Features:
10187
+ * - Listens to partial profit and loss events via partialProfitSubject/partialLossSubject
10188
+ * - Accumulates all events (profit, loss) per symbol
10189
+ * - Generates markdown tables with detailed event information
10190
+ * - Provides statistics (total profit/loss events)
10191
+ * - Saves reports to disk in dump/partial/{symbol}.md
10192
+ *
10193
+ * @example
10194
+ * ```typescript
10195
+ * const service = new PartialMarkdownService();
10196
+ *
10197
+ * // Service automatically subscribes to subjects on init
10198
+ * // No manual callback setup needed
10199
+ *
10200
+ * // Later: generate and save report
10201
+ * await service.dump("BTCUSDT");
10202
+ * ```
10203
+ */
10204
+ class PartialMarkdownService {
10205
+ constructor() {
10206
+ /** Logger service for debug output */
10207
+ this.loggerService = inject(TYPES.loggerService);
10208
+ /**
10209
+ * Memoized function to get or create ReportStorage for a symbol.
10210
+ * Each symbol gets its own isolated storage instance.
10211
+ */
10212
+ this.getStorage = functoolsKit.memoize(([symbol]) => `${symbol}`, () => new ReportStorage());
10213
+ /**
10214
+ * Processes profit events and accumulates them.
10215
+ * Should be called from partialProfitSubject subscription.
10216
+ *
10217
+ * @param data - Profit event data
10218
+ *
10219
+ * @example
10220
+ * ```typescript
10221
+ * const service = new PartialMarkdownService();
10222
+ * // Service automatically subscribes in init()
10223
+ * ```
10224
+ */
10225
+ this.tickProfit = async (data) => {
10226
+ this.loggerService.log("partialMarkdownService tickProfit", {
10227
+ data,
10228
+ });
10229
+ const storage = this.getStorage(data.symbol);
10230
+ storage.addProfitEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
10231
+ };
10232
+ /**
10233
+ * Processes loss events and accumulates them.
10234
+ * Should be called from partialLossSubject subscription.
10235
+ *
10236
+ * @param data - Loss event data
10237
+ *
10238
+ * @example
10239
+ * ```typescript
10240
+ * const service = new PartialMarkdownService();
10241
+ * // Service automatically subscribes in init()
10242
+ * ```
10243
+ */
10244
+ this.tickLoss = async (data) => {
10245
+ this.loggerService.log("partialMarkdownService tickLoss", {
10246
+ data,
10247
+ });
10248
+ const storage = this.getStorage(data.symbol);
10249
+ storage.addLossEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
10250
+ };
10251
+ /**
10252
+ * Gets statistical data from all partial profit/loss events for a symbol.
10253
+ * Delegates to ReportStorage.getData().
10254
+ *
10255
+ * @param symbol - Trading pair symbol to get data for
10256
+ * @returns Statistical data object with all metrics
10257
+ *
10258
+ * @example
10259
+ * ```typescript
10260
+ * const service = new PartialMarkdownService();
10261
+ * const stats = await service.getData("BTCUSDT");
10262
+ * console.log(stats.totalProfit, stats.totalLoss);
10263
+ * ```
10264
+ */
10265
+ this.getData = async (symbol) => {
10266
+ this.loggerService.log("partialMarkdownService getData", {
10267
+ symbol,
10268
+ });
10269
+ const storage = this.getStorage(symbol);
10270
+ return storage.getData();
10271
+ };
10272
+ /**
10273
+ * Generates markdown report with all partial events for a symbol.
10274
+ * Delegates to ReportStorage.getReport().
10275
+ *
10276
+ * @param symbol - Trading pair symbol to generate report for
10277
+ * @returns Markdown formatted report string with table of all events
10278
+ *
10279
+ * @example
10280
+ * ```typescript
10281
+ * const service = new PartialMarkdownService();
10282
+ * const markdown = await service.getReport("BTCUSDT");
10283
+ * console.log(markdown);
10284
+ * ```
10285
+ */
10286
+ this.getReport = async (symbol) => {
10287
+ this.loggerService.log("partialMarkdownService getReport", {
10288
+ symbol,
10289
+ });
10290
+ const storage = this.getStorage(symbol);
10291
+ return storage.getReport(symbol);
10292
+ };
10293
+ /**
10294
+ * Saves symbol report to disk.
10295
+ * Creates directory if it doesn't exist.
10296
+ * Delegates to ReportStorage.dump().
10297
+ *
10298
+ * @param symbol - Trading pair symbol to save report for
10299
+ * @param path - Directory path to save report (default: "./dump/partial")
10300
+ *
10301
+ * @example
10302
+ * ```typescript
10303
+ * const service = new PartialMarkdownService();
10304
+ *
10305
+ * // Save to default path: ./dump/partial/BTCUSDT.md
10306
+ * await service.dump("BTCUSDT");
10307
+ *
10308
+ * // Save to custom path: ./custom/path/BTCUSDT.md
10309
+ * await service.dump("BTCUSDT", "./custom/path");
10310
+ * ```
10311
+ */
10312
+ this.dump = async (symbol, path = "./dump/partial") => {
10313
+ this.loggerService.log("partialMarkdownService dump", {
10314
+ symbol,
10315
+ path,
10316
+ });
10317
+ const storage = this.getStorage(symbol);
10318
+ await storage.dump(symbol, path);
10319
+ };
10320
+ /**
10321
+ * Clears accumulated event data from storage.
10322
+ * If symbol is provided, clears only that symbol's data.
10323
+ * If symbol is omitted, clears all symbols' data.
10324
+ *
10325
+ * @param symbol - Optional symbol to clear specific symbol data
10326
+ *
10327
+ * @example
10328
+ * ```typescript
10329
+ * const service = new PartialMarkdownService();
10330
+ *
10331
+ * // Clear specific symbol data
10332
+ * await service.clear("BTCUSDT");
10333
+ *
10334
+ * // Clear all symbols' data
10335
+ * await service.clear();
10336
+ * ```
10337
+ */
10338
+ this.clear = async (symbol) => {
10339
+ this.loggerService.log("partialMarkdownService clear", {
10340
+ symbol,
10341
+ });
10342
+ this.getStorage.clear(symbol);
10343
+ };
10344
+ /**
10345
+ * Initializes the service by subscribing to partial profit/loss events.
10346
+ * Uses singleshot to ensure initialization happens only once.
10347
+ * Automatically called on first use.
10348
+ *
10349
+ * @example
10350
+ * ```typescript
10351
+ * const service = new PartialMarkdownService();
10352
+ * await service.init(); // Subscribe to profit/loss events
10353
+ * ```
10354
+ */
10355
+ this.init = functoolsKit.singleshot(async () => {
10356
+ this.loggerService.log("partialMarkdownService init");
10357
+ partialProfitSubject.subscribe(this.tickProfit);
10358
+ partialLossSubject.subscribe(this.tickLoss);
10359
+ });
10360
+ }
10361
+ }
10362
+
10363
+ /**
10364
+ * Global service for partial profit/loss tracking.
10365
+ *
10366
+ * Thin delegation layer that forwards operations to PartialConnectionService.
10367
+ * Provides centralized logging for all partial operations at the global level.
10368
+ *
10369
+ * Architecture:
10370
+ * - Injected into ClientStrategy constructor via IStrategyParams
10371
+ * - Delegates all operations to PartialConnectionService
10372
+ * - Logs operations at "partialGlobalService" level before delegation
10373
+ *
10374
+ * Purpose:
10375
+ * - Single injection point for ClientStrategy (dependency injection pattern)
10376
+ * - Centralized logging for monitoring partial operations
10377
+ * - Layer of abstraction between strategy and connection layer
10378
+ *
10379
+ * @example
10380
+ * ```typescript
10381
+ * // Service injected into ClientStrategy via DI
10382
+ * const strategy = new ClientStrategy({
10383
+ * partial: partialGlobalService,
10384
+ * ...
10385
+ * });
10386
+ *
10387
+ * // Called during signal monitoring
10388
+ * await strategy.params.partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
10389
+ * // Logs at global level → delegates to PartialConnectionService
10390
+ * ```
10391
+ */
10392
+ class PartialGlobalService {
10393
+ constructor() {
10394
+ /**
10395
+ * Logger service injected from DI container.
10396
+ * Used for logging operations at global service level.
10397
+ */
10398
+ this.loggerService = inject(TYPES.loggerService);
10399
+ /**
10400
+ * Connection service injected from DI container.
10401
+ * Handles actual ClientPartial instance creation and management.
10402
+ */
10403
+ this.partialConnectionService = inject(TYPES.partialConnectionService);
10404
+ /**
10405
+ * Processes profit state and emits events for newly reached profit levels.
10406
+ *
10407
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10408
+ *
10409
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10410
+ * @param data - Signal row data
10411
+ * @param currentPrice - Current market price
10412
+ * @param revenuePercent - Current profit percentage (positive value)
10413
+ * @param backtest - True if backtest mode, false if live mode
10414
+ * @param when - Event timestamp (current time for live, candle time for backtest)
10415
+ * @returns Promise that resolves when profit processing is complete
10416
+ */
10417
+ this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
10418
+ this.loggerService.log("partialGlobalService profit", {
10419
+ symbol,
10420
+ data,
10421
+ currentPrice,
10422
+ revenuePercent,
10423
+ backtest,
10424
+ when,
10425
+ });
10426
+ return await this.partialConnectionService.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
10427
+ };
10428
+ /**
10429
+ * Processes loss state and emits events for newly reached loss levels.
10430
+ *
10431
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10432
+ *
10433
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10434
+ * @param data - Signal row data
10435
+ * @param currentPrice - Current market price
10436
+ * @param lossPercent - Current loss percentage (negative value)
10437
+ * @param backtest - True if backtest mode, false if live mode
10438
+ * @param when - Event timestamp (current time for live, candle time for backtest)
10439
+ * @returns Promise that resolves when loss processing is complete
10440
+ */
10441
+ this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
10442
+ this.loggerService.log("partialGlobalService loss", {
10443
+ symbol,
10444
+ data,
10445
+ currentPrice,
10446
+ lossPercent,
10447
+ backtest,
10448
+ when,
10449
+ });
10450
+ return await this.partialConnectionService.loss(symbol, data, currentPrice, lossPercent, backtest, when);
10451
+ };
10452
+ /**
10453
+ * Clears partial profit/loss state when signal closes.
10454
+ *
10455
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10456
+ *
10457
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10458
+ * @param data - Signal row data
10459
+ * @param priceClose - Final closing price
10460
+ * @returns Promise that resolves when clear is complete
10461
+ */
10462
+ this.clear = async (symbol, data, priceClose) => {
10463
+ this.loggerService.log("partialGlobalService profit", {
10464
+ symbol,
10465
+ data,
10466
+ priceClose,
10467
+ });
10468
+ return await this.partialConnectionService.clear(symbol, data, priceClose);
10469
+ };
10470
+ }
10471
+ }
10472
+
10473
+ {
10474
+ provide(TYPES.loggerService, () => new LoggerService());
10475
+ }
10476
+ {
10477
+ provide(TYPES.executionContextService, () => new ExecutionContextService());
10478
+ provide(TYPES.methodContextService, () => new MethodContextService());
10479
+ }
10480
+ {
10481
+ provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
10482
+ provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
10483
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
10484
+ provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
10485
+ provide(TYPES.riskConnectionService, () => new RiskConnectionService());
10486
+ provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
10487
+ provide(TYPES.partialConnectionService, () => new PartialConnectionService());
10488
+ }
10489
+ {
10490
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
10491
+ provide(TYPES.strategySchemaService, () => new StrategySchemaService());
10492
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
10493
+ provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
10494
+ provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
10495
+ provide(TYPES.riskSchemaService, () => new RiskSchemaService());
10496
+ provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
10497
+ }
10498
+ {
10499
+ provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
7878
10500
  provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
7879
10501
  provide(TYPES.frameGlobalService, () => new FrameGlobalService());
7880
10502
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
7881
10503
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
10504
+ provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
10505
+ provide(TYPES.partialGlobalService, () => new PartialGlobalService());
7882
10506
  }
7883
10507
  {
7884
10508
  provide(TYPES.liveCommandService, () => new LiveCommandService());
@@ -7902,6 +10526,7 @@ class RiskValidationService {
7902
10526
  provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
7903
10527
  provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
7904
10528
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
10529
+ provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
7905
10530
  }
7906
10531
  {
7907
10532
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -7910,6 +10535,10 @@ class RiskValidationService {
7910
10535
  provide(TYPES.walkerValidationService, () => new WalkerValidationService());
7911
10536
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
7912
10537
  provide(TYPES.riskValidationService, () => new RiskValidationService());
10538
+ provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
10539
+ }
10540
+ {
10541
+ provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
7913
10542
  }
7914
10543
 
7915
10544
  const baseServices = {
@@ -7925,6 +10554,8 @@ const connectionServices = {
7925
10554
  frameConnectionService: inject(TYPES.frameConnectionService),
7926
10555
  sizingConnectionService: inject(TYPES.sizingConnectionService),
7927
10556
  riskConnectionService: inject(TYPES.riskConnectionService),
10557
+ optimizerConnectionService: inject(TYPES.optimizerConnectionService),
10558
+ partialConnectionService: inject(TYPES.partialConnectionService),
7928
10559
  };
7929
10560
  const schemaServices = {
7930
10561
  exchangeSchemaService: inject(TYPES.exchangeSchemaService),
@@ -7933,6 +10564,7 @@ const schemaServices = {
7933
10564
  walkerSchemaService: inject(TYPES.walkerSchemaService),
7934
10565
  sizingSchemaService: inject(TYPES.sizingSchemaService),
7935
10566
  riskSchemaService: inject(TYPES.riskSchemaService),
10567
+ optimizerSchemaService: inject(TYPES.optimizerSchemaService),
7936
10568
  };
7937
10569
  const globalServices = {
7938
10570
  exchangeGlobalService: inject(TYPES.exchangeGlobalService),
@@ -7940,6 +10572,8 @@ const globalServices = {
7940
10572
  frameGlobalService: inject(TYPES.frameGlobalService),
7941
10573
  sizingGlobalService: inject(TYPES.sizingGlobalService),
7942
10574
  riskGlobalService: inject(TYPES.riskGlobalService),
10575
+ optimizerGlobalService: inject(TYPES.optimizerGlobalService),
10576
+ partialGlobalService: inject(TYPES.partialGlobalService),
7943
10577
  };
7944
10578
  const commandServices = {
7945
10579
  liveCommandService: inject(TYPES.liveCommandService),
@@ -7963,6 +10597,7 @@ const markdownServices = {
7963
10597
  performanceMarkdownService: inject(TYPES.performanceMarkdownService),
7964
10598
  walkerMarkdownService: inject(TYPES.walkerMarkdownService),
7965
10599
  heatMarkdownService: inject(TYPES.heatMarkdownService),
10600
+ partialMarkdownService: inject(TYPES.partialMarkdownService),
7966
10601
  };
7967
10602
  const validationServices = {
7968
10603
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -7971,6 +10606,10 @@ const validationServices = {
7971
10606
  walkerValidationService: inject(TYPES.walkerValidationService),
7972
10607
  sizingValidationService: inject(TYPES.sizingValidationService),
7973
10608
  riskValidationService: inject(TYPES.riskValidationService),
10609
+ optimizerValidationService: inject(TYPES.optimizerValidationService),
10610
+ };
10611
+ const templateServices = {
10612
+ optimizerTemplateService: inject(TYPES.optimizerTemplateService),
7974
10613
  };
7975
10614
  const backtest = {
7976
10615
  ...baseServices,
@@ -7983,6 +10622,7 @@ const backtest = {
7983
10622
  ...logicPublicServices,
7984
10623
  ...markdownServices,
7985
10624
  ...validationServices,
10625
+ ...templateServices,
7986
10626
  };
7987
10627
  init();
7988
10628
  var backtest$1 = backtest;
@@ -8028,6 +10668,7 @@ const ADD_FRAME_METHOD_NAME = "add.addFrame";
8028
10668
  const ADD_WALKER_METHOD_NAME = "add.addWalker";
8029
10669
  const ADD_SIZING_METHOD_NAME = "add.addSizing";
8030
10670
  const ADD_RISK_METHOD_NAME = "add.addRisk";
10671
+ const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizer";
8031
10672
  /**
8032
10673
  * Registers a trading strategy in the framework.
8033
10674
  *
@@ -8319,6 +10960,100 @@ function addRisk(riskSchema) {
8319
10960
  backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
8320
10961
  backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
8321
10962
  }
10963
+ /**
10964
+ * Registers an optimizer configuration in the framework.
10965
+ *
10966
+ * The optimizer generates trading strategies by:
10967
+ * - Collecting data from multiple sources across training periods
10968
+ * - Building LLM conversation history with fetched data
10969
+ * - Generating strategy prompts using getPrompt()
10970
+ * - Creating executable backtest code with templates
10971
+ *
10972
+ * The optimizer produces a complete .mjs file containing:
10973
+ * - Exchange, Frame, Strategy, and Walker configurations
10974
+ * - Multi-timeframe analysis logic
10975
+ * - LLM integration for signal generation
10976
+ * - Event listeners for progress tracking
10977
+ *
10978
+ * @param optimizerSchema - Optimizer configuration object
10979
+ * @param optimizerSchema.optimizerName - Unique optimizer identifier
10980
+ * @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
10981
+ * @param optimizerSchema.rangeTest - Testing time range for strategy validation
10982
+ * @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
10983
+ * @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
10984
+ * @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
10985
+ * @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
10986
+ *
10987
+ * @example
10988
+ * ```typescript
10989
+ * // Basic optimizer with single data source
10990
+ * addOptimizer({
10991
+ * optimizerName: "llm-strategy-generator",
10992
+ * rangeTrain: [
10993
+ * {
10994
+ * note: "Bull market period",
10995
+ * startDate: new Date("2024-01-01"),
10996
+ * endDate: new Date("2024-01-31"),
10997
+ * },
10998
+ * {
10999
+ * note: "Bear market period",
11000
+ * startDate: new Date("2024-02-01"),
11001
+ * endDate: new Date("2024-02-28"),
11002
+ * },
11003
+ * ],
11004
+ * rangeTest: {
11005
+ * note: "Validation period",
11006
+ * startDate: new Date("2024-03-01"),
11007
+ * endDate: new Date("2024-03-31"),
11008
+ * },
11009
+ * source: [
11010
+ * {
11011
+ * name: "historical-backtests",
11012
+ * fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
11013
+ * // Fetch historical backtest results from database
11014
+ * return await db.backtests.find({
11015
+ * symbol,
11016
+ * date: { $gte: startDate, $lte: endDate },
11017
+ * })
11018
+ * .skip(offset)
11019
+ * .limit(limit);
11020
+ * },
11021
+ * user: async (symbol, data, name) => {
11022
+ * return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
11023
+ * },
11024
+ * assistant: async (symbol, data, name) => {
11025
+ * return "Historical data analyzed successfully";
11026
+ * },
11027
+ * },
11028
+ * ],
11029
+ * getPrompt: async (symbol, messages) => {
11030
+ * // Generate strategy prompt from conversation
11031
+ * return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
11032
+ * },
11033
+ * callbacks: {
11034
+ * onData: (symbol, strategyData) => {
11035
+ * console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
11036
+ * },
11037
+ * onCode: (symbol, code) => {
11038
+ * console.log(`Generated ${code.length} characters of code for ${symbol}`);
11039
+ * },
11040
+ * onDump: (symbol, filepath) => {
11041
+ * console.log(`Saved strategy to ${filepath}`);
11042
+ * },
11043
+ * onSourceData: (symbol, sourceName, data, startDate, endDate) => {
11044
+ * console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
11045
+ * },
11046
+ * },
11047
+ * });
11048
+ * ```
11049
+ */
11050
+ function addOptimizer(optimizerSchema) {
11051
+ backtest$1.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
11052
+ optimizerSchema,
11053
+ });
11054
+ backtest$1.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
11055
+ backtest$1.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
11056
+ }
8322
11057
 
8323
11058
  const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
8324
11059
  const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
@@ -8326,6 +11061,7 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
8326
11061
  const LIST_WALKERS_METHOD_NAME = "list.listWalkers";
8327
11062
  const LIST_SIZINGS_METHOD_NAME = "list.listSizings";
8328
11063
  const LIST_RISKS_METHOD_NAME = "list.listRisks";
11064
+ const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizers";
8329
11065
  /**
8330
11066
  * Returns a list of all registered exchange schemas.
8331
11067
  *
@@ -8523,6 +11259,46 @@ async function listRisks() {
8523
11259
  backtest$1.loggerService.log(LIST_RISKS_METHOD_NAME);
8524
11260
  return await backtest$1.riskValidationService.list();
8525
11261
  }
11262
+ /**
11263
+ * Returns a list of all registered optimizer schemas.
11264
+ *
11265
+ * Retrieves all optimizers that have been registered via addOptimizer().
11266
+ * Useful for debugging, documentation, or building dynamic UIs.
11267
+ *
11268
+ * @returns Array of optimizer schemas with their configurations
11269
+ *
11270
+ * @example
11271
+ * ```typescript
11272
+ * import { listOptimizers, addOptimizer } from "backtest-kit";
11273
+ *
11274
+ * addOptimizer({
11275
+ * optimizerName: "llm-strategy-generator",
11276
+ * note: "Generates trading strategies using LLM",
11277
+ * rangeTrain: [
11278
+ * {
11279
+ * note: "Training period 1",
11280
+ * startDate: new Date("2024-01-01"),
11281
+ * endDate: new Date("2024-01-31"),
11282
+ * },
11283
+ * ],
11284
+ * rangeTest: {
11285
+ * note: "Testing period",
11286
+ * startDate: new Date("2024-02-01"),
11287
+ * endDate: new Date("2024-02-28"),
11288
+ * },
11289
+ * source: [],
11290
+ * getPrompt: async (symbol, messages) => "Generate strategy",
11291
+ * });
11292
+ *
11293
+ * const optimizers = listOptimizers();
11294
+ * console.log(optimizers);
11295
+ * // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
11296
+ * ```
11297
+ */
11298
+ async function listOptimizers() {
11299
+ backtest$1.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
11300
+ return await backtest$1.optimizerValidationService.list();
11301
+ }
8526
11302
 
8527
11303
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
8528
11304
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
@@ -8537,12 +11313,17 @@ const LISTEN_DONE_BACKTEST_METHOD_NAME = "event.listenDoneBacktest";
8537
11313
  const LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME = "event.listenDoneBacktestOnce";
8538
11314
  const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
8539
11315
  const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
8540
- const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
11316
+ const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
11317
+ const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
8541
11318
  const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
8542
11319
  const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
8543
11320
  const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
8544
11321
  const LISTEN_WALKER_COMPLETE_METHOD_NAME = "event.listenWalkerComplete";
8545
11322
  const LISTEN_VALIDATION_METHOD_NAME = "event.listenValidation";
11323
+ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
11324
+ const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
11325
+ const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
11326
+ const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
8546
11327
  /**
8547
11328
  * Subscribes to all signal events with queued async processing.
8548
11329
  *
@@ -8923,21 +11704,55 @@ function listenDoneWalkerOnce(filterFn, fn) {
8923
11704
  * Events are processed sequentially in order received, even if callback is async.
8924
11705
  * Uses queued wrapper to prevent concurrent execution of the callback.
8925
11706
  *
8926
- * @param fn - Callback function to handle progress events
11707
+ * @param fn - Callback function to handle progress events
11708
+ * @returns Unsubscribe function to stop listening to events
11709
+ *
11710
+ * @example
11711
+ * ```typescript
11712
+ * import { listenBacktestProgress, Backtest } from "backtest-kit";
11713
+ *
11714
+ * const unsubscribe = listenBacktestProgress((event) => {
11715
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
11716
+ * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
11717
+ * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
11718
+ * });
11719
+ *
11720
+ * Backtest.background("BTCUSDT", {
11721
+ * strategyName: "my-strategy",
11722
+ * exchangeName: "binance",
11723
+ * frameName: "1d-backtest"
11724
+ * });
11725
+ *
11726
+ * // Later: stop listening
11727
+ * unsubscribe();
11728
+ * ```
11729
+ */
11730
+ function listenBacktestProgress(fn) {
11731
+ backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
11732
+ return progressBacktestEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
11733
+ }
11734
+ /**
11735
+ * Subscribes to walker progress events with queued async processing.
11736
+ *
11737
+ * Emits during Walker.run() execution after each strategy completes.
11738
+ * Events are processed sequentially in order received, even if callback is async.
11739
+ * Uses queued wrapper to prevent concurrent execution of the callback.
11740
+ *
11741
+ * @param fn - Callback function to handle walker progress events
8927
11742
  * @returns Unsubscribe function to stop listening to events
8928
11743
  *
8929
11744
  * @example
8930
11745
  * ```typescript
8931
- * import { listenProgress, Backtest } from "backtest-kit";
11746
+ * import { listenWalkerProgress, Walker } from "backtest-kit";
8932
11747
  *
8933
- * const unsubscribe = listenProgress((event) => {
11748
+ * const unsubscribe = listenWalkerProgress((event) => {
8934
11749
  * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
8935
- * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
8936
- * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
11750
+ * console.log(`${event.processedStrategies} / ${event.totalStrategies} strategies`);
11751
+ * console.log(`Walker: ${event.walkerName}, Symbol: ${event.symbol}`);
8937
11752
  * });
8938
11753
  *
8939
- * Backtest.background("BTCUSDT", {
8940
- * strategyName: "my-strategy",
11754
+ * Walker.run("BTCUSDT", {
11755
+ * walkerName: "my-walker",
8941
11756
  * exchangeName: "binance",
8942
11757
  * frameName: "1d-backtest"
8943
11758
  * });
@@ -8946,9 +11761,9 @@ function listenDoneWalkerOnce(filterFn, fn) {
8946
11761
  * unsubscribe();
8947
11762
  * ```
8948
11763
  */
8949
- function listenProgress(fn) {
8950
- backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
8951
- return progressEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
11764
+ function listenWalkerProgress(fn) {
11765
+ backtest$1.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
11766
+ return progressWalkerEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
8952
11767
  }
8953
11768
  /**
8954
11769
  * Subscribes to performance metric events with queued async processing.
@@ -9126,6 +11941,130 @@ function listenValidation(fn) {
9126
11941
  backtest$1.loggerService.log(LISTEN_VALIDATION_METHOD_NAME);
9127
11942
  return validationSubject.subscribe(functoolsKit.queued(async (error) => fn(error)));
9128
11943
  }
11944
+ /**
11945
+ * Subscribes to partial profit level events with queued async processing.
11946
+ *
11947
+ * Emits when a signal reaches a profit level milestone (10%, 20%, 30%, etc).
11948
+ * Events are processed sequentially in order received, even if callback is async.
11949
+ * Uses queued wrapper to prevent concurrent execution of the callback.
11950
+ *
11951
+ * @param fn - Callback function to handle partial profit events
11952
+ * @returns Unsubscribe function to stop listening to events
11953
+ *
11954
+ * @example
11955
+ * ```typescript
11956
+ * import { listenPartialProfit } from "./function/event";
11957
+ *
11958
+ * const unsubscribe = listenPartialProfit((event) => {
11959
+ * console.log(`Signal ${event.data.id} reached ${event.level}% profit`);
11960
+ * console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
11961
+ * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
11962
+ * });
11963
+ *
11964
+ * // Later: stop listening
11965
+ * unsubscribe();
11966
+ * ```
11967
+ */
11968
+ function listenPartialProfit(fn) {
11969
+ backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_METHOD_NAME);
11970
+ return partialProfitSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
11971
+ }
11972
+ /**
11973
+ * Subscribes to filtered partial profit level events with one-time execution.
11974
+ *
11975
+ * Listens for events matching the filter predicate, then executes callback once
11976
+ * and automatically unsubscribes. Useful for waiting for specific profit conditions.
11977
+ *
11978
+ * @param filterFn - Predicate to filter which events trigger the callback
11979
+ * @param fn - Callback function to handle the filtered event (called only once)
11980
+ * @returns Unsubscribe function to cancel the listener before it fires
11981
+ *
11982
+ * @example
11983
+ * ```typescript
11984
+ * import { listenPartialProfitOnce } from "./function/event";
11985
+ *
11986
+ * // Wait for first 50% profit level on any signal
11987
+ * listenPartialProfitOnce(
11988
+ * (event) => event.level === 50,
11989
+ * (event) => console.log("50% profit reached:", event.data.id)
11990
+ * );
11991
+ *
11992
+ * // Wait for 30% profit on BTCUSDT
11993
+ * const cancel = listenPartialProfitOnce(
11994
+ * (event) => event.symbol === "BTCUSDT" && event.level === 30,
11995
+ * (event) => console.log("BTCUSDT hit 30% profit")
11996
+ * );
11997
+ *
11998
+ * // Cancel if needed before event fires
11999
+ * cancel();
12000
+ * ```
12001
+ */
12002
+ function listenPartialProfitOnce(filterFn, fn) {
12003
+ backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME);
12004
+ return partialProfitSubject.filter(filterFn).once(fn);
12005
+ }
12006
+ /**
12007
+ * Subscribes to partial loss level events with queued async processing.
12008
+ *
12009
+ * Emits when a signal reaches a loss level milestone (10%, 20%, 30%, etc).
12010
+ * Events are processed sequentially in order received, even if callback is async.
12011
+ * Uses queued wrapper to prevent concurrent execution of the callback.
12012
+ *
12013
+ * @param fn - Callback function to handle partial loss events
12014
+ * @returns Unsubscribe function to stop listening to events
12015
+ *
12016
+ * @example
12017
+ * ```typescript
12018
+ * import { listenPartialLoss } from "./function/event";
12019
+ *
12020
+ * const unsubscribe = listenPartialLoss((event) => {
12021
+ * console.log(`Signal ${event.data.id} reached ${event.level}% loss`);
12022
+ * console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
12023
+ * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
12024
+ * });
12025
+ *
12026
+ * // Later: stop listening
12027
+ * unsubscribe();
12028
+ * ```
12029
+ */
12030
+ function listenPartialLoss(fn) {
12031
+ backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_METHOD_NAME);
12032
+ return partialLossSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
12033
+ }
12034
+ /**
12035
+ * Subscribes to filtered partial loss level events with one-time execution.
12036
+ *
12037
+ * Listens for events matching the filter predicate, then executes callback once
12038
+ * and automatically unsubscribes. Useful for waiting for specific loss conditions.
12039
+ *
12040
+ * @param filterFn - Predicate to filter which events trigger the callback
12041
+ * @param fn - Callback function to handle the filtered event (called only once)
12042
+ * @returns Unsubscribe function to cancel the listener before it fires
12043
+ *
12044
+ * @example
12045
+ * ```typescript
12046
+ * import { listenPartialLossOnce } from "./function/event";
12047
+ *
12048
+ * // Wait for first 20% loss level on any signal
12049
+ * listenPartialLossOnce(
12050
+ * (event) => event.level === 20,
12051
+ * (event) => console.log("20% loss reached:", event.data.id)
12052
+ * );
12053
+ *
12054
+ * // Wait for 10% loss on ETHUSDT in live mode
12055
+ * const cancel = listenPartialLossOnce(
12056
+ * (event) => event.symbol === "ETHUSDT" && event.level === 10 && !event.backtest,
12057
+ * (event) => console.log("ETHUSDT hit 10% loss in live mode")
12058
+ * );
12059
+ *
12060
+ * // Cancel if needed before event fires
12061
+ * cancel();
12062
+ * ```
12063
+ */
12064
+ function listenPartialLossOnce(filterFn, fn) {
12065
+ backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
12066
+ return partialLossSubject.filter(filterFn).once(fn);
12067
+ }
9129
12068
 
9130
12069
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
9131
12070
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -9343,22 +12282,42 @@ class BacktestUtils {
9343
12282
  context,
9344
12283
  });
9345
12284
  let isStopped = false;
12285
+ let isDone = false;
9346
12286
  const task = async () => {
9347
12287
  for await (const _ of this.run(symbol, context)) {
9348
12288
  if (isStopped) {
9349
12289
  break;
9350
12290
  }
9351
12291
  }
9352
- await doneBacktestSubject.next({
9353
- exchangeName: context.exchangeName,
9354
- strategyName: context.strategyName,
9355
- backtest: true,
9356
- symbol,
9357
- });
12292
+ if (!isDone) {
12293
+ await doneBacktestSubject.next({
12294
+ exchangeName: context.exchangeName,
12295
+ strategyName: context.strategyName,
12296
+ backtest: true,
12297
+ symbol,
12298
+ });
12299
+ }
12300
+ isDone = true;
9358
12301
  };
9359
12302
  task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
9360
12303
  return () => {
9361
12304
  backtest$1.strategyGlobalService.stop(context.strategyName);
12305
+ backtest$1.strategyGlobalService
12306
+ .getPendingSignal(context.strategyName)
12307
+ .then(async (pendingSignal) => {
12308
+ if (pendingSignal) {
12309
+ return;
12310
+ }
12311
+ if (!isDone) {
12312
+ await doneBacktestSubject.next({
12313
+ exchangeName: context.exchangeName,
12314
+ strategyName: context.strategyName,
12315
+ backtest: true,
12316
+ symbol,
12317
+ });
12318
+ }
12319
+ isDone = true;
12320
+ });
9362
12321
  isStopped = true;
9363
12322
  };
9364
12323
  };
@@ -9402,11 +12361,11 @@ class BacktestUtils {
9402
12361
  * Saves strategy report to disk.
9403
12362
  *
9404
12363
  * @param strategyName - Strategy name to save report for
9405
- * @param path - Optional directory path to save report (default: "./logs/backtest")
12364
+ * @param path - Optional directory path to save report (default: "./dump/backtest")
9406
12365
  *
9407
12366
  * @example
9408
12367
  * ```typescript
9409
- * // Save to default path: ./logs/backtest/my-strategy.md
12368
+ * // Save to default path: ./dump/backtest/my-strategy.md
9410
12369
  * await Backtest.dump("my-strategy");
9411
12370
  *
9412
12371
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9553,8 +12512,11 @@ class LiveUtils {
9553
12512
  return () => {
9554
12513
  backtest$1.strategyGlobalService.stop(context.strategyName);
9555
12514
  backtest$1.strategyGlobalService
9556
- .getPendingSignal(symbol, new Date(), false)
9557
- .then(async () => {
12515
+ .getPendingSignal(context.strategyName)
12516
+ .then(async (pendingSignal) => {
12517
+ if (pendingSignal) {
12518
+ return;
12519
+ }
9558
12520
  if (!isDone) {
9559
12521
  await doneLiveSubject.next({
9560
12522
  exchangeName: context.exchangeName,
@@ -9608,11 +12570,11 @@ class LiveUtils {
9608
12570
  * Saves strategy report to disk.
9609
12571
  *
9610
12572
  * @param strategyName - Strategy name to save report for
9611
- * @param path - Optional directory path to save report (default: "./logs/live")
12573
+ * @param path - Optional directory path to save report (default: "./dump/live")
9612
12574
  *
9613
12575
  * @example
9614
12576
  * ```typescript
9615
- * // Save to default path: ./logs/live/my-strategy.md
12577
+ * // Save to default path: ./dump/live/my-strategy.md
9616
12578
  * await Live.dump("my-strategy");
9617
12579
  *
9618
12580
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9648,7 +12610,6 @@ const Live = new LiveUtils();
9648
12610
  const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
9649
12611
  const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
9650
12612
  const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
9651
- const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
9652
12613
  /**
9653
12614
  * Utility class for scheduled signals reporting operations.
9654
12615
  *
@@ -9716,11 +12677,11 @@ class ScheduleUtils {
9716
12677
  * Saves strategy report to disk.
9717
12678
  *
9718
12679
  * @param strategyName - Strategy name to save report for
9719
- * @param path - Optional directory path to save report (default: "./logs/schedule")
12680
+ * @param path - Optional directory path to save report (default: "./dump/schedule")
9720
12681
  *
9721
12682
  * @example
9722
12683
  * ```typescript
9723
- * // Save to default path: ./logs/schedule/my-strategy.md
12684
+ * // Save to default path: ./dump/schedule/my-strategy.md
9724
12685
  * await Schedule.dump("my-strategy");
9725
12686
  *
9726
12687
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9734,28 +12695,6 @@ class ScheduleUtils {
9734
12695
  });
9735
12696
  await backtest$1.scheduleMarkdownService.dump(strategyName, path);
9736
12697
  };
9737
- /**
9738
- * Clears accumulated scheduled signal data from storage.
9739
- * If strategyName is provided, clears only that strategy's data.
9740
- * If strategyName is omitted, clears all strategies' data.
9741
- *
9742
- * @param strategyName - Optional strategy name to clear specific strategy data
9743
- *
9744
- * @example
9745
- * ```typescript
9746
- * // Clear specific strategy data
9747
- * await Schedule.clear("my-strategy");
9748
- *
9749
- * // Clear all strategies' data
9750
- * await Schedule.clear();
9751
- * ```
9752
- */
9753
- this.clear = async (strategyName) => {
9754
- backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
9755
- strategyName,
9756
- });
9757
- await backtest$1.scheduleMarkdownService.clear(strategyName);
9758
- };
9759
12698
  }
9760
12699
  }
9761
12700
  /**
@@ -9862,21 +12801,21 @@ class Performance {
9862
12801
  * Saves performance report to disk.
9863
12802
  *
9864
12803
  * Creates directory if it doesn't exist.
9865
- * Default path: ./logs/performance/{strategyName}.md
12804
+ * Default path: ./dump/performance/{strategyName}.md
9866
12805
  *
9867
12806
  * @param strategyName - Strategy name to save report for
9868
12807
  * @param path - Optional custom directory path
9869
12808
  *
9870
12809
  * @example
9871
12810
  * ```typescript
9872
- * // Save to default path: ./logs/performance/my-strategy.md
12811
+ * // Save to default path: ./dump/performance/my-strategy.md
9873
12812
  * await Performance.dump("my-strategy");
9874
12813
  *
9875
12814
  * // Save to custom path: ./reports/perf/my-strategy.md
9876
12815
  * await Performance.dump("my-strategy", "./reports/perf");
9877
12816
  * ```
9878
12817
  */
9879
- static async dump(strategyName, path = "./logs/performance") {
12818
+ static async dump(strategyName, path = "./dump/performance") {
9880
12819
  return backtest$1.performanceMarkdownService.dump(strategyName, path);
9881
12820
  }
9882
12821
  /**
@@ -9990,25 +12929,39 @@ class WalkerUtils {
9990
12929
  });
9991
12930
  const walkerSchema = backtest$1.walkerSchemaService.get(context.walkerName);
9992
12931
  let isStopped = false;
12932
+ let isDone = false;
9993
12933
  const task = async () => {
9994
12934
  for await (const _ of this.run(symbol, context)) {
9995
12935
  if (isStopped) {
9996
12936
  break;
9997
12937
  }
9998
12938
  }
9999
- await doneWalkerSubject.next({
10000
- exchangeName: walkerSchema.exchangeName,
10001
- strategyName: context.walkerName,
10002
- backtest: true,
10003
- symbol,
10004
- });
12939
+ if (!isDone) {
12940
+ await doneWalkerSubject.next({
12941
+ exchangeName: walkerSchema.exchangeName,
12942
+ strategyName: context.walkerName,
12943
+ backtest: true,
12944
+ symbol,
12945
+ });
12946
+ }
12947
+ isDone = true;
10005
12948
  };
10006
12949
  task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
10007
12950
  return () => {
10008
- isStopped = true;
10009
12951
  for (const strategyName of walkerSchema.strategies) {
10010
12952
  backtest$1.strategyGlobalService.stop(strategyName);
10011
12953
  }
12954
+ walkerStopSubject.next(context.walkerName);
12955
+ if (!isDone) {
12956
+ doneWalkerSubject.next({
12957
+ exchangeName: walkerSchema.exchangeName,
12958
+ strategyName: context.walkerName,
12959
+ backtest: true,
12960
+ symbol,
12961
+ });
12962
+ }
12963
+ isDone = true;
12964
+ isStopped = true;
10012
12965
  };
10013
12966
  };
10014
12967
  /**
@@ -10064,11 +13017,11 @@ class WalkerUtils {
10064
13017
  *
10065
13018
  * @param symbol - Trading symbol
10066
13019
  * @param walkerName - Walker name to save report for
10067
- * @param path - Optional directory path to save report (default: "./logs/walker")
13020
+ * @param path - Optional directory path to save report (default: "./dump/walker")
10068
13021
  *
10069
13022
  * @example
10070
13023
  * ```typescript
10071
- * // Save to default path: ./logs/walker/my-walker.md
13024
+ * // Save to default path: ./dump/walker/my-walker.md
10072
13025
  * await Walker.dump("BTCUSDT", "my-walker");
10073
13026
  *
10074
13027
  * // Save to custom path: ./custom/path/my-walker.md
@@ -10197,11 +13150,11 @@ class HeatUtils {
10197
13150
  * Default filename: {strategyName}.md
10198
13151
  *
10199
13152
  * @param strategyName - Strategy name to save heatmap report for
10200
- * @param path - Optional directory path to save report (default: "./logs/heatmap")
13153
+ * @param path - Optional directory path to save report (default: "./dump/heatmap")
10201
13154
  *
10202
13155
  * @example
10203
13156
  * ```typescript
10204
- * // Save to default path: ./logs/heatmap/my-strategy.md
13157
+ * // Save to default path: ./dump/heatmap/my-strategy.md
10205
13158
  * await Heat.dump("my-strategy");
10206
13159
  *
10207
13160
  * // Save to custom path: ./reports/my-strategy.md
@@ -10364,20 +13317,279 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
10364
13317
  };
10365
13318
  const PositionSize = PositionSizeUtils;
10366
13319
 
13320
+ const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
13321
+ const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
13322
+ const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
13323
+ /**
13324
+ * Public API utilities for optimizer operations.
13325
+ * Provides high-level methods for strategy generation and code export.
13326
+ *
13327
+ * Usage:
13328
+ * ```typescript
13329
+ * import { Optimizer } from "backtest-kit";
13330
+ *
13331
+ * // Get strategy data
13332
+ * const strategies = await Optimizer.getData("BTCUSDT", {
13333
+ * optimizerName: "my-optimizer"
13334
+ * });
13335
+ *
13336
+ * // Generate code
13337
+ * const code = await Optimizer.getCode("BTCUSDT", {
13338
+ * optimizerName: "my-optimizer"
13339
+ * });
13340
+ *
13341
+ * // Save to file
13342
+ * await Optimizer.dump("BTCUSDT", {
13343
+ * optimizerName: "my-optimizer"
13344
+ * }, "./output");
13345
+ * ```
13346
+ */
13347
+ class OptimizerUtils {
13348
+ constructor() {
13349
+ /**
13350
+ * Fetches data from all sources and generates strategy metadata.
13351
+ * Processes each training range and builds LLM conversation history.
13352
+ *
13353
+ * @param symbol - Trading pair symbol
13354
+ * @param context - Context with optimizerName
13355
+ * @returns Array of generated strategies with conversation context
13356
+ * @throws Error if optimizer not found
13357
+ */
13358
+ this.getData = async (symbol, context) => {
13359
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
13360
+ symbol,
13361
+ context,
13362
+ });
13363
+ return await backtest$1.optimizerGlobalService.getData(symbol, context.optimizerName);
13364
+ };
13365
+ /**
13366
+ * Generates complete executable strategy code.
13367
+ * Includes imports, helpers, strategies, walker, and launcher.
13368
+ *
13369
+ * @param symbol - Trading pair symbol
13370
+ * @param context - Context with optimizerName
13371
+ * @returns Generated TypeScript/JavaScript code as string
13372
+ * @throws Error if optimizer not found
13373
+ */
13374
+ this.getCode = async (symbol, context) => {
13375
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
13376
+ symbol,
13377
+ context,
13378
+ });
13379
+ return await backtest$1.optimizerGlobalService.getCode(symbol, context.optimizerName);
13380
+ };
13381
+ /**
13382
+ * Generates and saves strategy code to file.
13383
+ * Creates directory if needed, writes .mjs file.
13384
+ *
13385
+ * Format: `{optimizerName}_{symbol}.mjs`
13386
+ *
13387
+ * @param symbol - Trading pair symbol
13388
+ * @param context - Context with optimizerName
13389
+ * @param path - Output directory path (default: "./")
13390
+ * @throws Error if optimizer not found or file write fails
13391
+ */
13392
+ this.dump = async (symbol, context, path) => {
13393
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
13394
+ symbol,
13395
+ context,
13396
+ path,
13397
+ });
13398
+ await backtest$1.optimizerGlobalService.dump(symbol, context.optimizerName, path);
13399
+ };
13400
+ }
13401
+ }
13402
+ /**
13403
+ * Singleton instance of OptimizerUtils.
13404
+ * Public API for optimizer operations.
13405
+ *
13406
+ * @example
13407
+ * ```typescript
13408
+ * import { Optimizer } from "backtest-kit";
13409
+ *
13410
+ * await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
13411
+ * ```
13412
+ */
13413
+ const Optimizer = new OptimizerUtils();
13414
+
13415
+ const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
13416
+ const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
13417
+ const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
13418
+ /**
13419
+ * Utility class for accessing partial profit/loss reports and statistics.
13420
+ *
13421
+ * Provides static-like methods (via singleton instance) to retrieve data
13422
+ * accumulated by PartialMarkdownService from partial profit/loss events.
13423
+ *
13424
+ * Features:
13425
+ * - Statistical data extraction (total profit/loss events count)
13426
+ * - Markdown report generation with event tables
13427
+ * - File export to disk
13428
+ *
13429
+ * Data source:
13430
+ * - PartialMarkdownService listens to partialProfitSubject/partialLossSubject
13431
+ * - Accumulates events in ReportStorage (max 250 events per symbol)
13432
+ * - Events include: timestamp, action, symbol, signalId, position, level, price, mode
13433
+ *
13434
+ * @example
13435
+ * ```typescript
13436
+ * import { Partial } from "./classes/Partial";
13437
+ *
13438
+ * // Get statistical data for BTCUSDT
13439
+ * const stats = await Partial.getData("BTCUSDT");
13440
+ * console.log(`Total events: ${stats.totalEvents}`);
13441
+ * console.log(`Profit events: ${stats.totalProfit}`);
13442
+ * console.log(`Loss events: ${stats.totalLoss}`);
13443
+ *
13444
+ * // Generate markdown report
13445
+ * const markdown = await Partial.getReport("BTCUSDT");
13446
+ * console.log(markdown); // Formatted table with all events
13447
+ *
13448
+ * // Export report to file
13449
+ * await Partial.dump("BTCUSDT"); // Saves to ./dump/partial/BTCUSDT.md
13450
+ * await Partial.dump("BTCUSDT", "./custom/path"); // Custom directory
13451
+ * ```
13452
+ */
13453
+ class PartialUtils {
13454
+ constructor() {
13455
+ /**
13456
+ * Retrieves statistical data from accumulated partial profit/loss events.
13457
+ *
13458
+ * Delegates to PartialMarkdownService.getData() which reads from ReportStorage.
13459
+ * Returns aggregated metrics calculated from all profit and loss events.
13460
+ *
13461
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13462
+ * @returns Promise resolving to PartialStatistics object with counts and event list
13463
+ *
13464
+ * @example
13465
+ * ```typescript
13466
+ * const stats = await Partial.getData("BTCUSDT");
13467
+ *
13468
+ * console.log(`Total events: ${stats.totalEvents}`);
13469
+ * console.log(`Profit events: ${stats.totalProfit} (${(stats.totalProfit / stats.totalEvents * 100).toFixed(1)}%)`);
13470
+ * console.log(`Loss events: ${stats.totalLoss} (${(stats.totalLoss / stats.totalEvents * 100).toFixed(1)}%)`);
13471
+ *
13472
+ * // Iterate through all events
13473
+ * for (const event of stats.eventList) {
13474
+ * console.log(`${event.action.toUpperCase()}: Signal ${event.signalId} reached ${event.level}%`);
13475
+ * }
13476
+ * ```
13477
+ */
13478
+ this.getData = async (symbol) => {
13479
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol });
13480
+ return await backtest$1.partialMarkdownService.getData(symbol);
13481
+ };
13482
+ /**
13483
+ * Generates markdown report with all partial profit/loss events for a symbol.
13484
+ *
13485
+ * Creates formatted table containing:
13486
+ * - Action (PROFIT/LOSS)
13487
+ * - Symbol
13488
+ * - Signal ID
13489
+ * - Position (LONG/SHORT)
13490
+ * - Level % (+10%, -20%, etc)
13491
+ * - Current Price
13492
+ * - Timestamp (ISO 8601)
13493
+ * - Mode (Backtest/Live)
13494
+ *
13495
+ * Also includes summary statistics at the end.
13496
+ *
13497
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13498
+ * @returns Promise resolving to markdown formatted report string
13499
+ *
13500
+ * @example
13501
+ * ```typescript
13502
+ * const markdown = await Partial.getReport("BTCUSDT");
13503
+ * console.log(markdown);
13504
+ *
13505
+ * // Output:
13506
+ * // # Partial Profit/Loss Report: BTCUSDT
13507
+ * //
13508
+ * // | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
13509
+ * // | --- | --- | --- | --- | --- | --- | --- | --- |
13510
+ * // | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
13511
+ * // | LOSS | BTCUSDT | abc123 | LONG | -10% | 49000.00000000 USD | 2024-01-15T11:00:00.000Z | Backtest |
13512
+ * //
13513
+ * // **Total events:** 2
13514
+ * // **Profit events:** 1
13515
+ * // **Loss events:** 1
13516
+ * ```
13517
+ */
13518
+ this.getReport = async (symbol) => {
13519
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol });
13520
+ return await backtest$1.partialMarkdownService.getReport(symbol);
13521
+ };
13522
+ /**
13523
+ * Generates and saves markdown report to file.
13524
+ *
13525
+ * Creates directory if it doesn't exist.
13526
+ * Filename format: {symbol}.md (e.g., "BTCUSDT.md")
13527
+ *
13528
+ * Delegates to PartialMarkdownService.dump() which:
13529
+ * 1. Generates markdown report via getReport()
13530
+ * 2. Creates output directory (recursive mkdir)
13531
+ * 3. Writes file with UTF-8 encoding
13532
+ * 4. Logs success/failure to console
13533
+ *
13534
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13535
+ * @param path - Output directory path (default: "./dump/partial")
13536
+ * @returns Promise that resolves when file is written
13537
+ *
13538
+ * @example
13539
+ * ```typescript
13540
+ * // Save to default path: ./dump/partial/BTCUSDT.md
13541
+ * await Partial.dump("BTCUSDT");
13542
+ *
13543
+ * // Save to custom path: ./reports/partial/BTCUSDT.md
13544
+ * await Partial.dump("BTCUSDT", "./reports/partial");
13545
+ *
13546
+ * // After multiple symbols backtested, export all reports
13547
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
13548
+ * await Partial.dump(symbol, "./backtest-results");
13549
+ * }
13550
+ * ```
13551
+ */
13552
+ this.dump = async (symbol, path) => {
13553
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, path });
13554
+ await backtest$1.partialMarkdownService.dump(symbol, path);
13555
+ };
13556
+ }
13557
+ }
13558
+ /**
13559
+ * Global singleton instance of PartialUtils.
13560
+ * Provides static-like access to partial profit/loss reporting methods.
13561
+ *
13562
+ * @example
13563
+ * ```typescript
13564
+ * import { Partial } from "backtest-kit";
13565
+ *
13566
+ * // Usage same as PartialUtils methods
13567
+ * const stats = await Partial.getData("BTCUSDT");
13568
+ * const report = await Partial.getReport("BTCUSDT");
13569
+ * await Partial.dump("BTCUSDT");
13570
+ * ```
13571
+ */
13572
+ const Partial = new PartialUtils();
13573
+
10367
13574
  exports.Backtest = Backtest;
10368
13575
  exports.ExecutionContextService = ExecutionContextService;
10369
13576
  exports.Heat = Heat;
10370
13577
  exports.Live = Live;
10371
13578
  exports.MethodContextService = MethodContextService;
13579
+ exports.Optimizer = Optimizer;
13580
+ exports.Partial = Partial;
10372
13581
  exports.Performance = Performance;
10373
13582
  exports.PersistBase = PersistBase;
13583
+ exports.PersistPartialAdapter = PersistPartialAdapter;
10374
13584
  exports.PersistRiskAdapter = PersistRiskAdapter;
13585
+ exports.PersistScheduleAdapter = PersistScheduleAdapter;
10375
13586
  exports.PersistSignalAdapter = PersistSignalAdapter;
10376
13587
  exports.PositionSize = PositionSize;
10377
13588
  exports.Schedule = Schedule;
10378
13589
  exports.Walker = Walker;
10379
13590
  exports.addExchange = addExchange;
10380
13591
  exports.addFrame = addFrame;
13592
+ exports.addOptimizer = addOptimizer;
10381
13593
  exports.addRisk = addRisk;
10382
13594
  exports.addSizing = addSizing;
10383
13595
  exports.addStrategy = addStrategy;
@@ -10392,10 +13604,12 @@ exports.getMode = getMode;
10392
13604
  exports.lib = backtest;
10393
13605
  exports.listExchanges = listExchanges;
10394
13606
  exports.listFrames = listFrames;
13607
+ exports.listOptimizers = listOptimizers;
10395
13608
  exports.listRisks = listRisks;
10396
13609
  exports.listSizings = listSizings;
10397
13610
  exports.listStrategies = listStrategies;
10398
13611
  exports.listWalkers = listWalkers;
13612
+ exports.listenBacktestProgress = listenBacktestProgress;
10399
13613
  exports.listenDoneBacktest = listenDoneBacktest;
10400
13614
  exports.listenDoneBacktestOnce = listenDoneBacktestOnce;
10401
13615
  exports.listenDoneLive = listenDoneLive;
@@ -10403,8 +13617,11 @@ exports.listenDoneLiveOnce = listenDoneLiveOnce;
10403
13617
  exports.listenDoneWalker = listenDoneWalker;
10404
13618
  exports.listenDoneWalkerOnce = listenDoneWalkerOnce;
10405
13619
  exports.listenError = listenError;
13620
+ exports.listenPartialLoss = listenPartialLoss;
13621
+ exports.listenPartialLossOnce = listenPartialLossOnce;
13622
+ exports.listenPartialProfit = listenPartialProfit;
13623
+ exports.listenPartialProfitOnce = listenPartialProfitOnce;
10406
13624
  exports.listenPerformance = listenPerformance;
10407
- exports.listenProgress = listenProgress;
10408
13625
  exports.listenSignal = listenSignal;
10409
13626
  exports.listenSignalBacktest = listenSignalBacktest;
10410
13627
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
@@ -10415,5 +13632,6 @@ exports.listenValidation = listenValidation;
10415
13632
  exports.listenWalker = listenWalker;
10416
13633
  exports.listenWalkerComplete = listenWalkerComplete;
10417
13634
  exports.listenWalkerOnce = listenWalkerOnce;
13635
+ exports.listenWalkerProgress = listenWalkerProgress;
10418
13636
  exports.setConfig = setConfig;
10419
13637
  exports.setLogger = setLogger;