backtest-kit 1.3.2 → 1.4.1

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 +3349 -183
  3. package/build/index.mjs +3339 -184
  4. package/package.json +3 -2
  5. package/types.d.ts +2260 -122
package/build/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, queued } from 'functools-kit';
3
+ import { errorData, getErrorMessage, sleep, memoize, makeExtendable, singleshot, not, trycatch, retry, Subject, randomString, ToolRegistry, isObject, resolveDocuments, str, iterateDocuments, distinctDocuments, queued } from 'functools-kit';
4
4
  import fs, { mkdir, writeFile } from 'fs/promises';
5
5
  import path, { join } from 'path';
6
6
  import crypto from 'crypto';
@@ -116,6 +116,8 @@ const connectionServices$1 = {
116
116
  frameConnectionService: Symbol('frameConnectionService'),
117
117
  sizingConnectionService: Symbol('sizingConnectionService'),
118
118
  riskConnectionService: Symbol('riskConnectionService'),
119
+ optimizerConnectionService: Symbol('optimizerConnectionService'),
120
+ partialConnectionService: Symbol('partialConnectionService'),
119
121
  };
120
122
  const schemaServices$1 = {
121
123
  exchangeSchemaService: Symbol('exchangeSchemaService'),
@@ -124,6 +126,7 @@ const schemaServices$1 = {
124
126
  walkerSchemaService: Symbol('walkerSchemaService'),
125
127
  sizingSchemaService: Symbol('sizingSchemaService'),
126
128
  riskSchemaService: Symbol('riskSchemaService'),
129
+ optimizerSchemaService: Symbol('optimizerSchemaService'),
127
130
  };
128
131
  const globalServices$1 = {
129
132
  exchangeGlobalService: Symbol('exchangeGlobalService'),
@@ -131,6 +134,8 @@ const globalServices$1 = {
131
134
  frameGlobalService: Symbol('frameGlobalService'),
132
135
  sizingGlobalService: Symbol('sizingGlobalService'),
133
136
  riskGlobalService: Symbol('riskGlobalService'),
137
+ optimizerGlobalService: Symbol('optimizerGlobalService'),
138
+ partialGlobalService: Symbol('partialGlobalService'),
134
139
  };
135
140
  const commandServices$1 = {
136
141
  liveCommandService: Symbol('liveCommandService'),
@@ -154,6 +159,7 @@ const markdownServices$1 = {
154
159
  performanceMarkdownService: Symbol('performanceMarkdownService'),
155
160
  walkerMarkdownService: Symbol('walkerMarkdownService'),
156
161
  heatMarkdownService: Symbol('heatMarkdownService'),
162
+ partialMarkdownService: Symbol('partialMarkdownService'),
157
163
  };
158
164
  const validationServices$1 = {
159
165
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -162,6 +168,10 @@ const validationServices$1 = {
162
168
  walkerValidationService: Symbol('walkerValidationService'),
163
169
  sizingValidationService: Symbol('sizingValidationService'),
164
170
  riskValidationService: Symbol('riskValidationService'),
171
+ optimizerValidationService: Symbol('optimizerValidationService'),
172
+ };
173
+ const templateServices$1 = {
174
+ optimizerTemplateService: Symbol('optimizerTemplateService'),
165
175
  };
166
176
  const TYPES = {
167
177
  ...baseServices$1,
@@ -174,6 +184,7 @@ const TYPES = {
174
184
  ...logicPublicServices$1,
175
185
  ...markdownServices$1,
176
186
  ...validationServices$1,
187
+ ...templateServices$1,
177
188
  };
178
189
 
179
190
  /**
@@ -875,6 +886,12 @@ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
875
886
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
876
887
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
877
888
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
889
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER = "PersistScheduleUtils.usePersistScheduleAdapter";
890
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA = "PersistScheduleUtils.readScheduleData";
891
+ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writeScheduleData";
892
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
893
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
894
+ const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
878
895
  const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
879
896
  const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
880
897
  const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
@@ -940,7 +957,7 @@ const PersistBase = makeExtendable(class {
940
957
  * Creates new persistence instance.
941
958
  *
942
959
  * @param entityName - Unique entity type identifier
943
- * @param baseDir - Base directory for all entities (default: ./logs/data)
960
+ * @param baseDir - Base directory for all entities (default: ./dump/data)
944
961
  */
945
962
  constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
946
963
  this.entityName = entityName;
@@ -1199,7 +1216,7 @@ class PersistSignalUtils {
1199
1216
  this.PersistSignalFactory = PersistBase;
1200
1217
  this.getSignalStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
1201
1218
  strategyName,
1202
- `./logs/data/signal/`,
1219
+ `./dump/data/signal/`,
1203
1220
  ]));
1204
1221
  /**
1205
1222
  * Reads persisted signal data for a strategy and symbol.
@@ -1295,7 +1312,7 @@ class PersistRiskUtils {
1295
1312
  this.PersistRiskFactory = PersistBase;
1296
1313
  this.getRiskStorage = memoize(([riskName]) => `${riskName}`, (riskName) => Reflect.construct(this.PersistRiskFactory, [
1297
1314
  riskName,
1298
- `./logs/data/risk/`,
1315
+ `./dump/data/risk/`,
1299
1316
  ]));
1300
1317
  /**
1301
1318
  * Reads persisted active positions for a risk profile.
@@ -1372,6 +1389,192 @@ class PersistRiskUtils {
1372
1389
  * ```
1373
1390
  */
1374
1391
  const PersistRiskAdapter = new PersistRiskUtils();
1392
+ /**
1393
+ * Utility class for managing scheduled signal persistence.
1394
+ *
1395
+ * Features:
1396
+ * - Memoized storage instances per strategy
1397
+ * - Custom adapter support
1398
+ * - Atomic read/write operations for scheduled signals
1399
+ * - Crash-safe scheduled signal state management
1400
+ *
1401
+ * Used by ClientStrategy for live mode persistence of scheduled signals (_scheduledSignal).
1402
+ */
1403
+ class PersistScheduleUtils {
1404
+ constructor() {
1405
+ this.PersistScheduleFactory = PersistBase;
1406
+ this.getScheduleStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistScheduleFactory, [
1407
+ strategyName,
1408
+ `./dump/data/schedule/`,
1409
+ ]));
1410
+ /**
1411
+ * Reads persisted scheduled signal data for a strategy and symbol.
1412
+ *
1413
+ * Called by ClientStrategy.waitForInit() to restore scheduled signal state.
1414
+ * Returns null if no scheduled signal exists.
1415
+ *
1416
+ * @param strategyName - Strategy identifier
1417
+ * @param symbol - Trading pair symbol
1418
+ * @returns Promise resolving to scheduled signal or null
1419
+ */
1420
+ this.readScheduleData = async (strategyName, symbol) => {
1421
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1422
+ const isInitial = !this.getScheduleStorage.has(strategyName);
1423
+ const stateStorage = this.getScheduleStorage(strategyName);
1424
+ await stateStorage.waitForInit(isInitial);
1425
+ if (await stateStorage.hasValue(symbol)) {
1426
+ return await stateStorage.readValue(symbol);
1427
+ }
1428
+ return null;
1429
+ };
1430
+ /**
1431
+ * Writes scheduled signal data to disk with atomic file writes.
1432
+ *
1433
+ * Called by ClientStrategy.setScheduledSignal() to persist state.
1434
+ * Uses atomic writes to prevent corruption on crashes.
1435
+ *
1436
+ * @param scheduledSignalRow - Scheduled signal data (null to clear)
1437
+ * @param strategyName - Strategy identifier
1438
+ * @param symbol - Trading pair symbol
1439
+ * @returns Promise that resolves when write is complete
1440
+ */
1441
+ this.writeScheduleData = async (scheduledSignalRow, strategyName, symbol) => {
1442
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1443
+ const isInitial = !this.getScheduleStorage.has(strategyName);
1444
+ const stateStorage = this.getScheduleStorage(strategyName);
1445
+ await stateStorage.waitForInit(isInitial);
1446
+ await stateStorage.writeValue(symbol, scheduledSignalRow);
1447
+ };
1448
+ }
1449
+ /**
1450
+ * Registers a custom persistence adapter.
1451
+ *
1452
+ * @param Ctor - Custom PersistBase constructor
1453
+ *
1454
+ * @example
1455
+ * ```typescript
1456
+ * class RedisPersist extends PersistBase {
1457
+ * async readValue(id) { return JSON.parse(await redis.get(id)); }
1458
+ * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1459
+ * }
1460
+ * PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
1461
+ * ```
1462
+ */
1463
+ usePersistScheduleAdapter(Ctor) {
1464
+ backtest$1.loggerService.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1465
+ this.PersistScheduleFactory = Ctor;
1466
+ }
1467
+ }
1468
+ /**
1469
+ * Global singleton instance of PersistScheduleUtils.
1470
+ * Used by ClientStrategy for scheduled signal persistence.
1471
+ *
1472
+ * @example
1473
+ * ```typescript
1474
+ * // Custom adapter
1475
+ * PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
1476
+ *
1477
+ * // Read scheduled signal
1478
+ * const scheduled = await PersistScheduleAdapter.readScheduleData("my-strategy", "BTCUSDT");
1479
+ *
1480
+ * // Write scheduled signal
1481
+ * await PersistScheduleAdapter.writeScheduleData(scheduled, "my-strategy", "BTCUSDT");
1482
+ * ```
1483
+ */
1484
+ const PersistScheduleAdapter = new PersistScheduleUtils();
1485
+ /**
1486
+ * Utility class for managing partial profit/loss levels persistence.
1487
+ *
1488
+ * Features:
1489
+ * - Memoized storage instances per symbol
1490
+ * - Custom adapter support
1491
+ * - Atomic read/write operations for partial data
1492
+ * - Crash-safe partial state management
1493
+ *
1494
+ * Used by ClientPartial for live mode persistence of profit/loss levels.
1495
+ */
1496
+ class PersistPartialUtils {
1497
+ constructor() {
1498
+ this.PersistPartialFactory = PersistBase;
1499
+ this.getPartialStorage = memoize(([symbol]) => `${symbol}`, (symbol) => Reflect.construct(this.PersistPartialFactory, [
1500
+ symbol,
1501
+ `./dump/data/partial/`,
1502
+ ]));
1503
+ /**
1504
+ * Reads persisted partial data for a symbol.
1505
+ *
1506
+ * Called by ClientPartial.waitForInit() to restore state.
1507
+ * Returns empty object if no partial data exists.
1508
+ *
1509
+ * @param symbol - Trading pair symbol
1510
+ * @returns Promise resolving to partial data record
1511
+ */
1512
+ this.readPartialData = async (symbol) => {
1513
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1514
+ const isInitial = !this.getPartialStorage.has(symbol);
1515
+ const stateStorage = this.getPartialStorage(symbol);
1516
+ await stateStorage.waitForInit(isInitial);
1517
+ const PARTIAL_STORAGE_KEY = "levels";
1518
+ if (await stateStorage.hasValue(PARTIAL_STORAGE_KEY)) {
1519
+ return await stateStorage.readValue(PARTIAL_STORAGE_KEY);
1520
+ }
1521
+ return {};
1522
+ };
1523
+ /**
1524
+ * Writes partial data to disk with atomic file writes.
1525
+ *
1526
+ * Called by ClientPartial after profit/loss level changes to persist state.
1527
+ * Uses atomic writes to prevent corruption on crashes.
1528
+ *
1529
+ * @param partialData - Record of signal IDs to partial data
1530
+ * @param symbol - Trading pair symbol
1531
+ * @returns Promise that resolves when write is complete
1532
+ */
1533
+ this.writePartialData = async (partialData, symbol) => {
1534
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1535
+ const isInitial = !this.getPartialStorage.has(symbol);
1536
+ const stateStorage = this.getPartialStorage(symbol);
1537
+ await stateStorage.waitForInit(isInitial);
1538
+ const PARTIAL_STORAGE_KEY = "levels";
1539
+ await stateStorage.writeValue(PARTIAL_STORAGE_KEY, partialData);
1540
+ };
1541
+ }
1542
+ /**
1543
+ * Registers a custom persistence adapter.
1544
+ *
1545
+ * @param Ctor - Custom PersistBase constructor
1546
+ *
1547
+ * @example
1548
+ * ```typescript
1549
+ * class RedisPersist extends PersistBase {
1550
+ * async readValue(id) { return JSON.parse(await redis.get(id)); }
1551
+ * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1552
+ * }
1553
+ * PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
1554
+ * ```
1555
+ */
1556
+ usePersistPartialAdapter(Ctor) {
1557
+ backtest$1.loggerService.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1558
+ this.PersistPartialFactory = Ctor;
1559
+ }
1560
+ }
1561
+ /**
1562
+ * Global singleton instance of PersistPartialUtils.
1563
+ * Used by ClientPartial for partial profit/loss levels persistence.
1564
+ *
1565
+ * @example
1566
+ * ```typescript
1567
+ * // Custom adapter
1568
+ * PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
1569
+ *
1570
+ * // Read partial data
1571
+ * const partialData = await PersistPartialAdapter.readPartialData("BTCUSDT");
1572
+ *
1573
+ * // Write partial data
1574
+ * await PersistPartialAdapter.writePartialData(partialData, "BTCUSDT");
1575
+ * ```
1576
+ */
1577
+ const PersistPartialAdapter = new PersistPartialUtils();
1375
1578
 
1376
1579
  /**
1377
1580
  * Global signal emitter for all trading events (live + backtest).
@@ -1412,7 +1615,12 @@ const doneWalkerSubject = new Subject();
1412
1615
  * Progress emitter for backtest execution progress.
1413
1616
  * Emits progress updates during backtest execution.
1414
1617
  */
1415
- const progressEmitter = new Subject();
1618
+ const progressBacktestEmitter = new Subject();
1619
+ /**
1620
+ * Progress emitter for walker execution progress.
1621
+ * Emits progress updates during walker execution.
1622
+ */
1623
+ const progressWalkerEmitter = new Subject();
1416
1624
  /**
1417
1625
  * Performance emitter for execution metrics.
1418
1626
  * Emits performance metrics for profiling and bottleneck detection.
@@ -1438,6 +1646,16 @@ const walkerStopSubject = new Subject();
1438
1646
  * Emits when risk validation functions throw errors during signal checking.
1439
1647
  */
1440
1648
  const validationSubject = new Subject();
1649
+ /**
1650
+ * Partial profit emitter for profit level milestones.
1651
+ * Emits when a signal reaches a profit level (10%, 20%, 30%, etc).
1652
+ */
1653
+ const partialProfitSubject = new Subject();
1654
+ /**
1655
+ * Partial loss emitter for loss level milestones.
1656
+ * Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
1657
+ */
1658
+ const partialLossSubject = new Subject();
1441
1659
 
1442
1660
  var emitters = /*#__PURE__*/Object.freeze({
1443
1661
  __proto__: null,
@@ -1445,8 +1663,11 @@ var emitters = /*#__PURE__*/Object.freeze({
1445
1663
  doneLiveSubject: doneLiveSubject,
1446
1664
  doneWalkerSubject: doneWalkerSubject,
1447
1665
  errorEmitter: errorEmitter,
1666
+ partialLossSubject: partialLossSubject,
1667
+ partialProfitSubject: partialProfitSubject,
1448
1668
  performanceEmitter: performanceEmitter,
1449
- progressEmitter: progressEmitter,
1669
+ progressBacktestEmitter: progressBacktestEmitter,
1670
+ progressWalkerEmitter: progressWalkerEmitter,
1450
1671
  signalBacktestEmitter: signalBacktestEmitter,
1451
1672
  signalEmitter: signalEmitter,
1452
1673
  signalLiveEmitter: signalLiveEmitter,
@@ -1466,6 +1687,25 @@ const INTERVAL_MINUTES$1 = {
1466
1687
  };
1467
1688
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1468
1689
  const errors = [];
1690
+ // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
1691
+ if (signal.id === undefined || signal.id === null || signal.id === '') {
1692
+ errors.push('id is required and must be a non-empty string');
1693
+ }
1694
+ if (signal.exchangeName === undefined || signal.exchangeName === null || signal.exchangeName === '') {
1695
+ errors.push('exchangeName is required');
1696
+ }
1697
+ if (signal.strategyName === undefined || signal.strategyName === null || signal.strategyName === '') {
1698
+ errors.push('strategyName is required');
1699
+ }
1700
+ if (signal.symbol === undefined || signal.symbol === null || signal.symbol === '') {
1701
+ errors.push('symbol is required and must be a non-empty string');
1702
+ }
1703
+ if (signal._isScheduled === undefined || signal._isScheduled === null) {
1704
+ errors.push('_isScheduled is required');
1705
+ }
1706
+ if (signal.position === undefined || signal.position === null) {
1707
+ errors.push('position is required and must be "long" or "short"');
1708
+ }
1469
1709
  // ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
1470
1710
  if (!isFinite(currentPrice)) {
1471
1711
  errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
@@ -1715,26 +1955,42 @@ const GET_AVG_PRICE_FN = (candles) => {
1715
1955
  ? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
1716
1956
  : sumPriceVolume / totalVolume;
1717
1957
  };
1718
- const WAIT_FOR_INIT_FN$1 = async (self) => {
1958
+ const WAIT_FOR_INIT_FN$2 = async (self) => {
1719
1959
  self.params.logger.debug("ClientStrategy waitForInit");
1720
1960
  if (self.params.execution.context.backtest) {
1721
1961
  return;
1722
1962
  }
1963
+ // Restore pending signal
1723
1964
  const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
1724
- if (!pendingSignal) {
1725
- return;
1726
- }
1727
- if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
1728
- return;
1729
- }
1730
- if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
1731
- return;
1965
+ if (pendingSignal) {
1966
+ if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
1967
+ return;
1968
+ }
1969
+ if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
1970
+ return;
1971
+ }
1972
+ self._pendingSignal = pendingSignal;
1973
+ // Call onActive callback for restored signal
1974
+ if (self.params.callbacks?.onActive) {
1975
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1976
+ self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
1977
+ }
1732
1978
  }
1733
- self._pendingSignal = pendingSignal;
1734
- // Call onActive callback for restored signal
1735
- if (self.params.callbacks?.onActive) {
1736
- const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1737
- self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
1979
+ // Restore scheduled signal
1980
+ const scheduledSignal = await PersistScheduleAdapter.readScheduleData(self.params.strategyName, self.params.execution.context.symbol);
1981
+ if (scheduledSignal) {
1982
+ if (scheduledSignal.exchangeName !== self.params.method.context.exchangeName) {
1983
+ return;
1984
+ }
1985
+ if (scheduledSignal.strategyName !== self.params.method.context.strategyName) {
1986
+ return;
1987
+ }
1988
+ self._scheduledSignal = scheduledSignal;
1989
+ // Call onSchedule callback for restored scheduled signal
1990
+ if (self.params.callbacks?.onSchedule) {
1991
+ const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
1992
+ self.params.callbacks.onSchedule(self.params.execution.context.symbol, scheduledSignal, currentPrice, self.params.execution.context.backtest);
1993
+ }
1738
1994
  }
1739
1995
  };
1740
1996
  const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
@@ -1751,7 +2007,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
1751
2007
  elapsedMinutes: Math.floor(elapsedTime / 60000),
1752
2008
  maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
1753
2009
  });
1754
- self._scheduledSignal = null;
2010
+ await self.setScheduledSignal(null);
1755
2011
  if (self.params.callbacks?.onCancel) {
1756
2012
  self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
1757
2013
  }
@@ -1806,7 +2062,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
1806
2062
  averagePrice: currentPrice,
1807
2063
  priceStopLoss: scheduled.priceStopLoss,
1808
2064
  });
1809
- self._scheduledSignal = null;
2065
+ await self.setScheduledSignal(null);
1810
2066
  const result = {
1811
2067
  action: "idle",
1812
2068
  signal: null,
@@ -1827,7 +2083,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
1827
2083
  symbol: self.params.execution.context.symbol,
1828
2084
  signalId: scheduled.id,
1829
2085
  });
1830
- self._scheduledSignal = null;
2086
+ await self.setScheduledSignal(null);
1831
2087
  return null;
1832
2088
  }
1833
2089
  // В LIVE режиме activationTimestamp - это текущее время при tick()
@@ -1855,10 +2111,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
1855
2111
  symbol: self.params.execution.context.symbol,
1856
2112
  signalId: scheduled.id,
1857
2113
  });
1858
- self._scheduledSignal = null;
2114
+ await self.setScheduledSignal(null);
1859
2115
  return null;
1860
2116
  }
1861
- self._scheduledSignal = null;
2117
+ await self.setScheduledSignal(null);
1862
2118
  // КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
1863
2119
  const activatedSignal = {
1864
2120
  ...scheduled,
@@ -1996,6 +2252,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
1996
2252
  if (self.params.callbacks?.onClose) {
1997
2253
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
1998
2254
  }
2255
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2256
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
1999
2257
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2000
2258
  strategyName: self.params.method.context.strategyName,
2001
2259
  riskName: self.params.riskName,
@@ -2018,6 +2276,34 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2018
2276
  return result;
2019
2277
  };
2020
2278
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2279
+ // Calculate revenue percentage for partial fill/loss callbacks
2280
+ {
2281
+ let revenuePercent = 0;
2282
+ if (signal.position === "long") {
2283
+ // For long: positive if current > open, negative if current < open
2284
+ revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
2285
+ }
2286
+ else if (signal.position === "short") {
2287
+ // For short: positive if current < open, negative if current > open
2288
+ revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
2289
+ }
2290
+ // Call onPartialProfit if revenue is positive (but not reached TP yet)
2291
+ if (revenuePercent > 0) {
2292
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2293
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2294
+ if (self.params.callbacks?.onPartialProfit) {
2295
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2296
+ }
2297
+ }
2298
+ // Call onPartialLoss if revenue is negative (but not hit SL yet)
2299
+ if (revenuePercent < 0) {
2300
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2301
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2302
+ if (self.params.callbacks?.onPartialLoss) {
2303
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2304
+ }
2305
+ }
2306
+ }
2021
2307
  const result = {
2022
2308
  action: "active",
2023
2309
  signal: signal,
@@ -2056,7 +2342,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
2056
2342
  averagePrice,
2057
2343
  priceStopLoss: scheduled.priceStopLoss,
2058
2344
  });
2059
- self._scheduledSignal = null;
2345
+ await self.setScheduledSignal(null);
2060
2346
  if (self.params.callbacks?.onCancel) {
2061
2347
  self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
2062
2348
  }
@@ -2081,7 +2367,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2081
2367
  symbol: self.params.execution.context.symbol,
2082
2368
  signalId: scheduled.id,
2083
2369
  });
2084
- self._scheduledSignal = null;
2370
+ await self.setScheduledSignal(null);
2085
2371
  return false;
2086
2372
  }
2087
2373
  // В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
@@ -2106,10 +2392,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
2106
2392
  symbol: self.params.execution.context.symbol,
2107
2393
  signalId: scheduled.id,
2108
2394
  });
2109
- self._scheduledSignal = null;
2395
+ await self.setScheduledSignal(null);
2110
2396
  return false;
2111
2397
  }
2112
- self._scheduledSignal = null;
2398
+ await self.setScheduledSignal(null);
2113
2399
  // КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
2114
2400
  const activatedSignal = {
2115
2401
  ...scheduled,
@@ -2145,6 +2431,8 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2145
2431
  if (self.params.callbacks?.onClose) {
2146
2432
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2147
2433
  }
2434
+ // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2435
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
2148
2436
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2149
2437
  strategyName: self.params.method.context.strategyName,
2150
2438
  riskName: self.params.riskName,
@@ -2286,6 +2574,33 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2286
2574
  }
2287
2575
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2288
2576
  }
2577
+ // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
2578
+ // Calculate revenue percentage
2579
+ {
2580
+ let revenuePercent = 0;
2581
+ if (signal.position === "long") {
2582
+ revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
2583
+ }
2584
+ else if (signal.position === "short") {
2585
+ revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
2586
+ }
2587
+ // Call onPartialProfit if revenue is positive (but not reached TP yet)
2588
+ if (revenuePercent > 0) {
2589
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2590
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2591
+ if (self.params.callbacks?.onPartialProfit) {
2592
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2593
+ }
2594
+ }
2595
+ // Call onPartialLoss if revenue is negative (but not hit SL yet)
2596
+ if (revenuePercent < 0) {
2597
+ // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2598
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2599
+ if (self.params.callbacks?.onPartialLoss) {
2600
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2601
+ }
2602
+ }
2603
+ }
2289
2604
  }
2290
2605
  return null;
2291
2606
  };
@@ -2332,7 +2647,7 @@ class ClientStrategy {
2332
2647
  *
2333
2648
  * @returns Promise that resolves when initialization is complete
2334
2649
  */
2335
- this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
2650
+ this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$2(this));
2336
2651
  }
2337
2652
  /**
2338
2653
  * Updates pending signal and persists to disk in live mode.
@@ -2358,6 +2673,25 @@ class ClientStrategy {
2358
2673
  }
2359
2674
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
2360
2675
  }
2676
+ /**
2677
+ * Updates scheduled signal and persists to disk in live mode.
2678
+ *
2679
+ * Centralized method for all scheduled signal state changes.
2680
+ * Uses atomic file writes to prevent corruption.
2681
+ *
2682
+ * @param scheduledSignal - New scheduled signal state (null to clear)
2683
+ * @returns Promise that resolves when update is complete
2684
+ */
2685
+ async setScheduledSignal(scheduledSignal) {
2686
+ this.params.logger.debug("ClientStrategy setScheduledSignal", {
2687
+ scheduledSignal,
2688
+ });
2689
+ this._scheduledSignal = scheduledSignal;
2690
+ if (this.params.execution.context.backtest) {
2691
+ return;
2692
+ }
2693
+ await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, this.params.strategyName, this.params.execution.context.symbol);
2694
+ }
2361
2695
  /**
2362
2696
  * Retrieves the current pending signal.
2363
2697
  * If no signal is pending, returns null.
@@ -2432,7 +2766,7 @@ class ClientStrategy {
2432
2766
  const signal = await GET_SIGNAL_FN(this);
2433
2767
  if (signal) {
2434
2768
  if (signal._isScheduled === true) {
2435
- this._scheduledSignal = signal;
2769
+ await this.setScheduledSignal(signal);
2436
2770
  return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
2437
2771
  }
2438
2772
  await this.setPendingSignal(signal);
@@ -2608,7 +2942,7 @@ class ClientStrategy {
2608
2942
  this._isStopped = true;
2609
2943
  // Clear scheduled signal if exists
2610
2944
  if (this._scheduledSignal) {
2611
- this._scheduledSignal = null;
2945
+ await this.setScheduledSignal(null);
2612
2946
  }
2613
2947
  }
2614
2948
  }
@@ -2647,6 +2981,7 @@ class StrategyConnectionService {
2647
2981
  this.riskConnectionService = inject(TYPES.riskConnectionService);
2648
2982
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
2649
2983
  this.methodContextService = inject(TYPES.methodContextService);
2984
+ this.partialConnectionService = inject(TYPES.partialConnectionService);
2650
2985
  /**
2651
2986
  * Retrieves memoized ClientStrategy instance for given strategy name.
2652
2987
  *
@@ -2663,6 +2998,7 @@ class StrategyConnectionService {
2663
2998
  execution: this.executionContextService,
2664
2999
  method: this.methodContextService,
2665
3000
  logger: this.loggerService,
3001
+ partial: this.partialConnectionService,
2666
3002
  exchange: this.exchangeConnectionService,
2667
3003
  risk: riskName ? this.riskConnectionService.getRisk(riskName) : NOOP_RISK,
2668
3004
  riskName,
@@ -3149,7 +3485,7 @@ const DO_VALIDATION_FN = trycatch(async (validation, params) => {
3149
3485
  * Uses singleshot pattern to ensure it only runs once.
3150
3486
  * This function is exported for use in tests or other modules.
3151
3487
  */
3152
- const WAIT_FOR_INIT_FN = async (self) => {
3488
+ const WAIT_FOR_INIT_FN$1 = async (self) => {
3153
3489
  self.params.logger.debug("ClientRisk waitForInit");
3154
3490
  const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName);
3155
3491
  self._activePositions = new Map(persistedPositions);
@@ -3180,7 +3516,7 @@ class ClientRisk {
3180
3516
  * Uses singleshot pattern to ensure initialization happens exactly once.
3181
3517
  * Skips persistence in backtest mode.
3182
3518
  */
3183
- this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN(this));
3519
+ this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN$1(this));
3184
3520
  /**
3185
3521
  * Checks if a signal should be allowed based on risk limits.
3186
3522
  *
@@ -4355,7 +4691,7 @@ class BacktestLogicPrivateService {
4355
4691
  const when = timeframes[i];
4356
4692
  // Emit progress event if context is available
4357
4693
  {
4358
- await progressEmitter.next({
4694
+ await progressBacktestEmitter.next({
4359
4695
  exchangeName: this.methodContextService.context.exchangeName,
4360
4696
  strategyName: this.methodContextService.context.strategyName,
4361
4697
  symbol,
@@ -4489,7 +4825,7 @@ class BacktestLogicPrivateService {
4489
4825
  }
4490
4826
  // Emit final progress event (100%)
4491
4827
  {
4492
- await progressEmitter.next({
4828
+ await progressBacktestEmitter.next({
4493
4829
  exchangeName: this.methodContextService.context.exchangeName,
4494
4830
  strategyName: this.methodContextService.context.strategyName,
4495
4831
  symbol,
@@ -4730,6 +5066,16 @@ class WalkerLogicPrivateService {
4730
5066
  strategiesTested,
4731
5067
  totalStrategies: strategies.length,
4732
5068
  };
5069
+ // Emit progress event
5070
+ await progressWalkerEmitter.next({
5071
+ walkerName: context.walkerName,
5072
+ exchangeName: context.exchangeName,
5073
+ frameName: context.frameName,
5074
+ symbol,
5075
+ totalStrategies: strategies.length,
5076
+ processedStrategies: strategiesTested,
5077
+ progress: strategies.length > 0 ? strategiesTested / strategies.length : 0,
5078
+ });
4733
5079
  // Call onStrategyComplete callback if provided
4734
5080
  if (walkerSchema.callbacks?.onStrategyComplete) {
4735
5081
  walkerSchema.callbacks.onStrategyComplete(strategyName, symbol, stats, metricValue);
@@ -5075,7 +5421,7 @@ function isUnsafe$3(value) {
5075
5421
  }
5076
5422
  return false;
5077
5423
  }
5078
- const columns$3 = [
5424
+ const columns$4 = [
5079
5425
  {
5080
5426
  key: "signalId",
5081
5427
  label: "Signal ID",
@@ -5153,7 +5499,7 @@ const columns$3 = [
5153
5499
  * Storage class for accumulating closed signals per strategy.
5154
5500
  * Maintains a list of all closed signals and provides methods to generate reports.
5155
5501
  */
5156
- let ReportStorage$3 = class ReportStorage {
5502
+ let ReportStorage$4 = class ReportStorage {
5157
5503
  constructor() {
5158
5504
  /** Internal list of all closed signals for this strategy */
5159
5505
  this._signalList = [];
@@ -5243,9 +5589,9 @@ let ReportStorage$3 = class ReportStorage {
5243
5589
  if (stats.totalSignals === 0) {
5244
5590
  return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
5245
5591
  }
5246
- const header = columns$3.map((col) => col.label);
5247
- const separator = columns$3.map(() => "---");
5248
- const rows = this._signalList.map((closedSignal) => columns$3.map((col) => col.format(closedSignal)));
5592
+ const header = columns$4.map((col) => col.label);
5593
+ const separator = columns$4.map(() => "---");
5594
+ const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5249
5595
  const tableData = [header, separator, ...rows];
5250
5596
  const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5251
5597
  return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
@@ -5254,9 +5600,9 @@ let ReportStorage$3 = class ReportStorage {
5254
5600
  * Saves strategy report to disk.
5255
5601
  *
5256
5602
  * @param strategyName - Strategy name
5257
- * @param path - Directory path to save report (default: "./logs/backtest")
5603
+ * @param path - Directory path to save report (default: "./dump/backtest")
5258
5604
  */
5259
- async dump(strategyName, path = "./logs/backtest") {
5605
+ async dump(strategyName, path = "./dump/backtest") {
5260
5606
  const markdown = await this.getReport(strategyName);
5261
5607
  try {
5262
5608
  const dir = join(process.cwd(), path);
@@ -5306,7 +5652,7 @@ class BacktestMarkdownService {
5306
5652
  * Memoized function to get or create ReportStorage for a strategy.
5307
5653
  * Each strategy gets its own isolated storage instance.
5308
5654
  */
5309
- this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
5655
+ this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$4());
5310
5656
  /**
5311
5657
  * Processes tick events and accumulates closed signals.
5312
5658
  * Should be called from IStrategyCallbacks.onTick.
@@ -5384,20 +5730,20 @@ class BacktestMarkdownService {
5384
5730
  * Delegates to ReportStorage.dump().
5385
5731
  *
5386
5732
  * @param strategyName - Strategy name to save report for
5387
- * @param path - Directory path to save report (default: "./logs/backtest")
5733
+ * @param path - Directory path to save report (default: "./dump/backtest")
5388
5734
  *
5389
5735
  * @example
5390
5736
  * ```typescript
5391
5737
  * const service = new BacktestMarkdownService();
5392
5738
  *
5393
- * // Save to default path: ./logs/backtest/my-strategy.md
5739
+ * // Save to default path: ./dump/backtest/my-strategy.md
5394
5740
  * await service.dump("my-strategy");
5395
5741
  *
5396
5742
  * // Save to custom path: ./custom/path/my-strategy.md
5397
5743
  * await service.dump("my-strategy", "./custom/path");
5398
5744
  * ```
5399
5745
  */
5400
- this.dump = async (strategyName, path = "./logs/backtest") => {
5746
+ this.dump = async (strategyName, path = "./dump/backtest") => {
5401
5747
  this.loggerService.log("backtestMarkdownService dump", {
5402
5748
  strategyName,
5403
5749
  path,
@@ -5465,7 +5811,7 @@ function isUnsafe$2(value) {
5465
5811
  }
5466
5812
  return false;
5467
5813
  }
5468
- const columns$2 = [
5814
+ const columns$3 = [
5469
5815
  {
5470
5816
  key: "timestamp",
5471
5817
  label: "Timestamp",
@@ -5539,12 +5885,12 @@ const columns$2 = [
5539
5885
  },
5540
5886
  ];
5541
5887
  /** Maximum number of events to store in live trading reports */
5542
- const MAX_EVENTS$2 = 250;
5888
+ const MAX_EVENTS$3 = 250;
5543
5889
  /**
5544
5890
  * Storage class for accumulating all tick events per strategy.
5545
5891
  * Maintains a chronological list of all events (idle, opened, active, closed).
5546
5892
  */
5547
- let ReportStorage$2 = class ReportStorage {
5893
+ let ReportStorage$3 = class ReportStorage {
5548
5894
  constructor() {
5549
5895
  /** Internal list of all tick events for this strategy */
5550
5896
  this._eventList = [];
@@ -5572,7 +5918,7 @@ let ReportStorage$2 = class ReportStorage {
5572
5918
  }
5573
5919
  {
5574
5920
  this._eventList.push(newEvent);
5575
- if (this._eventList.length > MAX_EVENTS$2) {
5921
+ if (this._eventList.length > MAX_EVENTS$3) {
5576
5922
  this._eventList.shift();
5577
5923
  }
5578
5924
  }
@@ -5596,7 +5942,7 @@ let ReportStorage$2 = class ReportStorage {
5596
5942
  stopLoss: data.signal.priceStopLoss,
5597
5943
  });
5598
5944
  // Trim queue if exceeded MAX_EVENTS
5599
- if (this._eventList.length > MAX_EVENTS$2) {
5945
+ if (this._eventList.length > MAX_EVENTS$3) {
5600
5946
  this._eventList.shift();
5601
5947
  }
5602
5948
  }
@@ -5628,7 +5974,7 @@ let ReportStorage$2 = class ReportStorage {
5628
5974
  else {
5629
5975
  this._eventList.push(newEvent);
5630
5976
  // Trim queue if exceeded MAX_EVENTS
5631
- if (this._eventList.length > MAX_EVENTS$2) {
5977
+ if (this._eventList.length > MAX_EVENTS$3) {
5632
5978
  this._eventList.shift();
5633
5979
  }
5634
5980
  }
@@ -5666,7 +6012,7 @@ let ReportStorage$2 = class ReportStorage {
5666
6012
  else {
5667
6013
  this._eventList.push(newEvent);
5668
6014
  // Trim queue if exceeded MAX_EVENTS
5669
- if (this._eventList.length > MAX_EVENTS$2) {
6015
+ if (this._eventList.length > MAX_EVENTS$3) {
5670
6016
  this._eventList.shift();
5671
6017
  }
5672
6018
  }
@@ -5763,9 +6109,9 @@ let ReportStorage$2 = class ReportStorage {
5763
6109
  if (stats.totalEvents === 0) {
5764
6110
  return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
5765
6111
  }
5766
- const header = columns$2.map((col) => col.label);
5767
- const separator = columns$2.map(() => "---");
5768
- const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6112
+ const header = columns$3.map((col) => col.label);
6113
+ const separator = columns$3.map(() => "---");
6114
+ const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
5769
6115
  const tableData = [header, separator, ...rows];
5770
6116
  const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5771
6117
  return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
@@ -5774,9 +6120,9 @@ let ReportStorage$2 = class ReportStorage {
5774
6120
  * Saves strategy report to disk.
5775
6121
  *
5776
6122
  * @param strategyName - Strategy name
5777
- * @param path - Directory path to save report (default: "./logs/live")
6123
+ * @param path - Directory path to save report (default: "./dump/live")
5778
6124
  */
5779
- async dump(strategyName, path = "./logs/live") {
6125
+ async dump(strategyName, path = "./dump/live") {
5780
6126
  const markdown = await this.getReport(strategyName);
5781
6127
  try {
5782
6128
  const dir = join(process.cwd(), path);
@@ -5829,7 +6175,7 @@ class LiveMarkdownService {
5829
6175
  * Memoized function to get or create ReportStorage for a strategy.
5830
6176
  * Each strategy gets its own isolated storage instance.
5831
6177
  */
5832
- this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
6178
+ this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
5833
6179
  /**
5834
6180
  * Processes tick events and accumulates all event types.
5835
6181
  * Should be called from IStrategyCallbacks.onTick.
@@ -5917,20 +6263,20 @@ class LiveMarkdownService {
5917
6263
  * Delegates to ReportStorage.dump().
5918
6264
  *
5919
6265
  * @param strategyName - Strategy name to save report for
5920
- * @param path - Directory path to save report (default: "./logs/live")
6266
+ * @param path - Directory path to save report (default: "./dump/live")
5921
6267
  *
5922
6268
  * @example
5923
6269
  * ```typescript
5924
6270
  * const service = new LiveMarkdownService();
5925
6271
  *
5926
- * // Save to default path: ./logs/live/my-strategy.md
6272
+ * // Save to default path: ./dump/live/my-strategy.md
5927
6273
  * await service.dump("my-strategy");
5928
6274
  *
5929
6275
  * // Save to custom path: ./custom/path/my-strategy.md
5930
6276
  * await service.dump("my-strategy", "./custom/path");
5931
6277
  * ```
5932
6278
  */
5933
- this.dump = async (strategyName, path = "./logs/live") => {
6279
+ this.dump = async (strategyName, path = "./dump/live") => {
5934
6280
  this.loggerService.log("liveMarkdownService dump", {
5935
6281
  strategyName,
5936
6282
  path,
@@ -5980,7 +6326,7 @@ class LiveMarkdownService {
5980
6326
  }
5981
6327
  }
5982
6328
 
5983
- const columns$1 = [
6329
+ const columns$2 = [
5984
6330
  {
5985
6331
  key: "timestamp",
5986
6332
  label: "Timestamp",
@@ -6038,12 +6384,12 @@ const columns$1 = [
6038
6384
  },
6039
6385
  ];
6040
6386
  /** Maximum number of events to store in schedule reports */
6041
- const MAX_EVENTS$1 = 250;
6387
+ const MAX_EVENTS$2 = 250;
6042
6388
  /**
6043
6389
  * Storage class for accumulating scheduled signal events per strategy.
6044
6390
  * Maintains a chronological list of scheduled and cancelled events.
6045
6391
  */
6046
- let ReportStorage$1 = class ReportStorage {
6392
+ let ReportStorage$2 = class ReportStorage {
6047
6393
  constructor() {
6048
6394
  /** Internal list of all scheduled events for this strategy */
6049
6395
  this._eventList = [];
@@ -6067,7 +6413,7 @@ let ReportStorage$1 = class ReportStorage {
6067
6413
  stopLoss: data.signal.priceStopLoss,
6068
6414
  });
6069
6415
  // Trim queue if exceeded MAX_EVENTS
6070
- if (this._eventList.length > MAX_EVENTS$1) {
6416
+ if (this._eventList.length > MAX_EVENTS$2) {
6071
6417
  this._eventList.shift();
6072
6418
  }
6073
6419
  }
@@ -6103,7 +6449,7 @@ let ReportStorage$1 = class ReportStorage {
6103
6449
  else {
6104
6450
  this._eventList.push(newEvent);
6105
6451
  // Trim queue if exceeded MAX_EVENTS
6106
- if (this._eventList.length > MAX_EVENTS$1) {
6452
+ if (this._eventList.length > MAX_EVENTS$2) {
6107
6453
  this._eventList.shift();
6108
6454
  }
6109
6455
  }
@@ -6155,9 +6501,9 @@ let ReportStorage$1 = class ReportStorage {
6155
6501
  if (stats.totalEvents === 0) {
6156
6502
  return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
6157
6503
  }
6158
- const header = columns$1.map((col) => col.label);
6159
- const separator = columns$1.map(() => "---");
6160
- const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
6504
+ const header = columns$2.map((col) => col.label);
6505
+ const separator = columns$2.map(() => "---");
6506
+ const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6161
6507
  const tableData = [header, separator, ...rows];
6162
6508
  const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
6163
6509
  return 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`}`);
@@ -6166,9 +6512,9 @@ let ReportStorage$1 = class ReportStorage {
6166
6512
  * Saves strategy report to disk.
6167
6513
  *
6168
6514
  * @param strategyName - Strategy name
6169
- * @param path - Directory path to save report (default: "./logs/schedule")
6515
+ * @param path - Directory path to save report (default: "./dump/schedule")
6170
6516
  */
6171
- async dump(strategyName, path = "./logs/schedule") {
6517
+ async dump(strategyName, path = "./dump/schedule") {
6172
6518
  const markdown = await this.getReport(strategyName);
6173
6519
  try {
6174
6520
  const dir = join(process.cwd(), path);
@@ -6212,7 +6558,7 @@ class ScheduleMarkdownService {
6212
6558
  * Memoized function to get or create ReportStorage for a strategy.
6213
6559
  * Each strategy gets its own isolated storage instance.
6214
6560
  */
6215
- this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
6561
+ this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
6216
6562
  /**
6217
6563
  * Processes tick events and accumulates scheduled/cancelled events.
6218
6564
  * Should be called from signalLiveEmitter subscription.
@@ -6287,20 +6633,20 @@ class ScheduleMarkdownService {
6287
6633
  * Delegates to ReportStorage.dump().
6288
6634
  *
6289
6635
  * @param strategyName - Strategy name to save report for
6290
- * @param path - Directory path to save report (default: "./logs/schedule")
6636
+ * @param path - Directory path to save report (default: "./dump/schedule")
6291
6637
  *
6292
6638
  * @example
6293
6639
  * ```typescript
6294
6640
  * const service = new ScheduleMarkdownService();
6295
6641
  *
6296
- * // Save to default path: ./logs/schedule/my-strategy.md
6642
+ * // Save to default path: ./dump/schedule/my-strategy.md
6297
6643
  * await service.dump("my-strategy");
6298
6644
  *
6299
6645
  * // Save to custom path: ./custom/path/my-strategy.md
6300
6646
  * await service.dump("my-strategy", "./custom/path");
6301
6647
  * ```
6302
6648
  */
6303
- this.dump = async (strategyName, path = "./logs/schedule") => {
6649
+ this.dump = async (strategyName, path = "./dump/schedule") => {
6304
6650
  this.loggerService.log("scheduleMarkdownService dump", {
6305
6651
  strategyName,
6306
6652
  path,
@@ -6360,7 +6706,7 @@ function percentile(sortedArray, p) {
6360
6706
  return sortedArray[Math.max(0, index)];
6361
6707
  }
6362
6708
  /** Maximum number of performance events to store per strategy */
6363
- const MAX_EVENTS = 10000;
6709
+ const MAX_EVENTS$1 = 10000;
6364
6710
  /**
6365
6711
  * Storage class for accumulating performance metrics per strategy.
6366
6712
  * Maintains a list of all performance events and provides aggregated statistics.
@@ -6378,7 +6724,7 @@ class PerformanceStorage {
6378
6724
  addEvent(event) {
6379
6725
  this._events.push(event);
6380
6726
  // Trim queue if exceeded MAX_EVENTS (keep most recent)
6381
- if (this._events.length > MAX_EVENTS) {
6727
+ if (this._events.length > MAX_EVENTS$1) {
6382
6728
  this._events.shift();
6383
6729
  }
6384
6730
  }
@@ -6517,7 +6863,7 @@ class PerformanceStorage {
6517
6863
  * @param strategyName - Strategy name
6518
6864
  * @param path - Directory path to save report
6519
6865
  */
6520
- async dump(strategyName, path = "./logs/performance") {
6866
+ async dump(strategyName, path = "./dump/performance") {
6521
6867
  const markdown = await this.getReport(strategyName);
6522
6868
  try {
6523
6869
  const dir = join(process.cwd(), path);
@@ -6630,14 +6976,14 @@ class PerformanceMarkdownService {
6630
6976
  *
6631
6977
  * @example
6632
6978
  * ```typescript
6633
- * // Save to default path: ./logs/performance/my-strategy.md
6979
+ * // Save to default path: ./dump/performance/my-strategy.md
6634
6980
  * await performanceService.dump("my-strategy");
6635
6981
  *
6636
6982
  * // Save to custom path
6637
6983
  * await performanceService.dump("my-strategy", "./custom/path");
6638
6984
  * ```
6639
6985
  */
6640
- this.dump = async (strategyName, path = "./logs/performance") => {
6986
+ this.dump = async (strategyName, path = "./dump/performance") => {
6641
6987
  this.loggerService.log("performanceMarkdownService dump", {
6642
6988
  strategyName,
6643
6989
  path,
@@ -6699,7 +7045,7 @@ function formatMetric(value) {
6699
7045
  * Storage class for accumulating walker results.
6700
7046
  * Maintains a list of all strategy results and provides methods to generate reports.
6701
7047
  */
6702
- class ReportStorage {
7048
+ let ReportStorage$1 = class ReportStorage {
6703
7049
  constructor(walkerName) {
6704
7050
  this.walkerName = walkerName;
6705
7051
  /** Walker metadata (set from first addResult call) */
@@ -6767,9 +7113,9 @@ class ReportStorage {
6767
7113
  * @param symbol - Trading symbol
6768
7114
  * @param metric - Metric being optimized
6769
7115
  * @param context - Context with exchangeName and frameName
6770
- * @param path - Directory path to save report (default: "./logs/walker")
7116
+ * @param path - Directory path to save report (default: "./dump/walker")
6771
7117
  */
6772
- async dump(symbol, metric, context, path = "./logs/walker") {
7118
+ async dump(symbol, metric, context, path = "./dump/walker") {
6773
7119
  const markdown = await this.getReport(symbol, metric, context);
6774
7120
  try {
6775
7121
  const dir = join(process.cwd(), path);
@@ -6783,7 +7129,7 @@ class ReportStorage {
6783
7129
  console.error(`Failed to save walker report:`, error);
6784
7130
  }
6785
7131
  }
6786
- }
7132
+ };
6787
7133
  /**
6788
7134
  * Service for generating and saving walker markdown reports.
6789
7135
  *
@@ -6808,7 +7154,7 @@ class WalkerMarkdownService {
6808
7154
  * Memoized function to get or create ReportStorage for a walker.
6809
7155
  * Each walker gets its own isolated storage instance.
6810
7156
  */
6811
- this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage(walkerName));
7157
+ this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$1(walkerName));
6812
7158
  /**
6813
7159
  * Processes walker progress events and accumulates strategy results.
6814
7160
  * Should be called from walkerEmitter.
@@ -6891,20 +7237,20 @@ class WalkerMarkdownService {
6891
7237
  * @param symbol - Trading symbol
6892
7238
  * @param metric - Metric being optimized
6893
7239
  * @param context - Context with exchangeName and frameName
6894
- * @param path - Directory path to save report (default: "./logs/walker")
7240
+ * @param path - Directory path to save report (default: "./dump/walker")
6895
7241
  *
6896
7242
  * @example
6897
7243
  * ```typescript
6898
7244
  * const service = new WalkerMarkdownService();
6899
7245
  *
6900
- * // Save to default path: ./logs/walker/my-walker.md
7246
+ * // Save to default path: ./dump/walker/my-walker.md
6901
7247
  * await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" });
6902
7248
  *
6903
7249
  * // Save to custom path: ./custom/path/my-walker.md
6904
7250
  * await service.dump("my-walker", "BTCUSDT", "sharpeRatio", { exchangeName: "binance", frameName: "1d" }, "./custom/path");
6905
7251
  * ```
6906
7252
  */
6907
- this.dump = async (walkerName, symbol, metric, context, path = "./logs/walker") => {
7253
+ this.dump = async (walkerName, symbol, metric, context, path = "./dump/walker") => {
6908
7254
  this.loggerService.log("walkerMarkdownService dump", {
6909
7255
  walkerName,
6910
7256
  symbol,
@@ -6979,7 +7325,7 @@ function isUnsafe(value) {
6979
7325
  }
6980
7326
  return false;
6981
7327
  }
6982
- const columns = [
7328
+ const columns$1 = [
6983
7329
  {
6984
7330
  key: "symbol",
6985
7331
  label: "Symbol",
@@ -7275,9 +7621,9 @@ class HeatmapStorage {
7275
7621
  if (data.symbols.length === 0) {
7276
7622
  return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
7277
7623
  }
7278
- const header = columns.map((col) => col.label);
7279
- const separator = columns.map(() => "---");
7280
- const rows = data.symbols.map((row) => columns.map((col) => col.format(row)));
7624
+ const header = columns$1.map((col) => col.label);
7625
+ const separator = columns$1.map(() => "---");
7626
+ const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
7281
7627
  const tableData = [header, separator, ...rows];
7282
7628
  const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7283
7629
  return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
@@ -7286,9 +7632,9 @@ class HeatmapStorage {
7286
7632
  * Saves heatmap report to disk.
7287
7633
  *
7288
7634
  * @param strategyName - Strategy name for filename
7289
- * @param path - Directory path to save report (default: "./logs/heatmap")
7635
+ * @param path - Directory path to save report (default: "./dump/heatmap")
7290
7636
  */
7291
- async dump(strategyName, path = "./logs/heatmap") {
7637
+ async dump(strategyName, path = "./dump/heatmap") {
7292
7638
  const markdown = await this.getReport(strategyName);
7293
7639
  try {
7294
7640
  const dir = join(process.cwd(), path);
@@ -7419,20 +7765,20 @@ class HeatMarkdownService {
7419
7765
  * Default filename: {strategyName}.md
7420
7766
  *
7421
7767
  * @param strategyName - Strategy name to save heatmap report for
7422
- * @param path - Optional directory path to save report (default: "./logs/heatmap")
7768
+ * @param path - Optional directory path to save report (default: "./dump/heatmap")
7423
7769
  *
7424
7770
  * @example
7425
7771
  * ```typescript
7426
7772
  * const service = new HeatMarkdownService();
7427
7773
  *
7428
- * // Save to default path: ./logs/heatmap/my-strategy.md
7774
+ * // Save to default path: ./dump/heatmap/my-strategy.md
7429
7775
  * await service.dump("my-strategy");
7430
7776
  *
7431
7777
  * // Save to custom path: ./reports/my-strategy.md
7432
7778
  * await service.dump("my-strategy", "./reports");
7433
7779
  * ```
7434
7780
  */
7435
- this.dump = async (strategyName, path = "./logs/heatmap") => {
7781
+ this.dump = async (strategyName, path = "./dump/heatmap") => {
7436
7782
  this.loggerService.log(HEATMAP_METHOD_NAME_DUMP, {
7437
7783
  strategyName,
7438
7784
  path,
@@ -7864,47 +8210,2310 @@ class RiskValidationService {
7864
8210
  }
7865
8211
  }
7866
8212
 
7867
- {
7868
- provide(TYPES.loggerService, () => new LoggerService());
7869
- }
7870
- {
7871
- provide(TYPES.executionContextService, () => new ExecutionContextService());
7872
- provide(TYPES.methodContextService, () => new MethodContextService());
7873
- }
7874
- {
7875
- provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
7876
- provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
7877
- provide(TYPES.frameConnectionService, () => new FrameConnectionService());
7878
- provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
7879
- provide(TYPES.riskConnectionService, () => new RiskConnectionService());
7880
- }
7881
- {
7882
- provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
7883
- provide(TYPES.strategySchemaService, () => new StrategySchemaService());
7884
- provide(TYPES.frameSchemaService, () => new FrameSchemaService());
7885
- provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
7886
- provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
7887
- provide(TYPES.riskSchemaService, () => new RiskSchemaService());
7888
- }
7889
- {
7890
- provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
7891
- provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
7892
- provide(TYPES.frameGlobalService, () => new FrameGlobalService());
7893
- provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
7894
- provide(TYPES.riskGlobalService, () => new RiskGlobalService());
7895
- }
7896
- {
7897
- provide(TYPES.liveCommandService, () => new LiveCommandService());
7898
- provide(TYPES.backtestCommandService, () => new BacktestCommandService());
7899
- provide(TYPES.walkerCommandService, () => new WalkerCommandService());
7900
- }
7901
- {
7902
- provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
7903
- provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
7904
- provide(TYPES.walkerLogicPrivateService, () => new WalkerLogicPrivateService());
7905
- }
7906
- {
7907
- provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
8213
+ /**
8214
+ * Default template service for generating optimizer code snippets.
8215
+ * Implements all IOptimizerTemplate methods with Ollama LLM integration.
8216
+ *
8217
+ * Features:
8218
+ * - Multi-timeframe analysis (1m, 5m, 15m, 1h)
8219
+ * - JSON structured output for signals
8220
+ * - Debug logging to ./dump/strategy
8221
+ * - CCXT exchange integration
8222
+ * - Walker-based strategy comparison
8223
+ *
8224
+ * Can be partially overridden in optimizer schema configuration.
8225
+ */
8226
+ class OptimizerTemplateService {
8227
+ constructor() {
8228
+ this.loggerService = inject(TYPES.loggerService);
8229
+ /**
8230
+ * Generates the top banner with imports and constants.
8231
+ *
8232
+ * @param symbol - Trading pair symbol
8233
+ * @returns Shebang, imports, and WARN_KB constant
8234
+ */
8235
+ this.getTopBanner = async (symbol) => {
8236
+ this.loggerService.log("optimizerTemplateService getTopBanner", {
8237
+ symbol,
8238
+ });
8239
+ return [
8240
+ "#!/usr/bin/env node",
8241
+ "",
8242
+ `import { Ollama } from "ollama";`,
8243
+ `import ccxt from "ccxt";`,
8244
+ `import {`,
8245
+ ` addExchange,`,
8246
+ ` addStrategy,`,
8247
+ ` addFrame,`,
8248
+ ` addWalker,`,
8249
+ ` Walker,`,
8250
+ ` Backtest,`,
8251
+ ` getCandles,`,
8252
+ ` listenSignalBacktest,`,
8253
+ ` listenWalkerComplete,`,
8254
+ ` listenDoneBacktest,`,
8255
+ ` listenBacktestProgress,`,
8256
+ ` listenWalkerProgress,`,
8257
+ ` listenError,`,
8258
+ `} from "backtest-kit";`,
8259
+ `import { promises as fs } from "fs";`,
8260
+ `import { v4 as uuid } from "uuid";`,
8261
+ `import path from "path";`,
8262
+ ``,
8263
+ `const WARN_KB = 100;`
8264
+ ].join("\n");
8265
+ };
8266
+ /**
8267
+ * Generates default user message for LLM conversation.
8268
+ * Simple prompt to read and acknowledge data.
8269
+ *
8270
+ * @param symbol - Trading pair symbol
8271
+ * @param data - Fetched data array
8272
+ * @param name - Source name
8273
+ * @returns User message with JSON data
8274
+ */
8275
+ this.getUserMessage = async (symbol, data, name) => {
8276
+ this.loggerService.log("optimizerTemplateService getUserMessage", {
8277
+ symbol,
8278
+ data,
8279
+ name,
8280
+ });
8281
+ return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
8282
+ };
8283
+ /**
8284
+ * Generates default assistant message for LLM conversation.
8285
+ * Simple acknowledgment response.
8286
+ *
8287
+ * @param symbol - Trading pair symbol
8288
+ * @param data - Fetched data array
8289
+ * @param name - Source name
8290
+ * @returns Assistant acknowledgment message
8291
+ */
8292
+ this.getAssistantMessage = async (symbol, data, name) => {
8293
+ this.loggerService.log("optimizerTemplateService getAssistantMessage", {
8294
+ symbol,
8295
+ data,
8296
+ name,
8297
+ });
8298
+ return "ОК";
8299
+ };
8300
+ /**
8301
+ * Generates Walker configuration code.
8302
+ * Compares multiple strategies on test frame.
8303
+ *
8304
+ * @param walkerName - Unique walker identifier
8305
+ * @param exchangeName - Exchange to use for backtesting
8306
+ * @param frameName - Test frame name
8307
+ * @param strategies - Array of strategy names to compare
8308
+ * @returns Generated addWalker() call
8309
+ */
8310
+ this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
8311
+ this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
8312
+ walkerName,
8313
+ exchangeName,
8314
+ frameName,
8315
+ strategies,
8316
+ });
8317
+ // Escape special characters to prevent code injection
8318
+ const escapedWalkerName = String(walkerName)
8319
+ .replace(/\\/g, '\\\\')
8320
+ .replace(/"/g, '\\"');
8321
+ const escapedExchangeName = String(exchangeName)
8322
+ .replace(/\\/g, '\\\\')
8323
+ .replace(/"/g, '\\"');
8324
+ const escapedFrameName = String(frameName)
8325
+ .replace(/\\/g, '\\\\')
8326
+ .replace(/"/g, '\\"');
8327
+ const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
8328
+ return [
8329
+ `addWalker({`,
8330
+ ` walkerName: "${escapedWalkerName}",`,
8331
+ ` exchangeName: "${escapedExchangeName}",`,
8332
+ ` frameName: "${escapedFrameName}",`,
8333
+ ` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
8334
+ `});`
8335
+ ].join("\n");
8336
+ };
8337
+ /**
8338
+ * Generates Strategy configuration with LLM integration.
8339
+ * Includes multi-timeframe analysis and signal generation.
8340
+ *
8341
+ * @param strategyName - Unique strategy identifier
8342
+ * @param interval - Signal throttling interval (e.g., "5m")
8343
+ * @param prompt - Strategy logic from getPrompt()
8344
+ * @returns Generated addStrategy() call with getSignal() function
8345
+ */
8346
+ this.getStrategyTemplate = async (strategyName, interval, prompt) => {
8347
+ this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
8348
+ strategyName,
8349
+ interval,
8350
+ prompt,
8351
+ });
8352
+ // Escape special characters to prevent code injection
8353
+ const escapedStrategyName = String(strategyName)
8354
+ .replace(/\\/g, '\\\\')
8355
+ .replace(/"/g, '\\"');
8356
+ const escapedInterval = String(interval)
8357
+ .replace(/\\/g, '\\\\')
8358
+ .replace(/"/g, '\\"');
8359
+ const escapedPrompt = String(prompt)
8360
+ .replace(/\\/g, '\\\\')
8361
+ .replace(/`/g, '\\`')
8362
+ .replace(/\$/g, '\\$');
8363
+ return [
8364
+ `addStrategy({`,
8365
+ ` strategyName: "${escapedStrategyName}",`,
8366
+ ` interval: "${escapedInterval}",`,
8367
+ ` getSignal: async (symbol) => {`,
8368
+ ` const messages = [];`,
8369
+ ``,
8370
+ ` // Загружаем данные всех таймфреймов`,
8371
+ ` const microTermCandles = await getCandles(symbol, "1m", 30);`,
8372
+ ` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
8373
+ ` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
8374
+ ` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
8375
+ ``,
8376
+ ` function formatCandles(candles, timeframe) {`,
8377
+ ` return candles.map((c) =>`,
8378
+ ` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
8379
+ ` ).join("\\n");`,
8380
+ ` }`,
8381
+ ``,
8382
+ ` // Сообщение 1: Среднесрочный тренд`,
8383
+ ` messages.push(`,
8384
+ ` {`,
8385
+ ` role: "user",`,
8386
+ ` content: [`,
8387
+ ` \`\${symbol}\`,`,
8388
+ ` "Проанализируй свечи 1h:",`,
8389
+ ` "",`,
8390
+ ` formatCandles(mediumTermCandles, "1h")`,
8391
+ ` ].join("\\n"),`,
8392
+ ` },`,
8393
+ ` {`,
8394
+ ` role: "assistant",`,
8395
+ ` content: "Тренд 1h проанализирован",`,
8396
+ ` }`,
8397
+ ` );`,
8398
+ ``,
8399
+ ` // Сообщение 2: Краткосрочный тренд`,
8400
+ ` messages.push(`,
8401
+ ` {`,
8402
+ ` role: "user",`,
8403
+ ` content: [`,
8404
+ ` "Проанализируй свечи 15m:",`,
8405
+ ` "",`,
8406
+ ` formatCandles(shortTermCandles, "15m")`,
8407
+ ` ].join("\\n"),`,
8408
+ ` },`,
8409
+ ` {`,
8410
+ ` role: "assistant",`,
8411
+ ` content: "Тренд 15m проанализирован",`,
8412
+ ` }`,
8413
+ ` );`,
8414
+ ``,
8415
+ ` // Сообщение 3: Основной таймфрейм`,
8416
+ ` messages.push(`,
8417
+ ` {`,
8418
+ ` role: "user",`,
8419
+ ` content: [`,
8420
+ ` "Проанализируй свечи 5m:",`,
8421
+ ` "",`,
8422
+ ` formatCandles(mainTermCandles, "5m")`,
8423
+ ` ].join("\\n")`,
8424
+ ` },`,
8425
+ ` {`,
8426
+ ` role: "assistant",`,
8427
+ ` content: "Таймфрейм 5m проанализирован",`,
8428
+ ` }`,
8429
+ ` );`,
8430
+ ``,
8431
+ ` // Сообщение 4: Микро-структура`,
8432
+ ` messages.push(`,
8433
+ ` {`,
8434
+ ` role: "user",`,
8435
+ ` content: [`,
8436
+ ` "Проанализируй свечи 1m:",`,
8437
+ ` "",`,
8438
+ ` formatCandles(microTermCandles, "1m")`,
8439
+ ` ].join("\\n")`,
8440
+ ` },`,
8441
+ ` {`,
8442
+ ` role: "assistant",`,
8443
+ ` content: "Микроструктура 1m проанализирована",`,
8444
+ ` }`,
8445
+ ` );`,
8446
+ ``,
8447
+ ` // Сообщение 5: Запрос сигнала`,
8448
+ ` messages.push(`,
8449
+ ` {`,
8450
+ ` role: "user",`,
8451
+ ` content: [`,
8452
+ ` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
8453
+ ` "",`,
8454
+ ` \`${escapedPrompt}\`,`,
8455
+ ` "",`,
8456
+ ` "Если сигналы противоречивы или тренд слабый то position: wait"`,
8457
+ ` ].join("\\n"),`,
8458
+ ` }`,
8459
+ ` );`,
8460
+ ``,
8461
+ ` const resultId = uuid();`,
8462
+ ``,
8463
+ ` const result = await json(messages);`,
8464
+ ``,
8465
+ ` await dumpJson(resultId, messages, result);`,
8466
+ ``,
8467
+ ` return result;`,
8468
+ ` },`,
8469
+ `});`
8470
+ ].join("\n");
8471
+ };
8472
+ /**
8473
+ * Generates Exchange configuration code.
8474
+ * Uses CCXT Binance with standard formatters.
8475
+ *
8476
+ * @param symbol - Trading pair symbol (unused, for consistency)
8477
+ * @param exchangeName - Unique exchange identifier
8478
+ * @returns Generated addExchange() call with CCXT integration
8479
+ */
8480
+ this.getExchangeTemplate = async (symbol, exchangeName) => {
8481
+ this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
8482
+ exchangeName,
8483
+ symbol,
8484
+ });
8485
+ // Escape special characters to prevent code injection
8486
+ const escapedExchangeName = String(exchangeName)
8487
+ .replace(/\\/g, '\\\\')
8488
+ .replace(/"/g, '\\"');
8489
+ return [
8490
+ `addExchange({`,
8491
+ ` exchangeName: "${escapedExchangeName}",`,
8492
+ ` getCandles: async (symbol, interval, since, limit) => {`,
8493
+ ` const exchange = new ccxt.binance();`,
8494
+ ` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
8495
+ ` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
8496
+ ` timestamp, open, high, low, close, volume`,
8497
+ ` }));`,
8498
+ ` },`,
8499
+ ` formatPrice: async (symbol, price) => price.toFixed(2),`,
8500
+ ` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
8501
+ `});`
8502
+ ].join("\n");
8503
+ };
8504
+ /**
8505
+ * Generates Frame (timeframe) configuration code.
8506
+ *
8507
+ * @param symbol - Trading pair symbol (unused, for consistency)
8508
+ * @param frameName - Unique frame identifier
8509
+ * @param interval - Candle interval (e.g., "1m")
8510
+ * @param startDate - Frame start date
8511
+ * @param endDate - Frame end date
8512
+ * @returns Generated addFrame() call
8513
+ */
8514
+ this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
8515
+ this.loggerService.log("optimizerTemplateService getFrameTemplate", {
8516
+ symbol,
8517
+ frameName,
8518
+ interval,
8519
+ startDate,
8520
+ endDate,
8521
+ });
8522
+ // Escape special characters to prevent code injection
8523
+ const escapedFrameName = String(frameName)
8524
+ .replace(/\\/g, '\\\\')
8525
+ .replace(/"/g, '\\"');
8526
+ const escapedInterval = String(interval)
8527
+ .replace(/\\/g, '\\\\')
8528
+ .replace(/"/g, '\\"');
8529
+ return [
8530
+ `addFrame({`,
8531
+ ` frameName: "${escapedFrameName}",`,
8532
+ ` interval: "${escapedInterval}",`,
8533
+ ` startDate: new Date("${startDate.toISOString()}"),`,
8534
+ ` endDate: new Date("${endDate.toISOString()}"),`,
8535
+ `});`
8536
+ ].join("\n");
8537
+ };
8538
+ /**
8539
+ * Generates launcher code to run Walker with event listeners.
8540
+ * Includes progress tracking and completion handlers.
8541
+ *
8542
+ * @param symbol - Trading pair symbol
8543
+ * @param walkerName - Walker name to launch
8544
+ * @returns Generated Walker.background() call with listeners
8545
+ */
8546
+ this.getLauncherTemplate = async (symbol, walkerName) => {
8547
+ this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
8548
+ symbol,
8549
+ walkerName,
8550
+ });
8551
+ // Escape special characters to prevent code injection
8552
+ const escapedSymbol = String(symbol)
8553
+ .replace(/\\/g, '\\\\')
8554
+ .replace(/"/g, '\\"');
8555
+ const escapedWalkerName = String(walkerName)
8556
+ .replace(/\\/g, '\\\\')
8557
+ .replace(/"/g, '\\"');
8558
+ return [
8559
+ `Walker.background("${escapedSymbol}", {`,
8560
+ ` walkerName: "${escapedWalkerName}"`,
8561
+ `});`,
8562
+ ``,
8563
+ `listenSignalBacktest((event) => {`,
8564
+ ` console.log(event);`,
8565
+ `});`,
8566
+ ``,
8567
+ `listenBacktestProgress((event) => {`,
8568
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
8569
+ ` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
8570
+ `});`,
8571
+ ``,
8572
+ `listenWalkerProgress((event) => {`,
8573
+ ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
8574
+ ` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
8575
+ ` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
8576
+ `});`,
8577
+ ``,
8578
+ `listenWalkerComplete((results) => {`,
8579
+ ` console.log("Walker completed:", results.bestStrategy);`,
8580
+ ` Walker.dump("${escapedSymbol}", results.walkerName);`,
8581
+ `});`,
8582
+ ``,
8583
+ `listenDoneBacktest((event) => {`,
8584
+ ` console.log("Backtest completed:", event.symbol);`,
8585
+ ` Backtest.dump(event.strategyName);`,
8586
+ `});`,
8587
+ ``,
8588
+ `listenError((error) => {`,
8589
+ ` console.error("Error occurred:", error);`,
8590
+ `});`
8591
+ ].join("\n");
8592
+ };
8593
+ /**
8594
+ * Generates dumpJson() helper function for debug output.
8595
+ * Saves LLM conversations and results to ./dump/strategy/{resultId}/
8596
+ *
8597
+ * @param symbol - Trading pair symbol (unused, for consistency)
8598
+ * @returns Generated async dumpJson() function
8599
+ */
8600
+ this.getJsonDumpTemplate = async (symbol) => {
8601
+ this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
8602
+ symbol,
8603
+ });
8604
+ return [
8605
+ `async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
8606
+ ` // Extract system messages and system reminders from existing data`,
8607
+ ` const systemMessages = history.filter((m) => m.role === "system");`,
8608
+ ` const userMessages = history.filter((m) => m.role === "user");`,
8609
+ ` const subfolderPath = path.join(outputDir, resultId);`,
8610
+ ``,
8611
+ ` try {`,
8612
+ ` await fs.access(subfolderPath);`,
8613
+ ` return;`,
8614
+ ` } catch {`,
8615
+ ` await fs.mkdir(subfolderPath, { recursive: true });`,
8616
+ ` }`,
8617
+ ``,
8618
+ ` {`,
8619
+ ` let summary = "# Outline Result Summary\\n";`,
8620
+ ``,
8621
+ ` {`,
8622
+ ` summary += "\\n";`,
8623
+ ` summary += \`**ResultId**: \${resultId}\\n\`;`,
8624
+ ` summary += "\\n";`,
8625
+ ` }`,
8626
+ ``,
8627
+ ` if (result) {`,
8628
+ ` summary += "## Output Data\\n\\n";`,
8629
+ ` summary += "\`\`\`json\\n";`,
8630
+ ` summary += JSON.stringify(result, null, 2);`,
8631
+ ` summary += "\\n\`\`\`\\n\\n";`,
8632
+ ` }`,
8633
+ ``,
8634
+ ` // Add system messages to summary`,
8635
+ ` if (systemMessages.length > 0) {`,
8636
+ ` summary += "## System Messages\\n\\n";`,
8637
+ ` systemMessages.forEach((msg, idx) => {`,
8638
+ ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
8639
+ ` summary += msg.content;`,
8640
+ ` summary += "\\n";`,
8641
+ ` });`,
8642
+ ` }`,
8643
+ ``,
8644
+ ` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
8645
+ ` await fs.writeFile(summaryFile, summary, "utf8");`,
8646
+ ` }`,
8647
+ ``,
8648
+ ` {`,
8649
+ ` await Promise.all(`,
8650
+ ` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
8651
+ ` const messageNum = String(idx + 1).padStart(2, "0");`,
8652
+ ` const contentFileName = \`\${messageNum}_user_message.md\`;`,
8653
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
8654
+ ``,
8655
+ ` {`,
8656
+ ` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
8657
+ ` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
8658
+ ` if (messageSizeKb > WARN_KB) {`,
8659
+ ` console.warn(`,
8660
+ ` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
8661
+ ` );`,
8662
+ ` }`,
8663
+ ` }`,
8664
+ ``,
8665
+ ` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
8666
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
8667
+ ` content += message.content;`,
8668
+ ` content += "\\n";`,
8669
+ ``,
8670
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
8671
+ ` })`,
8672
+ ` );`,
8673
+ ` }`,
8674
+ ``,
8675
+ ` {`,
8676
+ ` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
8677
+ ` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
8678
+ ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
8679
+ ``,
8680
+ ` let content = "# Full Outline Result\\n\\n";`,
8681
+ ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
8682
+ ``,
8683
+ ` if (result) {`,
8684
+ ` content += "## Output Data\\n\\n";`,
8685
+ ` content += "\`\`\`json\\n";`,
8686
+ ` content += JSON.stringify(result, null, 2);`,
8687
+ ` content += "\\n\`\`\`\\n";`,
8688
+ ` }`,
8689
+ ``,
8690
+ ` await fs.writeFile(contentFilePath, content, "utf8");`,
8691
+ ` }`,
8692
+ `}`
8693
+ ].join("\n");
8694
+ };
8695
+ /**
8696
+ * Generates text() helper for LLM text generation.
8697
+ * Uses Ollama gpt-oss:20b model for market analysis.
8698
+ *
8699
+ * @param symbol - Trading pair symbol (used in prompt)
8700
+ * @returns Generated async text() function
8701
+ */
8702
+ this.getTextTemplate = async (symbol) => {
8703
+ this.loggerService.log("optimizerTemplateService getTextTemplate", {
8704
+ symbol,
8705
+ });
8706
+ // Escape special characters in symbol to prevent code injection
8707
+ const escapedSymbol = String(symbol)
8708
+ .replace(/\\/g, '\\\\')
8709
+ .replace(/`/g, '\\`')
8710
+ .replace(/\$/g, '\\$')
8711
+ .toUpperCase();
8712
+ return [
8713
+ `async function text(messages) {`,
8714
+ ` const ollama = new Ollama({`,
8715
+ ` host: "https://ollama.com",`,
8716
+ ` headers: {`,
8717
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
8718
+ ` },`,
8719
+ ` });`,
8720
+ ``,
8721
+ ` const response = await ollama.chat({`,
8722
+ ` model: "gpt-oss:20b",`,
8723
+ ` messages: [`,
8724
+ ` {`,
8725
+ ` role: "system",`,
8726
+ ` content: [`,
8727
+ ` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
8728
+ ` "только отчёт готовый для копипасты целиком",`,
8729
+ ` "",`,
8730
+ ` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
8731
+ ` ].join("\\n"),`,
8732
+ ` },`,
8733
+ ` ...messages,`,
8734
+ ` {`,
8735
+ ` role: "user",`,
8736
+ ` content: [`,
8737
+ ` "На каких условиях мне купить ${escapedSymbol}?",`,
8738
+ ` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
8739
+ ` "Какой RR ставить для позиций?",`,
8740
+ ` "Предпочтительны LONG или SHORT позиции?",`,
8741
+ ` "",`,
8742
+ ` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
8743
+ ` ].join("\\n")`,
8744
+ ` }`,
8745
+ ` ]`,
8746
+ ` });`,
8747
+ ``,
8748
+ ` const content = response.message.content.trim();`,
8749
+ ` return content`,
8750
+ ` .replace(/\\\\/g, '\\\\\\\\')`,
8751
+ ` .replace(/\`/g, '\\\\\`')`,
8752
+ ` .replace(/\\$/g, '\\\\$')`,
8753
+ ` .replace(/"/g, '\\\\"')`,
8754
+ ` .replace(/'/g, "\\\\'");`,
8755
+ `}`
8756
+ ].join("\n");
8757
+ };
8758
+ /**
8759
+ * Generates json() helper for structured LLM output.
8760
+ * Uses Ollama with JSON schema for trading signals.
8761
+ *
8762
+ * Signal schema:
8763
+ * - position: "wait" | "long" | "short"
8764
+ * - note: strategy explanation
8765
+ * - priceOpen: entry price
8766
+ * - priceTakeProfit: target price
8767
+ * - priceStopLoss: stop price
8768
+ * - minuteEstimatedTime: expected duration (max 360 min)
8769
+ *
8770
+ * @param symbol - Trading pair symbol (unused, for consistency)
8771
+ * @returns Generated async json() function with signal schema
8772
+ */
8773
+ this.getJsonTemplate = async (symbol) => {
8774
+ this.loggerService.log("optimizerTemplateService getJsonTemplate", {
8775
+ symbol,
8776
+ });
8777
+ return [
8778
+ `async function json(messages) {`,
8779
+ ` const ollama = new Ollama({`,
8780
+ ` host: "https://ollama.com",`,
8781
+ ` headers: {`,
8782
+ ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
8783
+ ` },`,
8784
+ ` });`,
8785
+ ``,
8786
+ ` const response = await ollama.chat({`,
8787
+ ` model: "gpt-oss:20b",`,
8788
+ ` messages: [`,
8789
+ ` {`,
8790
+ ` role: "system",`,
8791
+ ` content: [`,
8792
+ ` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
8793
+ ` "",`,
8794
+ ` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
8795
+ ` "",`,
8796
+ ` "1. ТИПЫ ПОЗИЦИЙ:",`,
8797
+ ` " - position='wait': нет четкого сигнала, жди лучших условий",`,
8798
+ ` " - position='long': бычий сигнал, цена будет расти",`,
8799
+ ` " - position='short': медвежий сигнал, цена будет падать",`,
8800
+ ` "",`,
8801
+ ` "2. ЦЕНА ВХОДА (priceOpen):",`,
8802
+ ` " - Может быть текущей рыночной ценой для немедленного входа",`,
8803
+ ` " - Может быть отложенной ценой для входа при достижении уровня",`,
8804
+ ` " - Укажи оптимальную цену входа согласно технического анализа",`,
8805
+ ` "",`,
8806
+ ` "3. УРОВНИ ВЫХОДА:",`,
8807
+ ` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
8808
+ ` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
8809
+ ` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
8810
+ ` "",`,
8811
+ ` "4. ВРЕМЕННЫЕ РАМКИ:",`,
8812
+ ` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
8813
+ ` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
8814
+ ` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
8815
+ ` ].join("\\n"),`,
8816
+ ` },`,
8817
+ ` ...messages,`,
8818
+ ` ],`,
8819
+ ` format: {`,
8820
+ ` type: "object",`,
8821
+ ` properties: {`,
8822
+ ` position: {`,
8823
+ ` type: "string",`,
8824
+ ` enum: ["wait", "long", "short"],`,
8825
+ ` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
8826
+ ` },`,
8827
+ ` note: {`,
8828
+ ` type: "string",`,
8829
+ ` description: "Professional trading recommendation with price levels",`,
8830
+ ` },`,
8831
+ ` priceOpen: {`,
8832
+ ` type: "number",`,
8833
+ ` description: "Entry price (current market price or limit order price)",`,
8834
+ ` },`,
8835
+ ` priceTakeProfit: {`,
8836
+ ` type: "number",`,
8837
+ ` description: "Take profit target price",`,
8838
+ ` },`,
8839
+ ` priceStopLoss: {`,
8840
+ ` type: "number",`,
8841
+ ` description: "Stop loss exit price",`,
8842
+ ` },`,
8843
+ ` minuteEstimatedTime: {`,
8844
+ ` type: "number",`,
8845
+ ` description: "Expected time to reach TP in minutes (max 360)",`,
8846
+ ` },`,
8847
+ ` },`,
8848
+ ` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
8849
+ ` },`,
8850
+ ` });`,
8851
+ ``,
8852
+ ` const jsonResponse = JSON.parse(response.message.content.trim());`,
8853
+ ` return jsonResponse;`,
8854
+ `}`
8855
+ ].join("\n");
8856
+ };
8857
+ }
8858
+ }
8859
+
8860
+ /**
8861
+ * Service for managing optimizer schema registration and retrieval.
8862
+ * Provides validation and registry management for optimizer configurations.
8863
+ *
8864
+ * Uses ToolRegistry for immutable schema storage.
8865
+ */
8866
+ class OptimizerSchemaService {
8867
+ constructor() {
8868
+ this.loggerService = inject(TYPES.loggerService);
8869
+ this._registry = new ToolRegistry("optimizerSchema");
8870
+ /**
8871
+ * Registers a new optimizer schema.
8872
+ * Validates required fields before registration.
8873
+ *
8874
+ * @param key - Unique optimizer name
8875
+ * @param value - Optimizer schema configuration
8876
+ * @throws Error if schema validation fails
8877
+ */
8878
+ this.register = (key, value) => {
8879
+ this.loggerService.log(`optimizerSchemaService register`, { key });
8880
+ this.validateShallow(value);
8881
+ this._registry = this._registry.register(key, value);
8882
+ };
8883
+ /**
8884
+ * Validates optimizer schema structure.
8885
+ * Checks required fields: optimizerName, rangeTrain, source, getPrompt.
8886
+ *
8887
+ * @param optimizerSchema - Schema to validate
8888
+ * @throws Error if validation fails
8889
+ */
8890
+ this.validateShallow = (optimizerSchema) => {
8891
+ this.loggerService.log(`optimizerTemplateService validateShallow`, {
8892
+ optimizerSchema,
8893
+ });
8894
+ if (typeof optimizerSchema.optimizerName !== "string") {
8895
+ throw new Error(`optimizer template validation failed: missing optimizerName`);
8896
+ }
8897
+ if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
8898
+ throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
8899
+ }
8900
+ if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
8901
+ throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
8902
+ }
8903
+ if (typeof optimizerSchema.getPrompt !== "function") {
8904
+ throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
8905
+ }
8906
+ };
8907
+ /**
8908
+ * Partially overrides an existing optimizer schema.
8909
+ * Merges provided values with existing schema.
8910
+ *
8911
+ * @param key - Optimizer name to override
8912
+ * @param value - Partial schema values to merge
8913
+ * @returns Updated complete schema
8914
+ * @throws Error if optimizer not found
8915
+ */
8916
+ this.override = (key, value) => {
8917
+ this.loggerService.log(`optimizerSchemaService override`, { key });
8918
+ this._registry = this._registry.override(key, value);
8919
+ return this._registry.get(key);
8920
+ };
8921
+ /**
8922
+ * Retrieves optimizer schema by name.
8923
+ *
8924
+ * @param key - Optimizer name
8925
+ * @returns Complete optimizer schema
8926
+ * @throws Error if optimizer not found
8927
+ */
8928
+ this.get = (key) => {
8929
+ this.loggerService.log(`optimizerSchemaService get`, { key });
8930
+ return this._registry.get(key);
8931
+ };
8932
+ }
8933
+ }
8934
+
8935
+ /**
8936
+ * Service for validating optimizer existence and managing optimizer registry.
8937
+ * Maintains a Map of registered optimizers for validation purposes.
8938
+ *
8939
+ * Uses memoization for efficient repeated validation checks.
8940
+ */
8941
+ class OptimizerValidationService {
8942
+ constructor() {
8943
+ this.loggerService = inject(TYPES.loggerService);
8944
+ this._optimizerMap = new Map();
8945
+ /**
8946
+ * Adds optimizer to validation registry.
8947
+ * Prevents duplicate optimizer names.
8948
+ *
8949
+ * @param optimizerName - Unique optimizer identifier
8950
+ * @param optimizerSchema - Complete optimizer schema
8951
+ * @throws Error if optimizer with same name already exists
8952
+ */
8953
+ this.addOptimizer = (optimizerName, optimizerSchema) => {
8954
+ this.loggerService.log("optimizerValidationService addOptimizer", {
8955
+ optimizerName,
8956
+ optimizerSchema,
8957
+ });
8958
+ if (this._optimizerMap.has(optimizerName)) {
8959
+ throw new Error(`optimizer ${optimizerName} already exist`);
8960
+ }
8961
+ this._optimizerMap.set(optimizerName, optimizerSchema);
8962
+ };
8963
+ /**
8964
+ * Validates that optimizer exists in registry.
8965
+ * Memoized for performance on repeated checks.
8966
+ *
8967
+ * @param optimizerName - Optimizer name to validate
8968
+ * @param source - Source method name for error messages
8969
+ * @throws Error if optimizer not found
8970
+ */
8971
+ this.validate = memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
8972
+ this.loggerService.log("optimizerValidationService validate", {
8973
+ optimizerName,
8974
+ source,
8975
+ });
8976
+ const optimizer = this._optimizerMap.get(optimizerName);
8977
+ if (!optimizer) {
8978
+ throw new Error(`optimizer ${optimizerName} not found source=${source}`);
8979
+ }
8980
+ return true;
8981
+ });
8982
+ /**
8983
+ * Lists all registered optimizer schemas.
8984
+ *
8985
+ * @returns Array of all optimizer schemas
8986
+ */
8987
+ this.list = async () => {
8988
+ this.loggerService.log("optimizerValidationService list");
8989
+ return Array.from(this._optimizerMap.values());
8990
+ };
8991
+ }
8992
+ }
8993
+
8994
+ const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
8995
+ const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
8996
+ const METHOD_NAME_DUMP = "optimizerGlobalService dump";
8997
+ /**
8998
+ * Global service for optimizer operations with validation.
8999
+ * Entry point for public API, performs validation before delegating to ConnectionService.
9000
+ *
9001
+ * Workflow:
9002
+ * 1. Log operation
9003
+ * 2. Validate optimizer exists
9004
+ * 3. Delegate to OptimizerConnectionService
9005
+ */
9006
+ class OptimizerGlobalService {
9007
+ constructor() {
9008
+ this.loggerService = inject(TYPES.loggerService);
9009
+ this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
9010
+ this.optimizerValidationService = inject(TYPES.optimizerValidationService);
9011
+ /**
9012
+ * Fetches data from all sources and generates strategy metadata.
9013
+ * Validates optimizer existence before execution.
9014
+ *
9015
+ * @param symbol - Trading pair symbol
9016
+ * @param optimizerName - Optimizer identifier
9017
+ * @returns Array of generated strategies with conversation context
9018
+ * @throws Error if optimizer not found
9019
+ */
9020
+ this.getData = async (symbol, optimizerName) => {
9021
+ this.loggerService.log(METHOD_NAME_GET_DATA, {
9022
+ symbol,
9023
+ optimizerName,
9024
+ });
9025
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
9026
+ return await this.optimizerConnectionService.getData(symbol, optimizerName);
9027
+ };
9028
+ /**
9029
+ * Generates complete executable strategy code.
9030
+ * Validates optimizer existence before execution.
9031
+ *
9032
+ * @param symbol - Trading pair symbol
9033
+ * @param optimizerName - Optimizer identifier
9034
+ * @returns Generated TypeScript/JavaScript code as string
9035
+ * @throws Error if optimizer not found
9036
+ */
9037
+ this.getCode = async (symbol, optimizerName) => {
9038
+ this.loggerService.log(METHOD_NAME_GET_CODE, {
9039
+ symbol,
9040
+ optimizerName,
9041
+ });
9042
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
9043
+ return await this.optimizerConnectionService.getCode(symbol, optimizerName);
9044
+ };
9045
+ /**
9046
+ * Generates and saves strategy code to file.
9047
+ * Validates optimizer existence before execution.
9048
+ *
9049
+ * @param symbol - Trading pair symbol
9050
+ * @param optimizerName - Optimizer identifier
9051
+ * @param path - Output directory path (optional)
9052
+ * @throws Error if optimizer not found
9053
+ */
9054
+ this.dump = async (symbol, optimizerName, path) => {
9055
+ this.loggerService.log(METHOD_NAME_DUMP, {
9056
+ symbol,
9057
+ optimizerName,
9058
+ path,
9059
+ });
9060
+ this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
9061
+ return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
9062
+ };
9063
+ }
9064
+ }
9065
+
9066
+ const ITERATION_LIMIT = 25;
9067
+ const DEFAULT_SOURCE_NAME = "unknown";
9068
+ const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
9069
+ /**
9070
+ * Default user message formatter.
9071
+ * Delegates to template's getUserMessage method.
9072
+ *
9073
+ * @param symbol - Trading pair symbol
9074
+ * @param data - Fetched data array
9075
+ * @param name - Source name
9076
+ * @param self - ClientOptimizer instance
9077
+ * @returns Formatted user message content
9078
+ */
9079
+ const DEFAULT_USER_FN = async (symbol, data, name, self) => {
9080
+ return await self.params.template.getUserMessage(symbol, data, name);
9081
+ };
9082
+ /**
9083
+ * Default assistant message formatter.
9084
+ * Delegates to template's getAssistantMessage method.
9085
+ *
9086
+ * @param symbol - Trading pair symbol
9087
+ * @param data - Fetched data array
9088
+ * @param name - Source name
9089
+ * @param self - ClientOptimizer instance
9090
+ * @returns Formatted assistant message content
9091
+ */
9092
+ const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
9093
+ return await self.params.template.getAssistantMessage(symbol, data, name);
9094
+ };
9095
+ /**
9096
+ * Resolves paginated data from source with deduplication.
9097
+ * Uses iterateDocuments to handle pagination automatically.
9098
+ *
9099
+ * @param fetch - Source fetch function
9100
+ * @param filterData - Filter arguments (symbol, dates)
9101
+ * @returns Deduplicated array of all fetched data
9102
+ */
9103
+ const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
9104
+ const iterator = iterateDocuments({
9105
+ limit: ITERATION_LIMIT,
9106
+ async createRequest({ limit, offset }) {
9107
+ return await fetch({
9108
+ symbol: filterData.symbol,
9109
+ startDate: filterData.startDate,
9110
+ endDate: filterData.endDate,
9111
+ limit,
9112
+ offset,
9113
+ });
9114
+ },
9115
+ });
9116
+ const distinct = distinctDocuments(iterator, (data) => data.id);
9117
+ return await resolveDocuments(distinct);
9118
+ };
9119
+ /**
9120
+ * Collects data from all sources and generates strategy metadata.
9121
+ * Iterates through training ranges, fetches data from each source,
9122
+ * builds LLM conversation history, and generates strategy prompts.
9123
+ *
9124
+ * @param symbol - Trading pair symbol
9125
+ * @param self - ClientOptimizer instance
9126
+ * @returns Array of generated strategies with conversation context
9127
+ */
9128
+ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
9129
+ const strategyList = [];
9130
+ for (const { startDate, endDate } of self.params.rangeTrain) {
9131
+ const messageList = [];
9132
+ for (const source of self.params.source) {
9133
+ if (typeof source === "function") {
9134
+ const data = await RESOLVE_PAGINATION_FN(source, {
9135
+ symbol,
9136
+ startDate,
9137
+ endDate,
9138
+ });
9139
+ if (self.params.callbacks?.onSourceData) {
9140
+ await self.params.callbacks.onSourceData(symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
9141
+ }
9142
+ const [userContent, assistantContent] = await Promise.all([
9143
+ DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
9144
+ DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
9145
+ ]);
9146
+ messageList.push({
9147
+ role: "user",
9148
+ content: userContent,
9149
+ }, {
9150
+ role: "assistant",
9151
+ content: assistantContent,
9152
+ });
9153
+ }
9154
+ else {
9155
+ const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
9156
+ const data = await RESOLVE_PAGINATION_FN(fetch, {
9157
+ symbol,
9158
+ startDate,
9159
+ endDate,
9160
+ });
9161
+ if (self.params.callbacks?.onSourceData) {
9162
+ await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
9163
+ }
9164
+ const [userContent, assistantContent] = await Promise.all([
9165
+ user(symbol, data, name, self),
9166
+ assistant(symbol, data, name, self),
9167
+ ]);
9168
+ messageList.push({
9169
+ role: "user",
9170
+ content: userContent,
9171
+ }, {
9172
+ role: "assistant",
9173
+ content: assistantContent,
9174
+ });
9175
+ }
9176
+ const name = "name" in source
9177
+ ? source.name || DEFAULT_SOURCE_NAME
9178
+ : DEFAULT_SOURCE_NAME;
9179
+ strategyList.push({
9180
+ symbol,
9181
+ name,
9182
+ messages: messageList,
9183
+ strategy: await self.params.getPrompt(symbol, messageList),
9184
+ });
9185
+ }
9186
+ }
9187
+ if (self.params.callbacks?.onData) {
9188
+ await self.params.callbacks.onData(symbol, strategyList);
9189
+ }
9190
+ return strategyList;
9191
+ };
9192
+ /**
9193
+ * Generates complete executable strategy code.
9194
+ * Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
9195
+ *
9196
+ * @param symbol - Trading pair symbol
9197
+ * @param self - ClientOptimizer instance
9198
+ * @returns Generated TypeScript/JavaScript code as string
9199
+ */
9200
+ const GET_STRATEGY_CODE_FN = async (symbol, self) => {
9201
+ const strategyData = await self.getData(symbol);
9202
+ const prefix = CREATE_PREFIX_FN();
9203
+ const sections = [];
9204
+ const exchangeName = `${prefix}_exchange`;
9205
+ // 1. Top banner with imports
9206
+ {
9207
+ sections.push(await self.params.template.getTopBanner(symbol));
9208
+ sections.push("");
9209
+ }
9210
+ // 2. JSON dump helper function
9211
+ {
9212
+ sections.push(await self.params.template.getJsonDumpTemplate(symbol));
9213
+ sections.push("");
9214
+ }
9215
+ // 3. Helper functions (text and json)
9216
+ {
9217
+ sections.push(await self.params.template.getTextTemplate(symbol));
9218
+ sections.push("");
9219
+ }
9220
+ {
9221
+ sections.push(await self.params.template.getJsonTemplate(symbol));
9222
+ sections.push("");
9223
+ }
9224
+ // 4. Exchange template (assuming first strategy has exchange info)
9225
+ {
9226
+ sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
9227
+ sections.push("");
9228
+ }
9229
+ // 5. Train frame templates
9230
+ {
9231
+ for (let i = 0; i < self.params.rangeTrain.length; i++) {
9232
+ const range = self.params.rangeTrain[i];
9233
+ const frameName = `${prefix}_train_frame-${i + 1}`;
9234
+ sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
9235
+ range.startDate, range.endDate));
9236
+ sections.push("");
9237
+ }
9238
+ }
9239
+ // 6. Test frame template
9240
+ {
9241
+ const testFrameName = `${prefix}_test_frame`;
9242
+ sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
9243
+ self.params.rangeTest.startDate, self.params.rangeTest.endDate));
9244
+ sections.push("");
9245
+ }
9246
+ // 7. Strategy templates for each generated strategy
9247
+ {
9248
+ for (let i = 0; i < strategyData.length; i++) {
9249
+ const strategy = strategyData[i];
9250
+ const strategyName = `${prefix}_strategy-${i + 1}`;
9251
+ const interval = "5m"; // default interval
9252
+ sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
9253
+ sections.push("");
9254
+ }
9255
+ }
9256
+ // 8. Walker template (uses test frame for validation)
9257
+ {
9258
+ const walkerName = `${prefix}_walker`;
9259
+ const testFrameName = `${prefix}_test_frame`;
9260
+ const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
9261
+ sections.push(await self.params.template.getWalkerTemplate(walkerName, `${prefix}_${exchangeName}`, testFrameName, strategies));
9262
+ sections.push("");
9263
+ }
9264
+ // 9. Launcher template
9265
+ {
9266
+ const walkerName = `${prefix}_walker`;
9267
+ sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
9268
+ sections.push("");
9269
+ }
9270
+ const code = sections.join("\n");
9271
+ if (self.params.callbacks?.onCode) {
9272
+ await self.params.callbacks.onCode(symbol, code);
9273
+ }
9274
+ return code;
9275
+ };
9276
+ /**
9277
+ * Saves generated strategy code to file.
9278
+ * Creates directory if needed, writes .mjs file with generated code.
9279
+ *
9280
+ * @param symbol - Trading pair symbol
9281
+ * @param path - Output directory path
9282
+ * @param self - ClientOptimizer instance
9283
+ */
9284
+ const GET_STRATEGY_DUMP_FN = async (symbol, path, self) => {
9285
+ const report = await self.getCode(symbol);
9286
+ try {
9287
+ const dir = join(process.cwd(), path);
9288
+ await mkdir(dir, { recursive: true });
9289
+ const filename = `${self.params.optimizerName}_${symbol}.mjs`;
9290
+ const filepath = join(dir, filename);
9291
+ await writeFile(filepath, report, "utf-8");
9292
+ self.params.logger.info(`Optimizer report saved: ${filepath}`);
9293
+ if (self.params.callbacks?.onDump) {
9294
+ await self.params.callbacks.onDump(symbol, filepath);
9295
+ }
9296
+ }
9297
+ catch (error) {
9298
+ self.params.logger.warn(`Failed to save optimizer report:`, error);
9299
+ throw error;
9300
+ }
9301
+ };
9302
+ /**
9303
+ * Client implementation for optimizer operations.
9304
+ *
9305
+ * Features:
9306
+ * - Data collection from multiple sources with pagination
9307
+ * - LLM conversation history building
9308
+ * - Strategy code generation with templates
9309
+ * - File export with callbacks
9310
+ *
9311
+ * Used by OptimizerConnectionService to create optimizer instances.
9312
+ */
9313
+ class ClientOptimizer {
9314
+ constructor(params) {
9315
+ this.params = params;
9316
+ /**
9317
+ * Fetches data from all sources and generates strategy metadata.
9318
+ * Processes each training range and builds LLM conversation history.
9319
+ *
9320
+ * @param symbol - Trading pair symbol
9321
+ * @returns Array of generated strategies with conversation context
9322
+ */
9323
+ this.getData = async (symbol) => {
9324
+ this.params.logger.debug("ClientOptimizer getData", {
9325
+ symbol,
9326
+ });
9327
+ return await GET_STRATEGY_DATA_FN(symbol, this);
9328
+ };
9329
+ /**
9330
+ * Generates complete executable strategy code.
9331
+ * Includes imports, helpers, strategies, walker, and launcher.
9332
+ *
9333
+ * @param symbol - Trading pair symbol
9334
+ * @returns Generated TypeScript/JavaScript code as string
9335
+ */
9336
+ this.getCode = async (symbol) => {
9337
+ this.params.logger.debug("ClientOptimizer getCode", {
9338
+ symbol,
9339
+ });
9340
+ return await GET_STRATEGY_CODE_FN(symbol, this);
9341
+ };
9342
+ /**
9343
+ * Generates and saves strategy code to file.
9344
+ * Creates directory if needed, writes .mjs file.
9345
+ *
9346
+ * @param symbol - Trading pair symbol
9347
+ * @param path - Output directory path (default: "./")
9348
+ */
9349
+ this.dump = async (symbol, path = "./") => {
9350
+ this.params.logger.debug("ClientOptimizer dump", {
9351
+ symbol,
9352
+ path,
9353
+ });
9354
+ return await GET_STRATEGY_DUMP_FN(symbol, path, this);
9355
+ };
9356
+ }
9357
+ }
9358
+
9359
+ /**
9360
+ * Service for creating and caching optimizer client instances.
9361
+ * Handles dependency injection and template merging.
9362
+ *
9363
+ * Features:
9364
+ * - Memoized optimizer instances (one per optimizerName)
9365
+ * - Template merging (custom + defaults)
9366
+ * - Logger injection
9367
+ * - Delegates to ClientOptimizer for actual operations
9368
+ */
9369
+ class OptimizerConnectionService {
9370
+ constructor() {
9371
+ this.loggerService = inject(TYPES.loggerService);
9372
+ this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
9373
+ this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
9374
+ /**
9375
+ * Creates or retrieves cached optimizer instance.
9376
+ * Memoized by optimizerName for performance.
9377
+ *
9378
+ * Merges custom templates from schema with defaults from OptimizerTemplateService.
9379
+ *
9380
+ * @param optimizerName - Unique optimizer identifier
9381
+ * @returns ClientOptimizer instance with resolved dependencies
9382
+ */
9383
+ this.getOptimizer = memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
9384
+ const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks } = this.optimizerSchemaService.get(optimizerName);
9385
+ 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;
9386
+ const template = {
9387
+ getAssistantMessage,
9388
+ getExchangeTemplate,
9389
+ getFrameTemplate,
9390
+ getJsonDumpTemplate,
9391
+ getJsonTemplate,
9392
+ getLauncherTemplate,
9393
+ getStrategyTemplate,
9394
+ getTextTemplate,
9395
+ getWalkerTemplate,
9396
+ getTopBanner,
9397
+ getUserMessage,
9398
+ };
9399
+ return new ClientOptimizer({
9400
+ optimizerName,
9401
+ logger: this.loggerService,
9402
+ getPrompt,
9403
+ rangeTest,
9404
+ rangeTrain,
9405
+ source,
9406
+ template,
9407
+ callbacks,
9408
+ });
9409
+ });
9410
+ /**
9411
+ * Fetches data from all sources and generates strategy metadata.
9412
+ *
9413
+ * @param symbol - Trading pair symbol
9414
+ * @param optimizerName - Optimizer identifier
9415
+ * @returns Array of generated strategies with conversation context
9416
+ */
9417
+ this.getData = async (symbol, optimizerName) => {
9418
+ this.loggerService.log("optimizerConnectionService getData", {
9419
+ symbol,
9420
+ optimizerName,
9421
+ });
9422
+ const optimizer = this.getOptimizer(optimizerName);
9423
+ return await optimizer.getData(symbol);
9424
+ };
9425
+ /**
9426
+ * Generates complete executable strategy code.
9427
+ *
9428
+ * @param symbol - Trading pair symbol
9429
+ * @param optimizerName - Optimizer identifier
9430
+ * @returns Generated TypeScript/JavaScript code as string
9431
+ */
9432
+ this.getCode = async (symbol, optimizerName) => {
9433
+ this.loggerService.log("optimizerConnectionService getCode", {
9434
+ symbol,
9435
+ optimizerName,
9436
+ });
9437
+ const optimizer = this.getOptimizer(optimizerName);
9438
+ return await optimizer.getCode(symbol);
9439
+ };
9440
+ /**
9441
+ * Generates and saves strategy code to file.
9442
+ *
9443
+ * @param symbol - Trading pair symbol
9444
+ * @param optimizerName - Optimizer identifier
9445
+ * @param path - Output directory path (optional)
9446
+ */
9447
+ this.dump = async (symbol, optimizerName, path) => {
9448
+ this.loggerService.log("optimizerConnectionService getCode", {
9449
+ symbol,
9450
+ optimizerName,
9451
+ });
9452
+ const optimizer = this.getOptimizer(optimizerName);
9453
+ return await optimizer.dump(symbol, path);
9454
+ };
9455
+ }
9456
+ }
9457
+
9458
+ /**
9459
+ * Symbol marker indicating that partial state needs initialization.
9460
+ * Used as sentinel value for _states before waitForInit() is called.
9461
+ */
9462
+ const NEED_FETCH = Symbol("need_fetch");
9463
+ /**
9464
+ * Array of profit level milestones to track (10%, 20%, ..., 100%).
9465
+ * Each level is checked during profit() method to emit events for newly reached levels.
9466
+ */
9467
+ const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
9468
+ /**
9469
+ * Array of loss level milestones to track (-10%, -20%, ..., -100%).
9470
+ * Each level is checked during loss() method to emit events for newly reached levels.
9471
+ */
9472
+ const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
9473
+ /**
9474
+ * Internal profit handler function for ClientPartial.
9475
+ *
9476
+ * Checks which profit levels have been reached and emits events for new levels only.
9477
+ * Uses Set-based deduplication to prevent duplicate events.
9478
+ *
9479
+ * @param symbol - Trading pair symbol
9480
+ * @param data - Signal row data
9481
+ * @param currentPrice - Current market price
9482
+ * @param revenuePercent - Current profit percentage (positive value)
9483
+ * @param backtest - True if backtest mode
9484
+ * @param when - Event timestamp
9485
+ * @param self - ClientPartial instance reference
9486
+ */
9487
+ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
9488
+ if (self._states === NEED_FETCH) {
9489
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9490
+ }
9491
+ let state = self._states.get(data.id);
9492
+ if (!state) {
9493
+ state = {
9494
+ profitLevels: new Set(),
9495
+ lossLevels: new Set(),
9496
+ };
9497
+ self._states.set(data.id, state);
9498
+ }
9499
+ let shouldPersist = false;
9500
+ for (const level of PROFIT_LEVELS) {
9501
+ if (revenuePercent >= level && !state.profitLevels.has(level)) {
9502
+ state.profitLevels.add(level);
9503
+ shouldPersist = true;
9504
+ self.params.logger.debug("ClientPartial profit level reached", {
9505
+ symbol,
9506
+ signalId: data.id,
9507
+ level,
9508
+ revenuePercent,
9509
+ backtest,
9510
+ });
9511
+ await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
9512
+ }
9513
+ }
9514
+ if (shouldPersist) {
9515
+ await self._persistState(symbol);
9516
+ }
9517
+ };
9518
+ /**
9519
+ * Internal loss handler function for ClientPartial.
9520
+ *
9521
+ * Checks which loss levels have been reached and emits events for new levels only.
9522
+ * Uses Set-based deduplication to prevent duplicate events.
9523
+ * Converts negative lossPercent to absolute value for level comparison.
9524
+ *
9525
+ * @param symbol - Trading pair symbol
9526
+ * @param data - Signal row data
9527
+ * @param currentPrice - Current market price
9528
+ * @param lossPercent - Current loss percentage (negative value)
9529
+ * @param backtest - True if backtest mode
9530
+ * @param when - Event timestamp
9531
+ * @param self - ClientPartial instance reference
9532
+ */
9533
+ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
9534
+ if (self._states === NEED_FETCH) {
9535
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9536
+ }
9537
+ let state = self._states.get(data.id);
9538
+ if (!state) {
9539
+ state = {
9540
+ profitLevels: new Set(),
9541
+ lossLevels: new Set(),
9542
+ };
9543
+ self._states.set(data.id, state);
9544
+ }
9545
+ const absLoss = Math.abs(lossPercent);
9546
+ let shouldPersist = false;
9547
+ for (const level of LOSS_LEVELS) {
9548
+ if (absLoss >= level && !state.lossLevels.has(level)) {
9549
+ state.lossLevels.add(level);
9550
+ shouldPersist = true;
9551
+ self.params.logger.debug("ClientPartial loss level reached", {
9552
+ symbol,
9553
+ signalId: data.id,
9554
+ level,
9555
+ lossPercent,
9556
+ backtest,
9557
+ });
9558
+ await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
9559
+ }
9560
+ }
9561
+ if (shouldPersist) {
9562
+ await self._persistState(symbol);
9563
+ }
9564
+ };
9565
+ /**
9566
+ * Internal initialization function for ClientPartial.
9567
+ *
9568
+ * Loads persisted partial state from disk and restores in-memory Maps.
9569
+ * Converts serialized arrays back to Sets for O(1) lookups.
9570
+ *
9571
+ * @param symbol - Trading pair symbol
9572
+ * @param self - ClientPartial instance reference
9573
+ */
9574
+ const WAIT_FOR_INIT_FN = async (symbol, self) => {
9575
+ self.params.logger.debug("ClientPartial waitForInit", { symbol });
9576
+ if (self._states === NEED_FETCH) {
9577
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9578
+ }
9579
+ const partialData = await PersistPartialAdapter.readPartialData(symbol);
9580
+ for (const [signalId, data] of Object.entries(partialData)) {
9581
+ const state = {
9582
+ profitLevels: new Set(data.profitLevels),
9583
+ lossLevels: new Set(data.lossLevels),
9584
+ };
9585
+ self._states.set(signalId, state);
9586
+ }
9587
+ self.params.logger.info("ClientPartial restored state", {
9588
+ symbol,
9589
+ signalCount: Object.keys(partialData).length,
9590
+ });
9591
+ };
9592
+ /**
9593
+ * Client implementation for partial profit/loss level tracking.
9594
+ *
9595
+ * Features:
9596
+ * - Tracks profit and loss level milestones (10%, 20%, 30%, etc) per signal
9597
+ * - Deduplicates events using Set-based state per signal ID
9598
+ * - Persists state to disk for crash recovery in live mode
9599
+ * - Emits events via onProfit/onLoss callbacks for each newly reached level
9600
+ *
9601
+ * Architecture:
9602
+ * - Created per signal ID by PartialConnectionService (memoized)
9603
+ * - State stored in Map<signalId, IPartialState> with Set<PartialLevel>
9604
+ * - Persistence handled by PersistPartialAdapter (atomic file writes)
9605
+ *
9606
+ * Lifecycle:
9607
+ * 1. Construction: Initialize empty Map
9608
+ * 2. waitForInit(): Load persisted state from disk
9609
+ * 3. profit()/loss(): Check levels, emit events, persist changes
9610
+ * 4. clear(): Remove signal state, persist, clean up memoized instance
9611
+ *
9612
+ * @example
9613
+ * ```typescript
9614
+ * import { ClientPartial } from "./client/ClientPartial";
9615
+ *
9616
+ * const partial = new ClientPartial({
9617
+ * logger: loggerService,
9618
+ * onProfit: async (symbol, data, price, level, backtest, timestamp) => {
9619
+ * console.log(`Signal ${data.id} reached ${level}% profit at ${price}`);
9620
+ * // Emit to partialProfitSubject
9621
+ * },
9622
+ * onLoss: async (symbol, data, price, level, backtest, timestamp) => {
9623
+ * console.log(`Signal ${data.id} reached -${level}% loss at ${price}`);
9624
+ * // Emit to partialLossSubject
9625
+ * }
9626
+ * });
9627
+ *
9628
+ * // Initialize from persisted state
9629
+ * await partial.waitForInit("BTCUSDT");
9630
+ *
9631
+ * // During signal monitoring in ClientStrategy
9632
+ * const signal = { id: "abc123", priceOpen: 50000, position: "long", ... };
9633
+ *
9634
+ * // Price rises to $55000 (10% profit)
9635
+ * await partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
9636
+ * // Emits onProfit callback for 10% level
9637
+ *
9638
+ * // Price rises to $61000 (22% profit)
9639
+ * await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
9640
+ * // Emits onProfit for 20% level only (10% already emitted)
9641
+ *
9642
+ * // Signal closes
9643
+ * await partial.clear("BTCUSDT", signal, 61000);
9644
+ * // State removed, changes persisted
9645
+ * ```
9646
+ */
9647
+ class ClientPartial {
9648
+ /**
9649
+ * Creates new ClientPartial instance.
9650
+ *
9651
+ * @param params - Partial parameters (logger, onProfit, onLoss callbacks)
9652
+ */
9653
+ constructor(params) {
9654
+ this.params = params;
9655
+ /**
9656
+ * Map of signal IDs to partial profit/loss state.
9657
+ * Uses NEED_FETCH sentinel before initialization.
9658
+ *
9659
+ * Each state contains:
9660
+ * - profitLevels: Set of reached profit levels (10, 20, 30, etc)
9661
+ * - lossLevels: Set of reached loss levels (10, 20, 30, etc)
9662
+ */
9663
+ this._states = NEED_FETCH;
9664
+ /**
9665
+ * Initializes partial state by loading from disk.
9666
+ *
9667
+ * Uses singleshot pattern to ensure initialization happens exactly once per symbol.
9668
+ * Reads persisted state from PersistPartialAdapter and restores to _states Map.
9669
+ *
9670
+ * Must be called before profit()/loss()/clear() methods.
9671
+ *
9672
+ * @param symbol - Trading pair symbol
9673
+ * @returns Promise that resolves when initialization is complete
9674
+ *
9675
+ * @example
9676
+ * ```typescript
9677
+ * const partial = new ClientPartial(params);
9678
+ * await partial.waitForInit("BTCUSDT"); // Load persisted state
9679
+ * // Now profit()/loss() can be called
9680
+ * ```
9681
+ */
9682
+ this.waitForInit = singleshot(async (symbol) => await WAIT_FOR_INIT_FN(symbol, this));
9683
+ this._states = new Map();
9684
+ }
9685
+ /**
9686
+ * Persists current partial state to disk.
9687
+ *
9688
+ * Converts in-memory Maps and Sets to JSON-serializable format:
9689
+ * - Map<signalId, IPartialState> → Record<signalId, IPartialData>
9690
+ * - Set<PartialLevel> → PartialLevel[]
9691
+ *
9692
+ * Called automatically after profit/loss level changes or clear().
9693
+ * Uses atomic file writes via PersistPartialAdapter.
9694
+ *
9695
+ * @param symbol - Trading pair symbol
9696
+ * @returns Promise that resolves when persistence is complete
9697
+ */
9698
+ async _persistState(symbol) {
9699
+ this.params.logger.debug("ClientPartial persistState", { symbol });
9700
+ if (this._states === NEED_FETCH) {
9701
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9702
+ }
9703
+ const partialData = {};
9704
+ for (const [signalId, state] of this._states.entries()) {
9705
+ partialData[signalId] = {
9706
+ profitLevels: Array.from(state.profitLevels),
9707
+ lossLevels: Array.from(state.lossLevels),
9708
+ };
9709
+ }
9710
+ await PersistPartialAdapter.writePartialData(partialData, symbol);
9711
+ }
9712
+ /**
9713
+ * Processes profit state and emits events for newly reached profit levels.
9714
+ *
9715
+ * Called by ClientStrategy during signal monitoring when revenuePercent > 0.
9716
+ * Iterates through PROFIT_LEVELS (10%, 20%, 30%, etc) and checks which
9717
+ * levels have been reached but not yet emitted (Set-based deduplication).
9718
+ *
9719
+ * For each new level:
9720
+ * 1. Adds level to state.profitLevels Set
9721
+ * 2. Logs debug message
9722
+ * 3. Calls params.onProfit callback (emits to partialProfitSubject)
9723
+ *
9724
+ * After all levels processed, persists state to disk if any new levels were found.
9725
+ *
9726
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9727
+ * @param data - Signal row data
9728
+ * @param currentPrice - Current market price
9729
+ * @param revenuePercent - Current profit percentage (positive value)
9730
+ * @param backtest - True if backtest mode, false if live mode
9731
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9732
+ * @returns Promise that resolves when profit processing is complete
9733
+ *
9734
+ * @example
9735
+ * ```typescript
9736
+ * // Signal at $50000, price rises to $61000 (22% profit)
9737
+ * await partial.profit("BTCUSDT", signal, 61000, 22.0, false, new Date());
9738
+ * // Emits events for 10% and 20% levels (if not already emitted)
9739
+ * // State persisted to disk
9740
+ * ```
9741
+ */
9742
+ async profit(symbol, data, currentPrice, revenuePercent, backtest, when) {
9743
+ this.params.logger.debug("ClientPartial profit", {
9744
+ symbol,
9745
+ signalId: data.id,
9746
+ currentPrice,
9747
+ revenuePercent,
9748
+ backtest,
9749
+ when,
9750
+ });
9751
+ return await HANDLE_PROFIT_FN(symbol, data, currentPrice, revenuePercent, backtest, when, this);
9752
+ }
9753
+ /**
9754
+ * Processes loss state and emits events for newly reached loss levels.
9755
+ *
9756
+ * Called by ClientStrategy during signal monitoring when revenuePercent < 0.
9757
+ * Converts negative lossPercent to absolute value and iterates through
9758
+ * LOSS_LEVELS (10%, 20%, 30%, etc) to check which levels have been reached
9759
+ * but not yet emitted (Set-based deduplication).
9760
+ *
9761
+ * For each new level:
9762
+ * 1. Adds level to state.lossLevels Set
9763
+ * 2. Logs debug message
9764
+ * 3. Calls params.onLoss callback (emits to partialLossSubject)
9765
+ *
9766
+ * After all levels processed, persists state to disk if any new levels were found.
9767
+ *
9768
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9769
+ * @param data - Signal row data
9770
+ * @param currentPrice - Current market price
9771
+ * @param lossPercent - Current loss percentage (negative value)
9772
+ * @param backtest - True if backtest mode, false if live mode
9773
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9774
+ * @returns Promise that resolves when loss processing is complete
9775
+ *
9776
+ * @example
9777
+ * ```typescript
9778
+ * // Signal at $50000, price drops to $39000 (-22% loss)
9779
+ * await partial.loss("BTCUSDT", signal, 39000, -22.0, false, new Date());
9780
+ * // Emits events for 10% and 20% loss levels (if not already emitted)
9781
+ * // State persisted to disk
9782
+ * ```
9783
+ */
9784
+ async loss(symbol, data, currentPrice, lossPercent, backtest, when) {
9785
+ this.params.logger.debug("ClientPartial loss", {
9786
+ symbol,
9787
+ signalId: data.id,
9788
+ currentPrice,
9789
+ lossPercent,
9790
+ backtest,
9791
+ when,
9792
+ });
9793
+ return await HANDLE_LOSS_FN(symbol, data, currentPrice, lossPercent, backtest, when, this);
9794
+ }
9795
+ /**
9796
+ * Clears partial profit/loss state for a signal when it closes.
9797
+ *
9798
+ * Called by ClientStrategy when signal completes (TP/SL/time_expired).
9799
+ * Removes signal's state from _states Map and persists changes to disk.
9800
+ *
9801
+ * After clear() completes:
9802
+ * - Signal state removed from memory (_states.delete)
9803
+ * - Changes persisted to disk (PersistPartialAdapter.writePartialData)
9804
+ * - Memoized ClientPartial instance cleared in PartialConnectionService
9805
+ *
9806
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9807
+ * @param data - Signal row data
9808
+ * @param priceClose - Final closing price
9809
+ * @returns Promise that resolves when clear is complete
9810
+ * @throws Error if ClientPartial not initialized (waitForInit not called)
9811
+ *
9812
+ * @example
9813
+ * ```typescript
9814
+ * // Signal closes at take profit
9815
+ * await partial.clear("BTCUSDT", signal, 52000);
9816
+ * // State removed: _states.delete(signal.id)
9817
+ * // Persisted: ./dump/data/partial/BTCUSDT/levels.json updated
9818
+ * // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
9819
+ * ```
9820
+ */
9821
+ async clear(symbol, data, priceClose) {
9822
+ this.params.logger.log("ClientPartial clear", {
9823
+ symbol,
9824
+ data,
9825
+ priceClose,
9826
+ });
9827
+ if (this._states === NEED_FETCH) {
9828
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
9829
+ }
9830
+ this._states.delete(data.id);
9831
+ await this._persistState(symbol);
9832
+ }
9833
+ }
9834
+
9835
+ /**
9836
+ * Callback function for emitting profit events to partialProfitSubject.
9837
+ *
9838
+ * Called by ClientPartial when a new profit level is reached.
9839
+ * Emits PartialProfitContract event to all subscribers.
9840
+ *
9841
+ * @param symbol - Trading pair symbol
9842
+ * @param data - Signal row data
9843
+ * @param currentPrice - Current market price
9844
+ * @param level - Profit level reached
9845
+ * @param backtest - True if backtest mode
9846
+ * @param timestamp - Event timestamp in milliseconds
9847
+ */
9848
+ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
9849
+ symbol,
9850
+ data,
9851
+ currentPrice,
9852
+ level,
9853
+ backtest,
9854
+ timestamp,
9855
+ });
9856
+ /**
9857
+ * Callback function for emitting loss events to partialLossSubject.
9858
+ *
9859
+ * Called by ClientPartial when a new loss level is reached.
9860
+ * Emits PartialLossContract event to all subscribers.
9861
+ *
9862
+ * @param symbol - Trading pair symbol
9863
+ * @param data - Signal row data
9864
+ * @param currentPrice - Current market price
9865
+ * @param level - Loss level reached
9866
+ * @param backtest - True if backtest mode
9867
+ * @param timestamp - Event timestamp in milliseconds
9868
+ */
9869
+ const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
9870
+ symbol,
9871
+ data,
9872
+ currentPrice,
9873
+ level,
9874
+ backtest,
9875
+ timestamp,
9876
+ });
9877
+ /**
9878
+ * Connection service for partial profit/loss tracking.
9879
+ *
9880
+ * Provides memoized ClientPartial instances per signal ID.
9881
+ * Acts as factory and lifetime manager for ClientPartial objects.
9882
+ *
9883
+ * Features:
9884
+ * - Creates one ClientPartial instance per signal ID (memoized)
9885
+ * - Configures instances with logger and event emitter callbacks
9886
+ * - Delegates profit/loss/clear operations to appropriate ClientPartial
9887
+ * - Cleans up memoized instances when signals are cleared
9888
+ *
9889
+ * Architecture:
9890
+ * - Injected into ClientStrategy via PartialGlobalService
9891
+ * - Uses memoize from functools-kit for instance caching
9892
+ * - Emits events to partialProfitSubject/partialLossSubject
9893
+ *
9894
+ * @example
9895
+ * ```typescript
9896
+ * // Service injected via DI
9897
+ * const service = inject<PartialConnectionService>(TYPES.partialConnectionService);
9898
+ *
9899
+ * // Called by ClientStrategy during signal monitoring
9900
+ * await service.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
9901
+ * // Creates or reuses ClientPartial for signal.id
9902
+ * // Delegates to ClientPartial.profit()
9903
+ *
9904
+ * // When signal closes
9905
+ * await service.clear("BTCUSDT", signal, 52000);
9906
+ * // Clears signal state and removes memoized instance
9907
+ * ```
9908
+ */
9909
+ class PartialConnectionService {
9910
+ constructor() {
9911
+ /**
9912
+ * Logger service injected from DI container.
9913
+ */
9914
+ this.loggerService = inject(TYPES.loggerService);
9915
+ /**
9916
+ * Memoized factory function for ClientPartial instances.
9917
+ *
9918
+ * Creates one ClientPartial per signal ID with configured callbacks.
9919
+ * Instances are cached until clear() is called.
9920
+ *
9921
+ * Key format: signalId
9922
+ * Value: ClientPartial instance with logger and event emitters
9923
+ */
9924
+ this.getPartial = memoize(([signalId]) => `${signalId}`, () => {
9925
+ return new ClientPartial({
9926
+ logger: this.loggerService,
9927
+ onProfit: COMMIT_PROFIT_FN,
9928
+ onLoss: COMMIT_LOSS_FN,
9929
+ });
9930
+ });
9931
+ /**
9932
+ * Processes profit state and emits events for newly reached profit levels.
9933
+ *
9934
+ * Retrieves or creates ClientPartial for signal ID, initializes it if needed,
9935
+ * then delegates to ClientPartial.profit() method.
9936
+ *
9937
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9938
+ * @param data - Signal row data
9939
+ * @param currentPrice - Current market price
9940
+ * @param revenuePercent - Current profit percentage (positive value)
9941
+ * @param backtest - True if backtest mode, false if live mode
9942
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9943
+ * @returns Promise that resolves when profit processing is complete
9944
+ */
9945
+ this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
9946
+ this.loggerService.log("partialConnectionService profit", {
9947
+ symbol,
9948
+ data,
9949
+ currentPrice,
9950
+ revenuePercent,
9951
+ backtest,
9952
+ when,
9953
+ });
9954
+ const partial = this.getPartial(data.id);
9955
+ await partial.waitForInit(symbol);
9956
+ return await partial.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
9957
+ };
9958
+ /**
9959
+ * Processes loss state and emits events for newly reached loss levels.
9960
+ *
9961
+ * Retrieves or creates ClientPartial for signal ID, initializes it if needed,
9962
+ * then delegates to ClientPartial.loss() method.
9963
+ *
9964
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9965
+ * @param data - Signal row data
9966
+ * @param currentPrice - Current market price
9967
+ * @param lossPercent - Current loss percentage (negative value)
9968
+ * @param backtest - True if backtest mode, false if live mode
9969
+ * @param when - Event timestamp (current time for live, candle time for backtest)
9970
+ * @returns Promise that resolves when loss processing is complete
9971
+ */
9972
+ this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
9973
+ this.loggerService.log("partialConnectionService loss", {
9974
+ symbol,
9975
+ data,
9976
+ currentPrice,
9977
+ lossPercent,
9978
+ backtest,
9979
+ when,
9980
+ });
9981
+ const partial = this.getPartial(data.id);
9982
+ await partial.waitForInit(symbol);
9983
+ return await partial.loss(symbol, data, currentPrice, lossPercent, backtest, when);
9984
+ };
9985
+ /**
9986
+ * Clears partial profit/loss state when signal closes.
9987
+ *
9988
+ * Retrieves ClientPartial for signal ID, initializes if needed,
9989
+ * delegates clear operation, then removes memoized instance.
9990
+ *
9991
+ * Sequence:
9992
+ * 1. Get ClientPartial from memoize cache
9993
+ * 2. Ensure initialization (waitForInit)
9994
+ * 3. Call ClientPartial.clear() - removes state, persists to disk
9995
+ * 4. Clear memoized instance - prevents memory leaks
9996
+ *
9997
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
9998
+ * @param data - Signal row data
9999
+ * @param priceClose - Final closing price
10000
+ * @returns Promise that resolves when clear is complete
10001
+ */
10002
+ this.clear = async (symbol, data, priceClose) => {
10003
+ this.loggerService.log("partialConnectionService profit", {
10004
+ symbol,
10005
+ data,
10006
+ priceClose,
10007
+ });
10008
+ const partial = this.getPartial(data.id);
10009
+ await partial.waitForInit(symbol);
10010
+ await partial.clear(symbol, data, priceClose);
10011
+ this.getPartial.clear(data.id);
10012
+ };
10013
+ }
10014
+ }
10015
+
10016
+ const columns = [
10017
+ {
10018
+ key: "action",
10019
+ label: "Action",
10020
+ format: (data) => data.action.toUpperCase(),
10021
+ },
10022
+ {
10023
+ key: "symbol",
10024
+ label: "Symbol",
10025
+ format: (data) => data.symbol,
10026
+ },
10027
+ {
10028
+ key: "signalId",
10029
+ label: "Signal ID",
10030
+ format: (data) => data.signalId,
10031
+ },
10032
+ {
10033
+ key: "position",
10034
+ label: "Position",
10035
+ format: (data) => data.position.toUpperCase(),
10036
+ },
10037
+ {
10038
+ key: "level",
10039
+ label: "Level %",
10040
+ format: (data) => data.action === "profit" ? `+${data.level}%` : `-${data.level}%`,
10041
+ },
10042
+ {
10043
+ key: "currentPrice",
10044
+ label: "Current Price",
10045
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
10046
+ },
10047
+ {
10048
+ key: "timestamp",
10049
+ label: "Timestamp",
10050
+ format: (data) => new Date(data.timestamp).toISOString(),
10051
+ },
10052
+ {
10053
+ key: "mode",
10054
+ label: "Mode",
10055
+ format: (data) => (data.backtest ? "Backtest" : "Live"),
10056
+ },
10057
+ ];
10058
+ /** Maximum number of events to store in partial reports */
10059
+ const MAX_EVENTS = 250;
10060
+ /**
10061
+ * Storage class for accumulating partial profit/loss events per symbol.
10062
+ * Maintains a chronological list of profit and loss level events.
10063
+ */
10064
+ class ReportStorage {
10065
+ constructor() {
10066
+ /** Internal list of all partial events for this symbol */
10067
+ this._eventList = [];
10068
+ }
10069
+ /**
10070
+ * Adds a profit event to the storage.
10071
+ *
10072
+ * @param symbol - Trading pair symbol
10073
+ * @param data - Signal row data
10074
+ * @param currentPrice - Current market price
10075
+ * @param level - Profit level reached
10076
+ * @param backtest - True if backtest mode
10077
+ */
10078
+ addProfitEvent(symbol, data, currentPrice, level, backtest, timestamp) {
10079
+ this._eventList.push({
10080
+ timestamp,
10081
+ action: "profit",
10082
+ symbol,
10083
+ signalId: data.id,
10084
+ position: data.position,
10085
+ currentPrice,
10086
+ level,
10087
+ backtest,
10088
+ });
10089
+ // Trim queue if exceeded MAX_EVENTS
10090
+ if (this._eventList.length > MAX_EVENTS) {
10091
+ this._eventList.shift();
10092
+ }
10093
+ }
10094
+ /**
10095
+ * Adds a loss event to the storage.
10096
+ *
10097
+ * @param symbol - Trading pair symbol
10098
+ * @param data - Signal row data
10099
+ * @param currentPrice - Current market price
10100
+ * @param level - Loss level reached
10101
+ * @param backtest - True if backtest mode
10102
+ */
10103
+ addLossEvent(symbol, data, currentPrice, level, backtest, timestamp) {
10104
+ this._eventList.push({
10105
+ timestamp,
10106
+ action: "loss",
10107
+ symbol,
10108
+ signalId: data.id,
10109
+ position: data.position,
10110
+ currentPrice,
10111
+ level,
10112
+ backtest,
10113
+ });
10114
+ // Trim queue if exceeded MAX_EVENTS
10115
+ if (this._eventList.length > MAX_EVENTS) {
10116
+ this._eventList.shift();
10117
+ }
10118
+ }
10119
+ /**
10120
+ * Calculates statistical data from partial profit/loss events (Controller).
10121
+ *
10122
+ * @returns Statistical data (empty object if no events)
10123
+ */
10124
+ async getData() {
10125
+ if (this._eventList.length === 0) {
10126
+ return {
10127
+ eventList: [],
10128
+ totalEvents: 0,
10129
+ totalProfit: 0,
10130
+ totalLoss: 0,
10131
+ };
10132
+ }
10133
+ const profitEvents = this._eventList.filter((e) => e.action === "profit");
10134
+ const lossEvents = this._eventList.filter((e) => e.action === "loss");
10135
+ return {
10136
+ eventList: this._eventList,
10137
+ totalEvents: this._eventList.length,
10138
+ totalProfit: profitEvents.length,
10139
+ totalLoss: lossEvents.length,
10140
+ };
10141
+ }
10142
+ /**
10143
+ * Generates markdown report with all partial events for a symbol (View).
10144
+ *
10145
+ * @param symbol - Trading pair symbol
10146
+ * @returns Markdown formatted report with all events
10147
+ */
10148
+ async getReport(symbol) {
10149
+ const stats = await this.getData();
10150
+ if (stats.totalEvents === 0) {
10151
+ return str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", "No partial profit/loss events recorded yet.");
10152
+ }
10153
+ const header = columns.map((col) => col.label);
10154
+ const separator = columns.map(() => "---");
10155
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10156
+ const tableData = [header, separator, ...rows];
10157
+ const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
10158
+ return str.newline(`# Partial Profit/Loss Report: ${symbol}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
10159
+ }
10160
+ /**
10161
+ * Saves symbol report to disk.
10162
+ *
10163
+ * @param symbol - Trading pair symbol
10164
+ * @param path - Directory path to save report (default: "./dump/partial")
10165
+ */
10166
+ async dump(symbol, path = "./dump/partial") {
10167
+ const markdown = await this.getReport(symbol);
10168
+ try {
10169
+ const dir = join(process.cwd(), path);
10170
+ await mkdir(dir, { recursive: true });
10171
+ const filename = `${symbol}.md`;
10172
+ const filepath = join(dir, filename);
10173
+ await writeFile(filepath, markdown, "utf-8");
10174
+ console.log(`Partial profit/loss report saved: ${filepath}`);
10175
+ }
10176
+ catch (error) {
10177
+ console.error(`Failed to save markdown report:`, error);
10178
+ }
10179
+ }
10180
+ }
10181
+ /**
10182
+ * Service for generating and saving partial profit/loss markdown reports.
10183
+ *
10184
+ * Features:
10185
+ * - Listens to partial profit and loss events via partialProfitSubject/partialLossSubject
10186
+ * - Accumulates all events (profit, loss) per symbol
10187
+ * - Generates markdown tables with detailed event information
10188
+ * - Provides statistics (total profit/loss events)
10189
+ * - Saves reports to disk in dump/partial/{symbol}.md
10190
+ *
10191
+ * @example
10192
+ * ```typescript
10193
+ * const service = new PartialMarkdownService();
10194
+ *
10195
+ * // Service automatically subscribes to subjects on init
10196
+ * // No manual callback setup needed
10197
+ *
10198
+ * // Later: generate and save report
10199
+ * await service.dump("BTCUSDT");
10200
+ * ```
10201
+ */
10202
+ class PartialMarkdownService {
10203
+ constructor() {
10204
+ /** Logger service for debug output */
10205
+ this.loggerService = inject(TYPES.loggerService);
10206
+ /**
10207
+ * Memoized function to get or create ReportStorage for a symbol.
10208
+ * Each symbol gets its own isolated storage instance.
10209
+ */
10210
+ this.getStorage = memoize(([symbol]) => `${symbol}`, () => new ReportStorage());
10211
+ /**
10212
+ * Processes profit events and accumulates them.
10213
+ * Should be called from partialProfitSubject subscription.
10214
+ *
10215
+ * @param data - Profit event data
10216
+ *
10217
+ * @example
10218
+ * ```typescript
10219
+ * const service = new PartialMarkdownService();
10220
+ * // Service automatically subscribes in init()
10221
+ * ```
10222
+ */
10223
+ this.tickProfit = async (data) => {
10224
+ this.loggerService.log("partialMarkdownService tickProfit", {
10225
+ data,
10226
+ });
10227
+ const storage = this.getStorage(data.symbol);
10228
+ storage.addProfitEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
10229
+ };
10230
+ /**
10231
+ * Processes loss events and accumulates them.
10232
+ * Should be called from partialLossSubject subscription.
10233
+ *
10234
+ * @param data - Loss event data
10235
+ *
10236
+ * @example
10237
+ * ```typescript
10238
+ * const service = new PartialMarkdownService();
10239
+ * // Service automatically subscribes in init()
10240
+ * ```
10241
+ */
10242
+ this.tickLoss = async (data) => {
10243
+ this.loggerService.log("partialMarkdownService tickLoss", {
10244
+ data,
10245
+ });
10246
+ const storage = this.getStorage(data.symbol);
10247
+ storage.addLossEvent(data.symbol, data.data, data.currentPrice, data.level, data.backtest, data.timestamp);
10248
+ };
10249
+ /**
10250
+ * Gets statistical data from all partial profit/loss events for a symbol.
10251
+ * Delegates to ReportStorage.getData().
10252
+ *
10253
+ * @param symbol - Trading pair symbol to get data for
10254
+ * @returns Statistical data object with all metrics
10255
+ *
10256
+ * @example
10257
+ * ```typescript
10258
+ * const service = new PartialMarkdownService();
10259
+ * const stats = await service.getData("BTCUSDT");
10260
+ * console.log(stats.totalProfit, stats.totalLoss);
10261
+ * ```
10262
+ */
10263
+ this.getData = async (symbol) => {
10264
+ this.loggerService.log("partialMarkdownService getData", {
10265
+ symbol,
10266
+ });
10267
+ const storage = this.getStorage(symbol);
10268
+ return storage.getData();
10269
+ };
10270
+ /**
10271
+ * Generates markdown report with all partial events for a symbol.
10272
+ * Delegates to ReportStorage.getReport().
10273
+ *
10274
+ * @param symbol - Trading pair symbol to generate report for
10275
+ * @returns Markdown formatted report string with table of all events
10276
+ *
10277
+ * @example
10278
+ * ```typescript
10279
+ * const service = new PartialMarkdownService();
10280
+ * const markdown = await service.getReport("BTCUSDT");
10281
+ * console.log(markdown);
10282
+ * ```
10283
+ */
10284
+ this.getReport = async (symbol) => {
10285
+ this.loggerService.log("partialMarkdownService getReport", {
10286
+ symbol,
10287
+ });
10288
+ const storage = this.getStorage(symbol);
10289
+ return storage.getReport(symbol);
10290
+ };
10291
+ /**
10292
+ * Saves symbol report to disk.
10293
+ * Creates directory if it doesn't exist.
10294
+ * Delegates to ReportStorage.dump().
10295
+ *
10296
+ * @param symbol - Trading pair symbol to save report for
10297
+ * @param path - Directory path to save report (default: "./dump/partial")
10298
+ *
10299
+ * @example
10300
+ * ```typescript
10301
+ * const service = new PartialMarkdownService();
10302
+ *
10303
+ * // Save to default path: ./dump/partial/BTCUSDT.md
10304
+ * await service.dump("BTCUSDT");
10305
+ *
10306
+ * // Save to custom path: ./custom/path/BTCUSDT.md
10307
+ * await service.dump("BTCUSDT", "./custom/path");
10308
+ * ```
10309
+ */
10310
+ this.dump = async (symbol, path = "./dump/partial") => {
10311
+ this.loggerService.log("partialMarkdownService dump", {
10312
+ symbol,
10313
+ path,
10314
+ });
10315
+ const storage = this.getStorage(symbol);
10316
+ await storage.dump(symbol, path);
10317
+ };
10318
+ /**
10319
+ * Clears accumulated event data from storage.
10320
+ * If symbol is provided, clears only that symbol's data.
10321
+ * If symbol is omitted, clears all symbols' data.
10322
+ *
10323
+ * @param symbol - Optional symbol to clear specific symbol data
10324
+ *
10325
+ * @example
10326
+ * ```typescript
10327
+ * const service = new PartialMarkdownService();
10328
+ *
10329
+ * // Clear specific symbol data
10330
+ * await service.clear("BTCUSDT");
10331
+ *
10332
+ * // Clear all symbols' data
10333
+ * await service.clear();
10334
+ * ```
10335
+ */
10336
+ this.clear = async (symbol) => {
10337
+ this.loggerService.log("partialMarkdownService clear", {
10338
+ symbol,
10339
+ });
10340
+ this.getStorage.clear(symbol);
10341
+ };
10342
+ /**
10343
+ * Initializes the service by subscribing to partial profit/loss events.
10344
+ * Uses singleshot to ensure initialization happens only once.
10345
+ * Automatically called on first use.
10346
+ *
10347
+ * @example
10348
+ * ```typescript
10349
+ * const service = new PartialMarkdownService();
10350
+ * await service.init(); // Subscribe to profit/loss events
10351
+ * ```
10352
+ */
10353
+ this.init = singleshot(async () => {
10354
+ this.loggerService.log("partialMarkdownService init");
10355
+ partialProfitSubject.subscribe(this.tickProfit);
10356
+ partialLossSubject.subscribe(this.tickLoss);
10357
+ });
10358
+ }
10359
+ }
10360
+
10361
+ /**
10362
+ * Global service for partial profit/loss tracking.
10363
+ *
10364
+ * Thin delegation layer that forwards operations to PartialConnectionService.
10365
+ * Provides centralized logging for all partial operations at the global level.
10366
+ *
10367
+ * Architecture:
10368
+ * - Injected into ClientStrategy constructor via IStrategyParams
10369
+ * - Delegates all operations to PartialConnectionService
10370
+ * - Logs operations at "partialGlobalService" level before delegation
10371
+ *
10372
+ * Purpose:
10373
+ * - Single injection point for ClientStrategy (dependency injection pattern)
10374
+ * - Centralized logging for monitoring partial operations
10375
+ * - Layer of abstraction between strategy and connection layer
10376
+ *
10377
+ * @example
10378
+ * ```typescript
10379
+ * // Service injected into ClientStrategy via DI
10380
+ * const strategy = new ClientStrategy({
10381
+ * partial: partialGlobalService,
10382
+ * ...
10383
+ * });
10384
+ *
10385
+ * // Called during signal monitoring
10386
+ * await strategy.params.partial.profit("BTCUSDT", signal, 55000, 10.0, false, new Date());
10387
+ * // Logs at global level → delegates to PartialConnectionService
10388
+ * ```
10389
+ */
10390
+ class PartialGlobalService {
10391
+ constructor() {
10392
+ /**
10393
+ * Logger service injected from DI container.
10394
+ * Used for logging operations at global service level.
10395
+ */
10396
+ this.loggerService = inject(TYPES.loggerService);
10397
+ /**
10398
+ * Connection service injected from DI container.
10399
+ * Handles actual ClientPartial instance creation and management.
10400
+ */
10401
+ this.partialConnectionService = inject(TYPES.partialConnectionService);
10402
+ /**
10403
+ * Processes profit state and emits events for newly reached profit levels.
10404
+ *
10405
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10406
+ *
10407
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10408
+ * @param data - Signal row data
10409
+ * @param currentPrice - Current market price
10410
+ * @param revenuePercent - Current profit percentage (positive value)
10411
+ * @param backtest - True if backtest mode, false if live mode
10412
+ * @param when - Event timestamp (current time for live, candle time for backtest)
10413
+ * @returns Promise that resolves when profit processing is complete
10414
+ */
10415
+ this.profit = async (symbol, data, currentPrice, revenuePercent, backtest, when) => {
10416
+ this.loggerService.log("partialGlobalService profit", {
10417
+ symbol,
10418
+ data,
10419
+ currentPrice,
10420
+ revenuePercent,
10421
+ backtest,
10422
+ when,
10423
+ });
10424
+ return await this.partialConnectionService.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
10425
+ };
10426
+ /**
10427
+ * Processes loss state and emits events for newly reached loss levels.
10428
+ *
10429
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10430
+ *
10431
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10432
+ * @param data - Signal row data
10433
+ * @param currentPrice - Current market price
10434
+ * @param lossPercent - Current loss percentage (negative value)
10435
+ * @param backtest - True if backtest mode, false if live mode
10436
+ * @param when - Event timestamp (current time for live, candle time for backtest)
10437
+ * @returns Promise that resolves when loss processing is complete
10438
+ */
10439
+ this.loss = async (symbol, data, currentPrice, lossPercent, backtest, when) => {
10440
+ this.loggerService.log("partialGlobalService loss", {
10441
+ symbol,
10442
+ data,
10443
+ currentPrice,
10444
+ lossPercent,
10445
+ backtest,
10446
+ when,
10447
+ });
10448
+ return await this.partialConnectionService.loss(symbol, data, currentPrice, lossPercent, backtest, when);
10449
+ };
10450
+ /**
10451
+ * Clears partial profit/loss state when signal closes.
10452
+ *
10453
+ * Logs operation at global service level, then delegates to PartialConnectionService.
10454
+ *
10455
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
10456
+ * @param data - Signal row data
10457
+ * @param priceClose - Final closing price
10458
+ * @returns Promise that resolves when clear is complete
10459
+ */
10460
+ this.clear = async (symbol, data, priceClose) => {
10461
+ this.loggerService.log("partialGlobalService profit", {
10462
+ symbol,
10463
+ data,
10464
+ priceClose,
10465
+ });
10466
+ return await this.partialConnectionService.clear(symbol, data, priceClose);
10467
+ };
10468
+ }
10469
+ }
10470
+
10471
+ {
10472
+ provide(TYPES.loggerService, () => new LoggerService());
10473
+ }
10474
+ {
10475
+ provide(TYPES.executionContextService, () => new ExecutionContextService());
10476
+ provide(TYPES.methodContextService, () => new MethodContextService());
10477
+ }
10478
+ {
10479
+ provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
10480
+ provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
10481
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
10482
+ provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
10483
+ provide(TYPES.riskConnectionService, () => new RiskConnectionService());
10484
+ provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
10485
+ provide(TYPES.partialConnectionService, () => new PartialConnectionService());
10486
+ }
10487
+ {
10488
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
10489
+ provide(TYPES.strategySchemaService, () => new StrategySchemaService());
10490
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
10491
+ provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
10492
+ provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
10493
+ provide(TYPES.riskSchemaService, () => new RiskSchemaService());
10494
+ provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
10495
+ }
10496
+ {
10497
+ provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
10498
+ provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
10499
+ provide(TYPES.frameGlobalService, () => new FrameGlobalService());
10500
+ provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
10501
+ provide(TYPES.riskGlobalService, () => new RiskGlobalService());
10502
+ provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
10503
+ provide(TYPES.partialGlobalService, () => new PartialGlobalService());
10504
+ }
10505
+ {
10506
+ provide(TYPES.liveCommandService, () => new LiveCommandService());
10507
+ provide(TYPES.backtestCommandService, () => new BacktestCommandService());
10508
+ provide(TYPES.walkerCommandService, () => new WalkerCommandService());
10509
+ }
10510
+ {
10511
+ provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
10512
+ provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
10513
+ provide(TYPES.walkerLogicPrivateService, () => new WalkerLogicPrivateService());
10514
+ }
10515
+ {
10516
+ provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
7908
10517
  provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
7909
10518
  provide(TYPES.walkerLogicPublicService, () => new WalkerLogicPublicService());
7910
10519
  }
@@ -7915,6 +10524,7 @@ class RiskValidationService {
7915
10524
  provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
7916
10525
  provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
7917
10526
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
10527
+ provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
7918
10528
  }
7919
10529
  {
7920
10530
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -7923,6 +10533,10 @@ class RiskValidationService {
7923
10533
  provide(TYPES.walkerValidationService, () => new WalkerValidationService());
7924
10534
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
7925
10535
  provide(TYPES.riskValidationService, () => new RiskValidationService());
10536
+ provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
10537
+ }
10538
+ {
10539
+ provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
7926
10540
  }
7927
10541
 
7928
10542
  const baseServices = {
@@ -7938,6 +10552,8 @@ const connectionServices = {
7938
10552
  frameConnectionService: inject(TYPES.frameConnectionService),
7939
10553
  sizingConnectionService: inject(TYPES.sizingConnectionService),
7940
10554
  riskConnectionService: inject(TYPES.riskConnectionService),
10555
+ optimizerConnectionService: inject(TYPES.optimizerConnectionService),
10556
+ partialConnectionService: inject(TYPES.partialConnectionService),
7941
10557
  };
7942
10558
  const schemaServices = {
7943
10559
  exchangeSchemaService: inject(TYPES.exchangeSchemaService),
@@ -7946,6 +10562,7 @@ const schemaServices = {
7946
10562
  walkerSchemaService: inject(TYPES.walkerSchemaService),
7947
10563
  sizingSchemaService: inject(TYPES.sizingSchemaService),
7948
10564
  riskSchemaService: inject(TYPES.riskSchemaService),
10565
+ optimizerSchemaService: inject(TYPES.optimizerSchemaService),
7949
10566
  };
7950
10567
  const globalServices = {
7951
10568
  exchangeGlobalService: inject(TYPES.exchangeGlobalService),
@@ -7953,6 +10570,8 @@ const globalServices = {
7953
10570
  frameGlobalService: inject(TYPES.frameGlobalService),
7954
10571
  sizingGlobalService: inject(TYPES.sizingGlobalService),
7955
10572
  riskGlobalService: inject(TYPES.riskGlobalService),
10573
+ optimizerGlobalService: inject(TYPES.optimizerGlobalService),
10574
+ partialGlobalService: inject(TYPES.partialGlobalService),
7956
10575
  };
7957
10576
  const commandServices = {
7958
10577
  liveCommandService: inject(TYPES.liveCommandService),
@@ -7976,6 +10595,7 @@ const markdownServices = {
7976
10595
  performanceMarkdownService: inject(TYPES.performanceMarkdownService),
7977
10596
  walkerMarkdownService: inject(TYPES.walkerMarkdownService),
7978
10597
  heatMarkdownService: inject(TYPES.heatMarkdownService),
10598
+ partialMarkdownService: inject(TYPES.partialMarkdownService),
7979
10599
  };
7980
10600
  const validationServices = {
7981
10601
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -7984,6 +10604,10 @@ const validationServices = {
7984
10604
  walkerValidationService: inject(TYPES.walkerValidationService),
7985
10605
  sizingValidationService: inject(TYPES.sizingValidationService),
7986
10606
  riskValidationService: inject(TYPES.riskValidationService),
10607
+ optimizerValidationService: inject(TYPES.optimizerValidationService),
10608
+ };
10609
+ const templateServices = {
10610
+ optimizerTemplateService: inject(TYPES.optimizerTemplateService),
7987
10611
  };
7988
10612
  const backtest = {
7989
10613
  ...baseServices,
@@ -7996,6 +10620,7 @@ const backtest = {
7996
10620
  ...logicPublicServices,
7997
10621
  ...markdownServices,
7998
10622
  ...validationServices,
10623
+ ...templateServices,
7999
10624
  };
8000
10625
  init();
8001
10626
  var backtest$1 = backtest;
@@ -8041,6 +10666,7 @@ const ADD_FRAME_METHOD_NAME = "add.addFrame";
8041
10666
  const ADD_WALKER_METHOD_NAME = "add.addWalker";
8042
10667
  const ADD_SIZING_METHOD_NAME = "add.addSizing";
8043
10668
  const ADD_RISK_METHOD_NAME = "add.addRisk";
10669
+ const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizer";
8044
10670
  /**
8045
10671
  * Registers a trading strategy in the framework.
8046
10672
  *
@@ -8329,8 +10955,102 @@ function addRisk(riskSchema) {
8329
10955
  backtest$1.loggerService.info(ADD_RISK_METHOD_NAME, {
8330
10956
  riskSchema,
8331
10957
  });
8332
- backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
8333
- backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
10958
+ backtest$1.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
10959
+ backtest$1.riskSchemaService.register(riskSchema.riskName, riskSchema);
10960
+ }
10961
+ /**
10962
+ * Registers an optimizer configuration in the framework.
10963
+ *
10964
+ * The optimizer generates trading strategies by:
10965
+ * - Collecting data from multiple sources across training periods
10966
+ * - Building LLM conversation history with fetched data
10967
+ * - Generating strategy prompts using getPrompt()
10968
+ * - Creating executable backtest code with templates
10969
+ *
10970
+ * The optimizer produces a complete .mjs file containing:
10971
+ * - Exchange, Frame, Strategy, and Walker configurations
10972
+ * - Multi-timeframe analysis logic
10973
+ * - LLM integration for signal generation
10974
+ * - Event listeners for progress tracking
10975
+ *
10976
+ * @param optimizerSchema - Optimizer configuration object
10977
+ * @param optimizerSchema.optimizerName - Unique optimizer identifier
10978
+ * @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
10979
+ * @param optimizerSchema.rangeTest - Testing time range for strategy validation
10980
+ * @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
10981
+ * @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
10982
+ * @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
10983
+ * @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
10984
+ *
10985
+ * @example
10986
+ * ```typescript
10987
+ * // Basic optimizer with single data source
10988
+ * addOptimizer({
10989
+ * optimizerName: "llm-strategy-generator",
10990
+ * rangeTrain: [
10991
+ * {
10992
+ * note: "Bull market period",
10993
+ * startDate: new Date("2024-01-01"),
10994
+ * endDate: new Date("2024-01-31"),
10995
+ * },
10996
+ * {
10997
+ * note: "Bear market period",
10998
+ * startDate: new Date("2024-02-01"),
10999
+ * endDate: new Date("2024-02-28"),
11000
+ * },
11001
+ * ],
11002
+ * rangeTest: {
11003
+ * note: "Validation period",
11004
+ * startDate: new Date("2024-03-01"),
11005
+ * endDate: new Date("2024-03-31"),
11006
+ * },
11007
+ * source: [
11008
+ * {
11009
+ * name: "historical-backtests",
11010
+ * fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
11011
+ * // Fetch historical backtest results from database
11012
+ * return await db.backtests.find({
11013
+ * symbol,
11014
+ * date: { $gte: startDate, $lte: endDate },
11015
+ * })
11016
+ * .skip(offset)
11017
+ * .limit(limit);
11018
+ * },
11019
+ * user: async (symbol, data, name) => {
11020
+ * return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
11021
+ * },
11022
+ * assistant: async (symbol, data, name) => {
11023
+ * return "Historical data analyzed successfully";
11024
+ * },
11025
+ * },
11026
+ * ],
11027
+ * getPrompt: async (symbol, messages) => {
11028
+ * // Generate strategy prompt from conversation
11029
+ * return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
11030
+ * },
11031
+ * callbacks: {
11032
+ * onData: (symbol, strategyData) => {
11033
+ * console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
11034
+ * },
11035
+ * onCode: (symbol, code) => {
11036
+ * console.log(`Generated ${code.length} characters of code for ${symbol}`);
11037
+ * },
11038
+ * onDump: (symbol, filepath) => {
11039
+ * console.log(`Saved strategy to ${filepath}`);
11040
+ * },
11041
+ * onSourceData: (symbol, sourceName, data, startDate, endDate) => {
11042
+ * console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
11043
+ * },
11044
+ * },
11045
+ * });
11046
+ * ```
11047
+ */
11048
+ function addOptimizer(optimizerSchema) {
11049
+ backtest$1.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
11050
+ optimizerSchema,
11051
+ });
11052
+ backtest$1.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
11053
+ backtest$1.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
8334
11054
  }
8335
11055
 
8336
11056
  const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
@@ -8339,6 +11059,7 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
8339
11059
  const LIST_WALKERS_METHOD_NAME = "list.listWalkers";
8340
11060
  const LIST_SIZINGS_METHOD_NAME = "list.listSizings";
8341
11061
  const LIST_RISKS_METHOD_NAME = "list.listRisks";
11062
+ const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizers";
8342
11063
  /**
8343
11064
  * Returns a list of all registered exchange schemas.
8344
11065
  *
@@ -8536,6 +11257,46 @@ async function listRisks() {
8536
11257
  backtest$1.loggerService.log(LIST_RISKS_METHOD_NAME);
8537
11258
  return await backtest$1.riskValidationService.list();
8538
11259
  }
11260
+ /**
11261
+ * Returns a list of all registered optimizer schemas.
11262
+ *
11263
+ * Retrieves all optimizers that have been registered via addOptimizer().
11264
+ * Useful for debugging, documentation, or building dynamic UIs.
11265
+ *
11266
+ * @returns Array of optimizer schemas with their configurations
11267
+ *
11268
+ * @example
11269
+ * ```typescript
11270
+ * import { listOptimizers, addOptimizer } from "backtest-kit";
11271
+ *
11272
+ * addOptimizer({
11273
+ * optimizerName: "llm-strategy-generator",
11274
+ * note: "Generates trading strategies using LLM",
11275
+ * rangeTrain: [
11276
+ * {
11277
+ * note: "Training period 1",
11278
+ * startDate: new Date("2024-01-01"),
11279
+ * endDate: new Date("2024-01-31"),
11280
+ * },
11281
+ * ],
11282
+ * rangeTest: {
11283
+ * note: "Testing period",
11284
+ * startDate: new Date("2024-02-01"),
11285
+ * endDate: new Date("2024-02-28"),
11286
+ * },
11287
+ * source: [],
11288
+ * getPrompt: async (symbol, messages) => "Generate strategy",
11289
+ * });
11290
+ *
11291
+ * const optimizers = listOptimizers();
11292
+ * console.log(optimizers);
11293
+ * // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
11294
+ * ```
11295
+ */
11296
+ async function listOptimizers() {
11297
+ backtest$1.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
11298
+ return await backtest$1.optimizerValidationService.list();
11299
+ }
8539
11300
 
8540
11301
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
8541
11302
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
@@ -8550,12 +11311,17 @@ const LISTEN_DONE_BACKTEST_METHOD_NAME = "event.listenDoneBacktest";
8550
11311
  const LISTEN_DONE_BACKTEST_ONCE_METHOD_NAME = "event.listenDoneBacktestOnce";
8551
11312
  const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
8552
11313
  const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
8553
- const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
11314
+ const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
11315
+ const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
8554
11316
  const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
8555
11317
  const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
8556
11318
  const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
8557
11319
  const LISTEN_WALKER_COMPLETE_METHOD_NAME = "event.listenWalkerComplete";
8558
11320
  const LISTEN_VALIDATION_METHOD_NAME = "event.listenValidation";
11321
+ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
11322
+ const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
11323
+ const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
11324
+ const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
8559
11325
  /**
8560
11326
  * Subscribes to all signal events with queued async processing.
8561
11327
  *
@@ -8941,9 +11707,9 @@ function listenDoneWalkerOnce(filterFn, fn) {
8941
11707
  *
8942
11708
  * @example
8943
11709
  * ```typescript
8944
- * import { listenProgress, Backtest } from "backtest-kit";
11710
+ * import { listenBacktestProgress, Backtest } from "backtest-kit";
8945
11711
  *
8946
- * const unsubscribe = listenProgress((event) => {
11712
+ * const unsubscribe = listenBacktestProgress((event) => {
8947
11713
  * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
8948
11714
  * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
8949
11715
  * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
@@ -8959,9 +11725,43 @@ function listenDoneWalkerOnce(filterFn, fn) {
8959
11725
  * unsubscribe();
8960
11726
  * ```
8961
11727
  */
8962
- function listenProgress(fn) {
11728
+ function listenBacktestProgress(fn) {
8963
11729
  backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
8964
- return progressEmitter.subscribe(queued(async (event) => fn(event)));
11730
+ return progressBacktestEmitter.subscribe(queued(async (event) => fn(event)));
11731
+ }
11732
+ /**
11733
+ * Subscribes to walker progress events with queued async processing.
11734
+ *
11735
+ * Emits during Walker.run() execution after each strategy completes.
11736
+ * Events are processed sequentially in order received, even if callback is async.
11737
+ * Uses queued wrapper to prevent concurrent execution of the callback.
11738
+ *
11739
+ * @param fn - Callback function to handle walker progress events
11740
+ * @returns Unsubscribe function to stop listening to events
11741
+ *
11742
+ * @example
11743
+ * ```typescript
11744
+ * import { listenWalkerProgress, Walker } from "backtest-kit";
11745
+ *
11746
+ * const unsubscribe = listenWalkerProgress((event) => {
11747
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
11748
+ * console.log(`${event.processedStrategies} / ${event.totalStrategies} strategies`);
11749
+ * console.log(`Walker: ${event.walkerName}, Symbol: ${event.symbol}`);
11750
+ * });
11751
+ *
11752
+ * Walker.run("BTCUSDT", {
11753
+ * walkerName: "my-walker",
11754
+ * exchangeName: "binance",
11755
+ * frameName: "1d-backtest"
11756
+ * });
11757
+ *
11758
+ * // Later: stop listening
11759
+ * unsubscribe();
11760
+ * ```
11761
+ */
11762
+ function listenWalkerProgress(fn) {
11763
+ backtest$1.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
11764
+ return progressWalkerEmitter.subscribe(queued(async (event) => fn(event)));
8965
11765
  }
8966
11766
  /**
8967
11767
  * Subscribes to performance metric events with queued async processing.
@@ -9139,6 +11939,130 @@ function listenValidation(fn) {
9139
11939
  backtest$1.loggerService.log(LISTEN_VALIDATION_METHOD_NAME);
9140
11940
  return validationSubject.subscribe(queued(async (error) => fn(error)));
9141
11941
  }
11942
+ /**
11943
+ * Subscribes to partial profit level events with queued async processing.
11944
+ *
11945
+ * Emits when a signal reaches a profit level milestone (10%, 20%, 30%, etc).
11946
+ * Events are processed sequentially in order received, even if callback is async.
11947
+ * Uses queued wrapper to prevent concurrent execution of the callback.
11948
+ *
11949
+ * @param fn - Callback function to handle partial profit events
11950
+ * @returns Unsubscribe function to stop listening to events
11951
+ *
11952
+ * @example
11953
+ * ```typescript
11954
+ * import { listenPartialProfit } from "./function/event";
11955
+ *
11956
+ * const unsubscribe = listenPartialProfit((event) => {
11957
+ * console.log(`Signal ${event.data.id} reached ${event.level}% profit`);
11958
+ * console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
11959
+ * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
11960
+ * });
11961
+ *
11962
+ * // Later: stop listening
11963
+ * unsubscribe();
11964
+ * ```
11965
+ */
11966
+ function listenPartialProfit(fn) {
11967
+ backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_METHOD_NAME);
11968
+ return partialProfitSubject.subscribe(queued(async (event) => fn(event)));
11969
+ }
11970
+ /**
11971
+ * Subscribes to filtered partial profit level events with one-time execution.
11972
+ *
11973
+ * Listens for events matching the filter predicate, then executes callback once
11974
+ * and automatically unsubscribes. Useful for waiting for specific profit conditions.
11975
+ *
11976
+ * @param filterFn - Predicate to filter which events trigger the callback
11977
+ * @param fn - Callback function to handle the filtered event (called only once)
11978
+ * @returns Unsubscribe function to cancel the listener before it fires
11979
+ *
11980
+ * @example
11981
+ * ```typescript
11982
+ * import { listenPartialProfitOnce } from "./function/event";
11983
+ *
11984
+ * // Wait for first 50% profit level on any signal
11985
+ * listenPartialProfitOnce(
11986
+ * (event) => event.level === 50,
11987
+ * (event) => console.log("50% profit reached:", event.data.id)
11988
+ * );
11989
+ *
11990
+ * // Wait for 30% profit on BTCUSDT
11991
+ * const cancel = listenPartialProfitOnce(
11992
+ * (event) => event.symbol === "BTCUSDT" && event.level === 30,
11993
+ * (event) => console.log("BTCUSDT hit 30% profit")
11994
+ * );
11995
+ *
11996
+ * // Cancel if needed before event fires
11997
+ * cancel();
11998
+ * ```
11999
+ */
12000
+ function listenPartialProfitOnce(filterFn, fn) {
12001
+ backtest$1.loggerService.log(LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME);
12002
+ return partialProfitSubject.filter(filterFn).once(fn);
12003
+ }
12004
+ /**
12005
+ * Subscribes to partial loss level events with queued async processing.
12006
+ *
12007
+ * Emits when a signal reaches a loss level milestone (10%, 20%, 30%, etc).
12008
+ * Events are processed sequentially in order received, even if callback is async.
12009
+ * Uses queued wrapper to prevent concurrent execution of the callback.
12010
+ *
12011
+ * @param fn - Callback function to handle partial loss events
12012
+ * @returns Unsubscribe function to stop listening to events
12013
+ *
12014
+ * @example
12015
+ * ```typescript
12016
+ * import { listenPartialLoss } from "./function/event";
12017
+ *
12018
+ * const unsubscribe = listenPartialLoss((event) => {
12019
+ * console.log(`Signal ${event.data.id} reached ${event.level}% loss`);
12020
+ * console.log(`Symbol: ${event.symbol}, Price: ${event.currentPrice}`);
12021
+ * console.log(`Mode: ${event.backtest ? "Backtest" : "Live"}`);
12022
+ * });
12023
+ *
12024
+ * // Later: stop listening
12025
+ * unsubscribe();
12026
+ * ```
12027
+ */
12028
+ function listenPartialLoss(fn) {
12029
+ backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_METHOD_NAME);
12030
+ return partialLossSubject.subscribe(queued(async (event) => fn(event)));
12031
+ }
12032
+ /**
12033
+ * Subscribes to filtered partial loss level events with one-time execution.
12034
+ *
12035
+ * Listens for events matching the filter predicate, then executes callback once
12036
+ * and automatically unsubscribes. Useful for waiting for specific loss conditions.
12037
+ *
12038
+ * @param filterFn - Predicate to filter which events trigger the callback
12039
+ * @param fn - Callback function to handle the filtered event (called only once)
12040
+ * @returns Unsubscribe function to cancel the listener before it fires
12041
+ *
12042
+ * @example
12043
+ * ```typescript
12044
+ * import { listenPartialLossOnce } from "./function/event";
12045
+ *
12046
+ * // Wait for first 20% loss level on any signal
12047
+ * listenPartialLossOnce(
12048
+ * (event) => event.level === 20,
12049
+ * (event) => console.log("20% loss reached:", event.data.id)
12050
+ * );
12051
+ *
12052
+ * // Wait for 10% loss on ETHUSDT in live mode
12053
+ * const cancel = listenPartialLossOnce(
12054
+ * (event) => event.symbol === "ETHUSDT" && event.level === 10 && !event.backtest,
12055
+ * (event) => console.log("ETHUSDT hit 10% loss in live mode")
12056
+ * );
12057
+ *
12058
+ * // Cancel if needed before event fires
12059
+ * cancel();
12060
+ * ```
12061
+ */
12062
+ function listenPartialLossOnce(filterFn, fn) {
12063
+ backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
12064
+ return partialLossSubject.filter(filterFn).once(fn);
12065
+ }
9142
12066
 
9143
12067
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
9144
12068
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -9435,11 +12359,11 @@ class BacktestUtils {
9435
12359
  * Saves strategy report to disk.
9436
12360
  *
9437
12361
  * @param strategyName - Strategy name to save report for
9438
- * @param path - Optional directory path to save report (default: "./logs/backtest")
12362
+ * @param path - Optional directory path to save report (default: "./dump/backtest")
9439
12363
  *
9440
12364
  * @example
9441
12365
  * ```typescript
9442
- * // Save to default path: ./logs/backtest/my-strategy.md
12366
+ * // Save to default path: ./dump/backtest/my-strategy.md
9443
12367
  * await Backtest.dump("my-strategy");
9444
12368
  *
9445
12369
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9644,11 +12568,11 @@ class LiveUtils {
9644
12568
  * Saves strategy report to disk.
9645
12569
  *
9646
12570
  * @param strategyName - Strategy name to save report for
9647
- * @param path - Optional directory path to save report (default: "./logs/live")
12571
+ * @param path - Optional directory path to save report (default: "./dump/live")
9648
12572
  *
9649
12573
  * @example
9650
12574
  * ```typescript
9651
- * // Save to default path: ./logs/live/my-strategy.md
12575
+ * // Save to default path: ./dump/live/my-strategy.md
9652
12576
  * await Live.dump("my-strategy");
9653
12577
  *
9654
12578
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9684,7 +12608,6 @@ const Live = new LiveUtils();
9684
12608
  const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
9685
12609
  const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
9686
12610
  const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
9687
- const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
9688
12611
  /**
9689
12612
  * Utility class for scheduled signals reporting operations.
9690
12613
  *
@@ -9752,11 +12675,11 @@ class ScheduleUtils {
9752
12675
  * Saves strategy report to disk.
9753
12676
  *
9754
12677
  * @param strategyName - Strategy name to save report for
9755
- * @param path - Optional directory path to save report (default: "./logs/schedule")
12678
+ * @param path - Optional directory path to save report (default: "./dump/schedule")
9756
12679
  *
9757
12680
  * @example
9758
12681
  * ```typescript
9759
- * // Save to default path: ./logs/schedule/my-strategy.md
12682
+ * // Save to default path: ./dump/schedule/my-strategy.md
9760
12683
  * await Schedule.dump("my-strategy");
9761
12684
  *
9762
12685
  * // Save to custom path: ./custom/path/my-strategy.md
@@ -9770,28 +12693,6 @@ class ScheduleUtils {
9770
12693
  });
9771
12694
  await backtest$1.scheduleMarkdownService.dump(strategyName, path);
9772
12695
  };
9773
- /**
9774
- * Clears accumulated scheduled signal data from storage.
9775
- * If strategyName is provided, clears only that strategy's data.
9776
- * If strategyName is omitted, clears all strategies' data.
9777
- *
9778
- * @param strategyName - Optional strategy name to clear specific strategy data
9779
- *
9780
- * @example
9781
- * ```typescript
9782
- * // Clear specific strategy data
9783
- * await Schedule.clear("my-strategy");
9784
- *
9785
- * // Clear all strategies' data
9786
- * await Schedule.clear();
9787
- * ```
9788
- */
9789
- this.clear = async (strategyName) => {
9790
- backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
9791
- strategyName,
9792
- });
9793
- await backtest$1.scheduleMarkdownService.clear(strategyName);
9794
- };
9795
12696
  }
9796
12697
  }
9797
12698
  /**
@@ -9898,21 +12799,21 @@ class Performance {
9898
12799
  * Saves performance report to disk.
9899
12800
  *
9900
12801
  * Creates directory if it doesn't exist.
9901
- * Default path: ./logs/performance/{strategyName}.md
12802
+ * Default path: ./dump/performance/{strategyName}.md
9902
12803
  *
9903
12804
  * @param strategyName - Strategy name to save report for
9904
12805
  * @param path - Optional custom directory path
9905
12806
  *
9906
12807
  * @example
9907
12808
  * ```typescript
9908
- * // Save to default path: ./logs/performance/my-strategy.md
12809
+ * // Save to default path: ./dump/performance/my-strategy.md
9909
12810
  * await Performance.dump("my-strategy");
9910
12811
  *
9911
12812
  * // Save to custom path: ./reports/perf/my-strategy.md
9912
12813
  * await Performance.dump("my-strategy", "./reports/perf");
9913
12814
  * ```
9914
12815
  */
9915
- static async dump(strategyName, path = "./logs/performance") {
12816
+ static async dump(strategyName, path = "./dump/performance") {
9916
12817
  return backtest$1.performanceMarkdownService.dump(strategyName, path);
9917
12818
  }
9918
12819
  /**
@@ -10114,11 +13015,11 @@ class WalkerUtils {
10114
13015
  *
10115
13016
  * @param symbol - Trading symbol
10116
13017
  * @param walkerName - Walker name to save report for
10117
- * @param path - Optional directory path to save report (default: "./logs/walker")
13018
+ * @param path - Optional directory path to save report (default: "./dump/walker")
10118
13019
  *
10119
13020
  * @example
10120
13021
  * ```typescript
10121
- * // Save to default path: ./logs/walker/my-walker.md
13022
+ * // Save to default path: ./dump/walker/my-walker.md
10122
13023
  * await Walker.dump("BTCUSDT", "my-walker");
10123
13024
  *
10124
13025
  * // Save to custom path: ./custom/path/my-walker.md
@@ -10247,11 +13148,11 @@ class HeatUtils {
10247
13148
  * Default filename: {strategyName}.md
10248
13149
  *
10249
13150
  * @param strategyName - Strategy name to save heatmap report for
10250
- * @param path - Optional directory path to save report (default: "./logs/heatmap")
13151
+ * @param path - Optional directory path to save report (default: "./dump/heatmap")
10251
13152
  *
10252
13153
  * @example
10253
13154
  * ```typescript
10254
- * // Save to default path: ./logs/heatmap/my-strategy.md
13155
+ * // Save to default path: ./dump/heatmap/my-strategy.md
10255
13156
  * await Heat.dump("my-strategy");
10256
13157
  *
10257
13158
  * // Save to custom path: ./reports/my-strategy.md
@@ -10414,4 +13315,258 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
10414
13315
  };
10415
13316
  const PositionSize = PositionSizeUtils;
10416
13317
 
10417
- export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Performance, PersistBase, PersistRiskAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listRisks, listSizings, listStrategies, listWalkers, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPerformance, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, setConfig, setLogger };
13318
+ const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
13319
+ const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
13320
+ const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
13321
+ /**
13322
+ * Public API utilities for optimizer operations.
13323
+ * Provides high-level methods for strategy generation and code export.
13324
+ *
13325
+ * Usage:
13326
+ * ```typescript
13327
+ * import { Optimizer } from "backtest-kit";
13328
+ *
13329
+ * // Get strategy data
13330
+ * const strategies = await Optimizer.getData("BTCUSDT", {
13331
+ * optimizerName: "my-optimizer"
13332
+ * });
13333
+ *
13334
+ * // Generate code
13335
+ * const code = await Optimizer.getCode("BTCUSDT", {
13336
+ * optimizerName: "my-optimizer"
13337
+ * });
13338
+ *
13339
+ * // Save to file
13340
+ * await Optimizer.dump("BTCUSDT", {
13341
+ * optimizerName: "my-optimizer"
13342
+ * }, "./output");
13343
+ * ```
13344
+ */
13345
+ class OptimizerUtils {
13346
+ constructor() {
13347
+ /**
13348
+ * Fetches data from all sources and generates strategy metadata.
13349
+ * Processes each training range and builds LLM conversation history.
13350
+ *
13351
+ * @param symbol - Trading pair symbol
13352
+ * @param context - Context with optimizerName
13353
+ * @returns Array of generated strategies with conversation context
13354
+ * @throws Error if optimizer not found
13355
+ */
13356
+ this.getData = async (symbol, context) => {
13357
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
13358
+ symbol,
13359
+ context,
13360
+ });
13361
+ return await backtest$1.optimizerGlobalService.getData(symbol, context.optimizerName);
13362
+ };
13363
+ /**
13364
+ * Generates complete executable strategy code.
13365
+ * Includes imports, helpers, strategies, walker, and launcher.
13366
+ *
13367
+ * @param symbol - Trading pair symbol
13368
+ * @param context - Context with optimizerName
13369
+ * @returns Generated TypeScript/JavaScript code as string
13370
+ * @throws Error if optimizer not found
13371
+ */
13372
+ this.getCode = async (symbol, context) => {
13373
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
13374
+ symbol,
13375
+ context,
13376
+ });
13377
+ return await backtest$1.optimizerGlobalService.getCode(symbol, context.optimizerName);
13378
+ };
13379
+ /**
13380
+ * Generates and saves strategy code to file.
13381
+ * Creates directory if needed, writes .mjs file.
13382
+ *
13383
+ * Format: `{optimizerName}_{symbol}.mjs`
13384
+ *
13385
+ * @param symbol - Trading pair symbol
13386
+ * @param context - Context with optimizerName
13387
+ * @param path - Output directory path (default: "./")
13388
+ * @throws Error if optimizer not found or file write fails
13389
+ */
13390
+ this.dump = async (symbol, context, path) => {
13391
+ backtest$1.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
13392
+ symbol,
13393
+ context,
13394
+ path,
13395
+ });
13396
+ await backtest$1.optimizerGlobalService.dump(symbol, context.optimizerName, path);
13397
+ };
13398
+ }
13399
+ }
13400
+ /**
13401
+ * Singleton instance of OptimizerUtils.
13402
+ * Public API for optimizer operations.
13403
+ *
13404
+ * @example
13405
+ * ```typescript
13406
+ * import { Optimizer } from "backtest-kit";
13407
+ *
13408
+ * await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
13409
+ * ```
13410
+ */
13411
+ const Optimizer = new OptimizerUtils();
13412
+
13413
+ const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
13414
+ const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
13415
+ const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
13416
+ /**
13417
+ * Utility class for accessing partial profit/loss reports and statistics.
13418
+ *
13419
+ * Provides static-like methods (via singleton instance) to retrieve data
13420
+ * accumulated by PartialMarkdownService from partial profit/loss events.
13421
+ *
13422
+ * Features:
13423
+ * - Statistical data extraction (total profit/loss events count)
13424
+ * - Markdown report generation with event tables
13425
+ * - File export to disk
13426
+ *
13427
+ * Data source:
13428
+ * - PartialMarkdownService listens to partialProfitSubject/partialLossSubject
13429
+ * - Accumulates events in ReportStorage (max 250 events per symbol)
13430
+ * - Events include: timestamp, action, symbol, signalId, position, level, price, mode
13431
+ *
13432
+ * @example
13433
+ * ```typescript
13434
+ * import { Partial } from "./classes/Partial";
13435
+ *
13436
+ * // Get statistical data for BTCUSDT
13437
+ * const stats = await Partial.getData("BTCUSDT");
13438
+ * console.log(`Total events: ${stats.totalEvents}`);
13439
+ * console.log(`Profit events: ${stats.totalProfit}`);
13440
+ * console.log(`Loss events: ${stats.totalLoss}`);
13441
+ *
13442
+ * // Generate markdown report
13443
+ * const markdown = await Partial.getReport("BTCUSDT");
13444
+ * console.log(markdown); // Formatted table with all events
13445
+ *
13446
+ * // Export report to file
13447
+ * await Partial.dump("BTCUSDT"); // Saves to ./dump/partial/BTCUSDT.md
13448
+ * await Partial.dump("BTCUSDT", "./custom/path"); // Custom directory
13449
+ * ```
13450
+ */
13451
+ class PartialUtils {
13452
+ constructor() {
13453
+ /**
13454
+ * Retrieves statistical data from accumulated partial profit/loss events.
13455
+ *
13456
+ * Delegates to PartialMarkdownService.getData() which reads from ReportStorage.
13457
+ * Returns aggregated metrics calculated from all profit and loss events.
13458
+ *
13459
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13460
+ * @returns Promise resolving to PartialStatistics object with counts and event list
13461
+ *
13462
+ * @example
13463
+ * ```typescript
13464
+ * const stats = await Partial.getData("BTCUSDT");
13465
+ *
13466
+ * console.log(`Total events: ${stats.totalEvents}`);
13467
+ * console.log(`Profit events: ${stats.totalProfit} (${(stats.totalProfit / stats.totalEvents * 100).toFixed(1)}%)`);
13468
+ * console.log(`Loss events: ${stats.totalLoss} (${(stats.totalLoss / stats.totalEvents * 100).toFixed(1)}%)`);
13469
+ *
13470
+ * // Iterate through all events
13471
+ * for (const event of stats.eventList) {
13472
+ * console.log(`${event.action.toUpperCase()}: Signal ${event.signalId} reached ${event.level}%`);
13473
+ * }
13474
+ * ```
13475
+ */
13476
+ this.getData = async (symbol) => {
13477
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol });
13478
+ return await backtest$1.partialMarkdownService.getData(symbol);
13479
+ };
13480
+ /**
13481
+ * Generates markdown report with all partial profit/loss events for a symbol.
13482
+ *
13483
+ * Creates formatted table containing:
13484
+ * - Action (PROFIT/LOSS)
13485
+ * - Symbol
13486
+ * - Signal ID
13487
+ * - Position (LONG/SHORT)
13488
+ * - Level % (+10%, -20%, etc)
13489
+ * - Current Price
13490
+ * - Timestamp (ISO 8601)
13491
+ * - Mode (Backtest/Live)
13492
+ *
13493
+ * Also includes summary statistics at the end.
13494
+ *
13495
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13496
+ * @returns Promise resolving to markdown formatted report string
13497
+ *
13498
+ * @example
13499
+ * ```typescript
13500
+ * const markdown = await Partial.getReport("BTCUSDT");
13501
+ * console.log(markdown);
13502
+ *
13503
+ * // Output:
13504
+ * // # Partial Profit/Loss Report: BTCUSDT
13505
+ * //
13506
+ * // | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
13507
+ * // | --- | --- | --- | --- | --- | --- | --- | --- |
13508
+ * // | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
13509
+ * // | LOSS | BTCUSDT | abc123 | LONG | -10% | 49000.00000000 USD | 2024-01-15T11:00:00.000Z | Backtest |
13510
+ * //
13511
+ * // **Total events:** 2
13512
+ * // **Profit events:** 1
13513
+ * // **Loss events:** 1
13514
+ * ```
13515
+ */
13516
+ this.getReport = async (symbol) => {
13517
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol });
13518
+ return await backtest$1.partialMarkdownService.getReport(symbol);
13519
+ };
13520
+ /**
13521
+ * Generates and saves markdown report to file.
13522
+ *
13523
+ * Creates directory if it doesn't exist.
13524
+ * Filename format: {symbol}.md (e.g., "BTCUSDT.md")
13525
+ *
13526
+ * Delegates to PartialMarkdownService.dump() which:
13527
+ * 1. Generates markdown report via getReport()
13528
+ * 2. Creates output directory (recursive mkdir)
13529
+ * 3. Writes file with UTF-8 encoding
13530
+ * 4. Logs success/failure to console
13531
+ *
13532
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
13533
+ * @param path - Output directory path (default: "./dump/partial")
13534
+ * @returns Promise that resolves when file is written
13535
+ *
13536
+ * @example
13537
+ * ```typescript
13538
+ * // Save to default path: ./dump/partial/BTCUSDT.md
13539
+ * await Partial.dump("BTCUSDT");
13540
+ *
13541
+ * // Save to custom path: ./reports/partial/BTCUSDT.md
13542
+ * await Partial.dump("BTCUSDT", "./reports/partial");
13543
+ *
13544
+ * // After multiple symbols backtested, export all reports
13545
+ * for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
13546
+ * await Partial.dump(symbol, "./backtest-results");
13547
+ * }
13548
+ * ```
13549
+ */
13550
+ this.dump = async (symbol, path) => {
13551
+ backtest$1.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, path });
13552
+ await backtest$1.partialMarkdownService.dump(symbol, path);
13553
+ };
13554
+ }
13555
+ }
13556
+ /**
13557
+ * Global singleton instance of PartialUtils.
13558
+ * Provides static-like access to partial profit/loss reporting methods.
13559
+ *
13560
+ * @example
13561
+ * ```typescript
13562
+ * import { Partial } from "backtest-kit";
13563
+ *
13564
+ * // Usage same as PartialUtils methods
13565
+ * const stats = await Partial.getData("BTCUSDT");
13566
+ * const report = await Partial.getReport("BTCUSDT");
13567
+ * await Partial.dump("BTCUSDT");
13568
+ * ```
13569
+ */
13570
+ const Partial = new PartialUtils();
13571
+
13572
+ export { Backtest, ExecutionContextService, Heat, Live, MethodContextService, Optimizer, Partial, Performance, PersistBase, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Schedule, Walker, addExchange, addFrame, addOptimizer, addRisk, addSizing, addStrategy, addWalker, emitters, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listOptimizers, listRisks, listSizings, listStrategies, listWalkers, listenBacktestProgress, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenPartialLoss, listenPartialLossOnce, listenPartialProfit, listenPartialProfitOnce, listenPerformance, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, setConfig, setLogger };