backtest-kit 2.1.2 → 2.2.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 (4) hide show
  1. package/build/index.cjs +463 -2324
  2. package/build/index.mjs +465 -2318
  3. package/package.json +1 -1
  4. package/types.d.ts +111 -1325
package/build/index.cjs CHANGED
@@ -8,10 +8,8 @@ var path = require('path');
8
8
  var crypto = require('crypto');
9
9
  var os = require('os');
10
10
  var fs$1 = require('fs');
11
- var module$1 = require('module');
12
11
  var util = require('util');
13
12
 
14
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
15
13
  function _interopNamespaceDefault(e) {
16
14
  var n = Object.create(null);
17
15
  if (e) {
@@ -73,7 +71,6 @@ const connectionServices$1 = {
73
71
  sizingConnectionService: Symbol('sizingConnectionService'),
74
72
  riskConnectionService: Symbol('riskConnectionService'),
75
73
  actionConnectionService: Symbol('actionConnectionService'),
76
- optimizerConnectionService: Symbol('optimizerConnectionService'),
77
74
  partialConnectionService: Symbol('partialConnectionService'),
78
75
  breakevenConnectionService: Symbol('breakevenConnectionService'),
79
76
  };
@@ -85,7 +82,6 @@ const schemaServices$1 = {
85
82
  sizingSchemaService: Symbol('sizingSchemaService'),
86
83
  riskSchemaService: Symbol('riskSchemaService'),
87
84
  actionSchemaService: Symbol('actionSchemaService'),
88
- optimizerSchemaService: Symbol('optimizerSchemaService'),
89
85
  };
90
86
  const coreServices$1 = {
91
87
  exchangeCoreService: Symbol('exchangeCoreService'),
@@ -96,7 +92,6 @@ const coreServices$1 = {
96
92
  const globalServices$1 = {
97
93
  sizingGlobalService: Symbol('sizingGlobalService'),
98
94
  riskGlobalService: Symbol('riskGlobalService'),
99
- optimizerGlobalService: Symbol('optimizerGlobalService'),
100
95
  partialGlobalService: Symbol('partialGlobalService'),
101
96
  breakevenGlobalService: Symbol('breakevenGlobalService'),
102
97
  };
@@ -146,16 +141,9 @@ const validationServices$1 = {
146
141
  sizingValidationService: Symbol('sizingValidationService'),
147
142
  riskValidationService: Symbol('riskValidationService'),
148
143
  actionValidationService: Symbol('actionValidationService'),
149
- optimizerValidationService: Symbol('optimizerValidationService'),
150
144
  configValidationService: Symbol('configValidationService'),
151
145
  columnValidationService: Symbol('columnValidationService'),
152
146
  };
153
- const templateServices$1 = {
154
- optimizerTemplateService: Symbol('optimizerTemplateService'),
155
- };
156
- const promptServices$1 = {
157
- signalPromptService: Symbol('signalPromptService'),
158
- };
159
147
  const TYPES = {
160
148
  ...baseServices$1,
161
149
  ...contextServices$1,
@@ -169,8 +157,6 @@ const TYPES = {
169
157
  ...markdownServices$1,
170
158
  ...reportServices$1,
171
159
  ...validationServices$1,
172
- ...templateServices$1,
173
- ...promptServices$1,
174
160
  };
175
161
 
176
162
  /**
@@ -494,11 +480,6 @@ const progressBacktestEmitter = new functoolsKit.Subject();
494
480
  * Emits progress updates during walker execution.
495
481
  */
496
482
  const progressWalkerEmitter = new functoolsKit.Subject();
497
- /**
498
- * Progress emitter for optimizer execution progress.
499
- * Emits progress updates during optimizer execution.
500
- */
501
- const progressOptimizerEmitter = new functoolsKit.Subject();
502
483
  /**
503
484
  * Performance emitter for execution metrics.
504
485
  * Emits performance metrics for profiling and bottleneck detection.
@@ -573,7 +554,6 @@ var emitters = /*#__PURE__*/Object.freeze({
573
554
  partialProfitSubject: partialProfitSubject,
574
555
  performanceEmitter: performanceEmitter,
575
556
  progressBacktestEmitter: progressBacktestEmitter,
576
- progressOptimizerEmitter: progressOptimizerEmitter,
577
557
  progressWalkerEmitter: progressWalkerEmitter,
578
558
  riskSubject: riskSubject,
579
559
  schedulePingSubject: schedulePingSubject,
@@ -1667,6 +1647,7 @@ class PersistCandleUtils {
1667
1647
  */
1668
1648
  const PersistCandleAdapter = new PersistCandleUtils();
1669
1649
 
1650
+ const MS_PER_MINUTE$1 = 60000;
1670
1651
  const INTERVAL_MINUTES$4 = {
1671
1652
  "1m": 1,
1672
1653
  "3m": 3,
@@ -1812,7 +1793,7 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
1812
1793
  const GET_CANDLES_FN = async (dto, since, self) => {
1813
1794
  const step = INTERVAL_MINUTES$4[dto.interval];
1814
1795
  const sinceTimestamp = since.getTime();
1815
- const untilTimestamp = sinceTimestamp + dto.limit * step * 60 * 1000;
1796
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
1816
1797
  // Try to read from cache first
1817
1798
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
1818
1799
  if (cachedCandles !== null) {
@@ -1918,7 +1899,7 @@ class ClientExchange {
1918
1899
  if (!adjust) {
1919
1900
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
1920
1901
  }
1921
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
1902
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * MS_PER_MINUTE$1);
1922
1903
  let allData = [];
1923
1904
  // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
1924
1905
  if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
@@ -1931,7 +1912,7 @@ class ClientExchange {
1931
1912
  remaining -= chunkLimit;
1932
1913
  if (remaining > 0) {
1933
1914
  // Move currentSince forward by the number of candles fetched
1934
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1915
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1935
1916
  }
1936
1917
  }
1937
1918
  }
@@ -1978,7 +1959,7 @@ class ClientExchange {
1978
1959
  const now = Date.now();
1979
1960
  // Вычисляем конечное время запроса
1980
1961
  const step = INTERVAL_MINUTES$4[interval];
1981
- const endTime = since.getTime() + limit * step * 60 * 1000;
1962
+ const endTime = since.getTime() + limit * step * MS_PER_MINUTE$1;
1982
1963
  // Проверяем что запрошенный период не заходит за Date.now()
1983
1964
  if (endTime > now) {
1984
1965
  return [];
@@ -1995,7 +1976,7 @@ class ClientExchange {
1995
1976
  remaining -= chunkLimit;
1996
1977
  if (remaining > 0) {
1997
1978
  // Move currentSince forward by the number of candles fetched
1998
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1979
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1999
1980
  }
2000
1981
  }
2001
1982
  }
@@ -2090,22 +2071,19 @@ class ClientExchange {
2090
2071
  /**
2091
2072
  * Fetches raw candles with flexible date/limit parameters.
2092
2073
  *
2093
- * Compatibility layer that:
2094
- * - RAW MODE (sDate + eDate + limit): fetches exactly as specified, NO look-ahead bias protection
2095
- * - Other modes: respects execution context and prevents look-ahead bias
2074
+ * All modes respect execution context and prevent look-ahead bias.
2096
2075
  *
2097
2076
  * Parameter combinations:
2098
- * 1. sDate + eDate + limit: RAW MODE - fetches exactly as specified, no validation against when
2099
- * 2. sDate + eDate: calculates limit from date range, validates endTimestamp <= when
2100
- * 3. eDate + limit: calculates sDate backward, validates endTimestamp <= when
2101
- * 4. sDate + limit: fetches forward, validates endTimestamp <= when
2077
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
2078
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
2079
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
2080
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
2102
2081
  * 5. Only limit: uses execution.context.when as reference (backward)
2103
2082
  *
2104
2083
  * Edge cases:
2105
2084
  * - If calculated limit is 0 or negative: throws error
2106
2085
  * - If sDate >= eDate: throws error
2107
- * - If startTimestamp >= endTimestamp: throws error
2108
- * - If endTimestamp > when (non-RAW modes only): throws error to prevent look-ahead bias
2086
+ * - If eDate > when: throws error to prevent look-ahead bias
2109
2087
  *
2110
2088
  * @param symbol - Trading pair symbol
2111
2089
  * @param interval - Candle interval
@@ -2124,73 +2102,75 @@ class ClientExchange {
2124
2102
  eDate,
2125
2103
  });
2126
2104
  const step = INTERVAL_MINUTES$4[interval];
2127
- const stepMs = step * 60 * 1000;
2128
- const when = this.params.execution.context.when.getTime();
2129
- let startTimestamp;
2130
- let endTimestamp;
2131
- let candleLimit;
2132
- let isRawMode = false;
2133
- // Case 1: sDate + eDate + limit - RAW MODE (no look-ahead bias protection)
2134
- if (sDate !== undefined &&
2135
- eDate !== undefined &&
2136
- limit !== undefined) {
2137
- isRawMode = true;
2105
+ if (!step) {
2106
+ throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2107
+ }
2108
+ const whenTimestamp = this.params.execution.context.when.getTime();
2109
+ let sinceTimestamp;
2110
+ let untilTimestamp;
2111
+ let calculatedLimit;
2112
+ // Case 1: all three parameters provided
2113
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
2138
2114
  if (sDate >= eDate) {
2139
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2115
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2140
2116
  }
2141
- startTimestamp = sDate;
2142
- endTimestamp = eDate;
2143
- candleLimit = limit;
2117
+ if (eDate > whenTimestamp) {
2118
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2119
+ }
2120
+ sinceTimestamp = sDate;
2121
+ untilTimestamp = eDate;
2122
+ calculatedLimit = limit;
2144
2123
  }
2145
- // Case 2: sDate + eDate - calculate limit, respect backtest context
2124
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
2146
2125
  else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
2147
2126
  if (sDate >= eDate) {
2148
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2149
- }
2150
- startTimestamp = sDate;
2151
- endTimestamp = eDate;
2152
- const rangeDuration = endTimestamp - startTimestamp;
2153
- candleLimit = Math.floor(rangeDuration / stepMs);
2154
- if (candleLimit <= 0) {
2155
- throw new Error(`ClientExchange getRawCandles: calculated limit is ${candleLimit} for range [${sDate}, ${eDate}]`);
2156
- }
2157
- }
2158
- // Case 3: eDate + limit - calculate sDate backward, respect backtest context
2159
- else if (eDate !== undefined && limit !== undefined && sDate === undefined) {
2160
- endTimestamp = eDate;
2161
- startTimestamp = eDate - limit * stepMs;
2162
- candleLimit = limit;
2163
- }
2164
- // Case 4: sDate + limit - fetch forward, respect backtest context
2165
- else if (sDate !== undefined && limit !== undefined && eDate === undefined) {
2166
- startTimestamp = sDate;
2167
- endTimestamp = sDate + limit * stepMs;
2168
- candleLimit = limit;
2169
- }
2170
- // Case 5: Only limit - use execution context (backward from when)
2171
- else if (limit !== undefined && sDate === undefined && eDate === undefined) {
2172
- endTimestamp = when;
2173
- startTimestamp = when - limit * stepMs;
2174
- candleLimit = limit;
2175
- }
2176
- // Invalid combination
2177
- else {
2178
- throw new Error(`ClientExchange getRawCandles: invalid parameter combination. Must provide either (limit), (eDate+limit), (sDate+limit), (sDate+eDate), or (sDate+eDate+limit)`);
2127
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2128
+ }
2129
+ if (eDate > whenTimestamp) {
2130
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2131
+ }
2132
+ sinceTimestamp = sDate;
2133
+ untilTimestamp = eDate;
2134
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE$1));
2135
+ if (calculatedLimit <= 0) {
2136
+ throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
2137
+ }
2138
+ }
2139
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
2140
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
2141
+ if (eDate > whenTimestamp) {
2142
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2143
+ }
2144
+ untilTimestamp = eDate;
2145
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE$1;
2146
+ calculatedLimit = limit;
2147
+ }
2148
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
2149
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
2150
+ sinceTimestamp = sDate;
2151
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE$1;
2152
+ if (untilTimestamp > whenTimestamp) {
2153
+ throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2154
+ }
2155
+ calculatedLimit = limit;
2179
2156
  }
2180
- // Validate timestamps
2181
- if (startTimestamp >= endTimestamp) {
2182
- throw new Error(`ClientExchange getRawCandles: startTimestamp (${startTimestamp}) >= endTimestamp (${endTimestamp})`);
2157
+ // Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
2158
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
2159
+ untilTimestamp = whenTimestamp;
2160
+ sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
2161
+ calculatedLimit = limit;
2183
2162
  }
2184
- // Check if trying to fetch future data (prevent look-ahead bias)
2185
- // ONLY for non-RAW modes - RAW MODE bypasses this check
2186
- if (!isRawMode && endTimestamp > when) {
2187
- throw new Error(`ClientExchange getRawCandles: endTimestamp (${endTimestamp}) is beyond execution context when (${when}) - look-ahead bias prevented`);
2163
+ // Invalid: no parameters or only sDate or only eDate
2164
+ else {
2165
+ throw new Error(`ClientExchange getRawCandles: invalid parameter combination. ` +
2166
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
2167
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
2188
2168
  }
2189
- const since = new Date(startTimestamp);
2169
+ // Fetch candles using existing logic
2170
+ const since = new Date(sinceTimestamp);
2190
2171
  let allData = [];
2191
- // Fetch data in chunks if limit exceeds max per request
2192
- if (candleLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2193
- let remaining = candleLimit;
2172
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2173
+ let remaining = calculatedLimit;
2194
2174
  let currentSince = new Date(since.getTime());
2195
2175
  while (remaining > 0) {
2196
2176
  const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
@@ -2198,16 +2178,16 @@ class ClientExchange {
2198
2178
  allData.push(...chunkData);
2199
2179
  remaining -= chunkLimit;
2200
2180
  if (remaining > 0) {
2201
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
2181
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
2202
2182
  }
2203
2183
  }
2204
2184
  }
2205
2185
  else {
2206
- allData = await GET_CANDLES_FN({ symbol, interval, limit: candleLimit }, since, this);
2186
+ allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2207
2187
  }
2208
2188
  // Filter candles to strictly match the requested range
2209
- const filteredData = allData.filter((candle) => candle.timestamp >= startTimestamp &&
2210
- candle.timestamp < endTimestamp);
2189
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2190
+ candle.timestamp < untilTimestamp);
2211
2191
  // Apply distinct by timestamp to remove duplicates
2212
2192
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2213
2193
  if (filteredData.length !== uniqueData.length) {
@@ -2215,12 +2195,12 @@ class ClientExchange {
2215
2195
  this.params.logger.warn(msg);
2216
2196
  console.warn(msg);
2217
2197
  }
2218
- if (uniqueData.length < candleLimit) {
2219
- const msg = `ClientExchange getRawCandles: Expected ${candleLimit} candles, got ${uniqueData.length}`;
2198
+ if (uniqueData.length < calculatedLimit) {
2199
+ const msg = `ClientExchange getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`;
2220
2200
  this.params.logger.warn(msg);
2221
2201
  console.warn(msg);
2222
2202
  }
2223
- await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, candleLimit, uniqueData);
2203
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
2224
2204
  return uniqueData;
2225
2205
  }
2226
2206
  /**
@@ -2242,7 +2222,7 @@ class ClientExchange {
2242
2222
  });
2243
2223
  const to = new Date(this.params.execution.context.when.getTime());
2244
2224
  const from = new Date(to.getTime() -
2245
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
2225
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2246
2226
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2247
2227
  }
2248
2228
  }
@@ -2433,6 +2413,28 @@ class ExchangeConnectionService {
2433
2413
  });
2434
2414
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2435
2415
  };
2416
+ /**
2417
+ * Fetches raw candles with flexible date/limit parameters.
2418
+ *
2419
+ * Routes to exchange determined by methodContextService.context.exchangeName.
2420
+ *
2421
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2422
+ * @param interval - Candle interval (e.g., "1h", "1d")
2423
+ * @param limit - Optional number of candles to fetch
2424
+ * @param sDate - Optional start date in milliseconds
2425
+ * @param eDate - Optional end date in milliseconds
2426
+ * @returns Promise resolving to array of candle data
2427
+ */
2428
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
2429
+ this.loggerService.log("exchangeConnectionService getRawCandles", {
2430
+ symbol,
2431
+ interval,
2432
+ limit,
2433
+ sDate,
2434
+ eDate,
2435
+ });
2436
+ return await this.getExchange(this.methodContextService.context.exchangeName).getRawCandles(symbol, interval, limit, sDate, eDate);
2437
+ };
2436
2438
  }
2437
2439
  }
2438
2440
 
@@ -9976,6 +9978,40 @@ class ExchangeCoreService {
9976
9978
  backtest,
9977
9979
  });
9978
9980
  };
9981
+ /**
9982
+ * Fetches raw candles with flexible date/limit parameters and execution context.
9983
+ *
9984
+ * @param symbol - Trading pair symbol
9985
+ * @param interval - Candle interval (e.g., "1m", "1h")
9986
+ * @param when - Timestamp for context (used in backtest mode)
9987
+ * @param backtest - Whether running in backtest mode
9988
+ * @param limit - Optional number of candles to fetch
9989
+ * @param sDate - Optional start date in milliseconds
9990
+ * @param eDate - Optional end date in milliseconds
9991
+ * @returns Promise resolving to array of candles
9992
+ */
9993
+ this.getRawCandles = async (symbol, interval, when, backtest, limit, sDate, eDate) => {
9994
+ this.loggerService.log("exchangeCoreService getRawCandles", {
9995
+ symbol,
9996
+ interval,
9997
+ when,
9998
+ backtest,
9999
+ limit,
10000
+ sDate,
10001
+ eDate,
10002
+ });
10003
+ if (!MethodContextService.hasContext()) {
10004
+ throw new Error("exchangeCoreService getRawCandles requires a method context");
10005
+ }
10006
+ await this.validate(this.methodContextService.context.exchangeName);
10007
+ return await ExecutionContextService.runInContext(async () => {
10008
+ return await this.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
10009
+ }, {
10010
+ symbol,
10011
+ when,
10012
+ backtest,
10013
+ });
10014
+ };
9979
10015
  }
9980
10016
  }
9981
10017
 
@@ -18365,1469 +18401,120 @@ class ActionValidationService {
18365
18401
  }
18366
18402
 
18367
18403
  /**
18368
- * Default template service for generating optimizer code snippets.
18369
- * Implements all IOptimizerTemplate methods with Ollama LLM integration.
18404
+ * Symbol marker indicating that partial state needs initialization.
18405
+ * Used as sentinel value for _states before waitForInit() is called.
18406
+ */
18407
+ const NEED_FETCH$1 = Symbol("need_fetch");
18408
+ /**
18409
+ * Array of profit level milestones to track (10%, 20%, ..., 100%).
18410
+ * Each level is checked during profit() method to emit events for newly reached levels.
18411
+ */
18412
+ const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
18413
+ /**
18414
+ * Array of loss level milestones to track (-10%, -20%, ..., -100%).
18415
+ * Each level is checked during loss() method to emit events for newly reached levels.
18416
+ */
18417
+ const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
18418
+ /**
18419
+ * Internal profit handler function for ClientPartial.
18370
18420
  *
18371
- * Features:
18372
- * - Multi-timeframe analysis (1m, 5m, 15m, 1h)
18373
- * - JSON structured output for signals
18374
- * - Debug logging to ./dump/strategy
18375
- * - CCXT exchange integration
18376
- * - Walker-based strategy comparison
18421
+ * Checks which profit levels have been reached and emits events for new levels only.
18422
+ * Uses Set-based deduplication to prevent duplicate events.
18377
18423
  *
18378
- * Can be partially overridden in optimizer schema configuration.
18424
+ * @param symbol - Trading pair symbol
18425
+ * @param data - Signal row data
18426
+ * @param currentPrice - Current market price
18427
+ * @param revenuePercent - Current profit percentage (positive value)
18428
+ * @param backtest - True if backtest mode
18429
+ * @param when - Event timestamp
18430
+ * @param self - ClientPartial instance reference
18379
18431
  */
18380
- class OptimizerTemplateService {
18381
- constructor() {
18382
- this.loggerService = inject(TYPES.loggerService);
18383
- /**
18384
- * Generates the top banner with imports and constants.
18385
- *
18386
- * @param symbol - Trading pair symbol
18387
- * @returns Shebang, imports, and WARN_KB constant
18388
- */
18389
- this.getTopBanner = async (symbol) => {
18390
- this.loggerService.log("optimizerTemplateService getTopBanner", {
18391
- symbol,
18392
- });
18393
- return [
18394
- "#!/usr/bin/env node",
18395
- "",
18396
- `import { Ollama } from "ollama";`,
18397
- `import ccxt from "ccxt";`,
18398
- `import {`,
18399
- ` addExchangeSchema,`,
18400
- ` addStrategySchema,`,
18401
- ` addFrameSchema,`,
18402
- ` addWalkerSchema,`,
18403
- ` Walker,`,
18404
- ` Backtest,`,
18405
- ` getCandles,`,
18406
- ` listenSignalBacktest,`,
18407
- ` listenWalkerComplete,`,
18408
- ` listenDoneBacktest,`,
18409
- ` listenBacktestProgress,`,
18410
- ` listenWalkerProgress,`,
18411
- ` listenError,`,
18412
- ` Markdown,`,
18413
- `} from "backtest-kit";`,
18414
- `import { promises as fs } from "fs";`,
18415
- `import { v4 as uuid } from "uuid";`,
18416
- `import path from "path";`,
18417
- ``,
18418
- `const WARN_KB = 100;`,
18419
- ``,
18420
- `Markdown.enable()`,
18421
- ].join("\n");
18422
- };
18423
- /**
18424
- * Generates default user message for LLM conversation.
18425
- * Simple prompt to read and acknowledge data.
18426
- *
18427
- * @param symbol - Trading pair symbol
18428
- * @param data - Fetched data array
18429
- * @param name - Source name
18430
- * @returns User message with JSON data
18431
- */
18432
- this.getUserMessage = async (symbol, data, name) => {
18433
- this.loggerService.log("optimizerTemplateService getUserMessage", {
18434
- symbol,
18435
- data,
18436
- name,
18437
- });
18438
- return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
18439
- };
18440
- /**
18441
- * Generates default assistant message for LLM conversation.
18442
- * Simple acknowledgment response.
18443
- *
18444
- * @param symbol - Trading pair symbol
18445
- * @param data - Fetched data array
18446
- * @param name - Source name
18447
- * @returns Assistant acknowledgment message
18448
- */
18449
- this.getAssistantMessage = async (symbol, data, name) => {
18450
- this.loggerService.log("optimizerTemplateService getAssistantMessage", {
18451
- symbol,
18452
- data,
18453
- name,
18454
- });
18455
- return "ОК";
18456
- };
18457
- /**
18458
- * Generates Walker configuration code.
18459
- * Compares multiple strategies on test frame.
18460
- *
18461
- * @param walkerName - Unique walker identifier
18462
- * @param exchangeName - Exchange to use for backtesting
18463
- * @param frameName - Test frame name
18464
- * @param strategies - Array of strategy names to compare
18465
- * @returns Generated addWalker() call
18466
- */
18467
- this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
18468
- this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
18469
- walkerName,
18470
- exchangeName,
18471
- frameName,
18472
- strategies,
18473
- });
18474
- // Escape special characters to prevent code injection
18475
- const escapedWalkerName = String(walkerName)
18476
- .replace(/\\/g, '\\\\')
18477
- .replace(/"/g, '\\"');
18478
- const escapedExchangeName = String(exchangeName)
18479
- .replace(/\\/g, '\\\\')
18480
- .replace(/"/g, '\\"');
18481
- const escapedFrameName = String(frameName)
18482
- .replace(/\\/g, '\\\\')
18483
- .replace(/"/g, '\\"');
18484
- const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
18485
- return [
18486
- `addWalkerSchema({`,
18487
- ` walkerName: "${escapedWalkerName}",`,
18488
- ` exchangeName: "${escapedExchangeName}",`,
18489
- ` frameName: "${escapedFrameName}",`,
18490
- ` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
18491
- `});`
18492
- ].join("\n");
18493
- };
18494
- /**
18495
- * Generates Strategy configuration with LLM integration.
18496
- * Includes multi-timeframe analysis and signal generation.
18497
- *
18498
- * @param strategyName - Unique strategy identifier
18499
- * @param interval - Signal throttling interval (e.g., "5m")
18500
- * @param prompt - Strategy logic from getPrompt()
18501
- * @returns Generated addStrategy() call with getSignal() function
18502
- */
18503
- this.getStrategyTemplate = async (strategyName, interval, prompt) => {
18504
- this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
18505
- strategyName,
18506
- interval,
18507
- prompt,
18508
- });
18509
- // Convert prompt to plain text first
18510
- const plainPrompt = toPlainString(prompt);
18511
- // Escape special characters to prevent code injection
18512
- const escapedStrategyName = String(strategyName)
18513
- .replace(/\\/g, '\\\\')
18514
- .replace(/"/g, '\\"');
18515
- const escapedInterval = String(interval)
18516
- .replace(/\\/g, '\\\\')
18517
- .replace(/"/g, '\\"');
18518
- const escapedPrompt = String(plainPrompt)
18519
- .replace(/\\/g, '\\\\')
18520
- .replace(/`/g, '\\`')
18521
- .replace(/\$/g, '\\$');
18522
- return [
18523
- `addStrategySchema({`,
18524
- ` strategyName: "${escapedStrategyName}",`,
18525
- ` interval: "${escapedInterval}",`,
18526
- ` getSignal: async (symbol) => {`,
18527
- ` const messages = [];`,
18528
- ``,
18529
- ` // Загружаем данные всех таймфреймов`,
18530
- ` const microTermCandles = await getCandles(symbol, "1m", 30);`,
18531
- ` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
18532
- ` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
18533
- ` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
18534
- ``,
18535
- ` function formatCandles(candles, timeframe) {`,
18536
- ` return candles.map((c) =>`,
18537
- ` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
18538
- ` ).join("\\n");`,
18539
- ` }`,
18540
- ``,
18541
- ` // Сообщение 1: Среднесрочный тренд`,
18542
- ` messages.push(`,
18543
- ` {`,
18544
- ` role: "user",`,
18545
- ` content: [`,
18546
- ` \`\${symbol}\`,`,
18547
- ` "Проанализируй свечи 1h:",`,
18548
- ` "",`,
18549
- ` formatCandles(mediumTermCandles, "1h")`,
18550
- ` ].join("\\n"),`,
18551
- ` },`,
18552
- ` {`,
18553
- ` role: "assistant",`,
18554
- ` content: "Тренд 1h проанализирован",`,
18555
- ` }`,
18556
- ` );`,
18557
- ``,
18558
- ` // Сообщение 2: Краткосрочный тренд`,
18559
- ` messages.push(`,
18560
- ` {`,
18561
- ` role: "user",`,
18562
- ` content: [`,
18563
- ` "Проанализируй свечи 15m:",`,
18564
- ` "",`,
18565
- ` formatCandles(shortTermCandles, "15m")`,
18566
- ` ].join("\\n"),`,
18567
- ` },`,
18568
- ` {`,
18569
- ` role: "assistant",`,
18570
- ` content: "Тренд 15m проанализирован",`,
18571
- ` }`,
18572
- ` );`,
18573
- ``,
18574
- ` // Сообщение 3: Основной таймфрейм`,
18575
- ` messages.push(`,
18576
- ` {`,
18577
- ` role: "user",`,
18578
- ` content: [`,
18579
- ` "Проанализируй свечи 5m:",`,
18580
- ` "",`,
18581
- ` formatCandles(mainTermCandles, "5m")`,
18582
- ` ].join("\\n")`,
18583
- ` },`,
18584
- ` {`,
18585
- ` role: "assistant",`,
18586
- ` content: "Таймфрейм 5m проанализирован",`,
18587
- ` }`,
18588
- ` );`,
18589
- ``,
18590
- ` // Сообщение 4: Микро-структура`,
18591
- ` messages.push(`,
18592
- ` {`,
18593
- ` role: "user",`,
18594
- ` content: [`,
18595
- ` "Проанализируй свечи 1m:",`,
18596
- ` "",`,
18597
- ` formatCandles(microTermCandles, "1m")`,
18598
- ` ].join("\\n")`,
18599
- ` },`,
18600
- ` {`,
18601
- ` role: "assistant",`,
18602
- ` content: "Микроструктура 1m проанализирована",`,
18603
- ` }`,
18604
- ` );`,
18605
- ``,
18606
- ` // Сообщение 5: Запрос сигнала`,
18607
- ` messages.push(`,
18608
- ` {`,
18609
- ` role: "user",`,
18610
- ` content: [`,
18611
- ` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
18612
- ` "",`,
18613
- ` \`${escapedPrompt}\`,`,
18614
- ` "",`,
18615
- ` "Если сигналы противоречивы или тренд слабый то position: wait"`,
18616
- ` ].join("\\n"),`,
18617
- ` }`,
18618
- ` );`,
18619
- ``,
18620
- ` const resultId = uuid();`,
18621
- ``,
18622
- ` const result = await json(messages);`,
18623
- ``,
18624
- ` await dumpJson(resultId, messages, result);`,
18625
- ``,
18626
- ` result.id = resultId;`,
18627
- ``,
18628
- ` return result;`,
18629
- ` },`,
18630
- `});`
18631
- ].join("\n");
18632
- };
18633
- /**
18634
- * Generates Exchange configuration code.
18635
- * Uses CCXT Binance with standard formatters.
18636
- *
18637
- * @param symbol - Trading pair symbol (unused, for consistency)
18638
- * @param exchangeName - Unique exchange identifier
18639
- * @returns Generated addExchange() call with CCXT integration
18640
- */
18641
- this.getExchangeTemplate = async (symbol, exchangeName) => {
18642
- this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
18643
- exchangeName,
18644
- symbol,
18645
- });
18646
- // Escape special characters to prevent code injection
18647
- const escapedExchangeName = String(exchangeName)
18648
- .replace(/\\/g, '\\\\')
18649
- .replace(/"/g, '\\"');
18650
- return [
18651
- `addExchangeSchema({`,
18652
- ` exchangeName: "${escapedExchangeName}",`,
18653
- ` getCandles: async (symbol, interval, since, limit) => {`,
18654
- ` const exchange = new ccxt.binance();`,
18655
- ` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
18656
- ` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
18657
- ` timestamp, open, high, low, close, volume`,
18658
- ` }));`,
18659
- ` },`,
18660
- ` formatPrice: async (symbol, price) => price.toFixed(2),`,
18661
- ` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
18662
- `});`
18663
- ].join("\n");
18664
- };
18665
- /**
18666
- * Generates Frame (timeframe) configuration code.
18667
- *
18668
- * @param symbol - Trading pair symbol (unused, for consistency)
18669
- * @param frameName - Unique frame identifier
18670
- * @param interval - Candle interval (e.g., "1m")
18671
- * @param startDate - Frame start date
18672
- * @param endDate - Frame end date
18673
- * @returns Generated addFrame() call
18674
- */
18675
- this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
18676
- this.loggerService.log("optimizerTemplateService getFrameTemplate", {
18677
- symbol,
18678
- frameName,
18679
- interval,
18680
- startDate,
18681
- endDate,
18682
- });
18683
- // Escape special characters to prevent code injection
18684
- const escapedFrameName = String(frameName)
18685
- .replace(/\\/g, '\\\\')
18686
- .replace(/"/g, '\\"');
18687
- const escapedInterval = String(interval)
18688
- .replace(/\\/g, '\\\\')
18689
- .replace(/"/g, '\\"');
18690
- return [
18691
- `addFrameSchema({`,
18692
- ` frameName: "${escapedFrameName}",`,
18693
- ` interval: "${escapedInterval}",`,
18694
- ` startDate: new Date("${startDate.toISOString()}"),`,
18695
- ` endDate: new Date("${endDate.toISOString()}"),`,
18696
- `});`
18697
- ].join("\n");
18698
- };
18699
- /**
18700
- * Generates launcher code to run Walker with event listeners.
18701
- * Includes progress tracking and completion handlers.
18702
- *
18703
- * @param symbol - Trading pair symbol
18704
- * @param walkerName - Walker name to launch
18705
- * @returns Generated Walker.background() call with listeners
18706
- */
18707
- this.getLauncherTemplate = async (symbol, walkerName) => {
18708
- this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
18709
- symbol,
18710
- walkerName,
18711
- });
18712
- // Escape special characters to prevent code injection
18713
- const escapedSymbol = String(symbol)
18714
- .replace(/\\/g, '\\\\')
18715
- .replace(/"/g, '\\"');
18716
- const escapedWalkerName = String(walkerName)
18717
- .replace(/\\/g, '\\\\')
18718
- .replace(/"/g, '\\"');
18719
- return [
18720
- `Walker.background("${escapedSymbol}", {`,
18721
- ` walkerName: "${escapedWalkerName}"`,
18722
- `});`,
18723
- ``,
18724
- `listenSignalBacktest((event) => {`,
18725
- ` console.log(event);`,
18726
- `});`,
18727
- ``,
18728
- `listenBacktestProgress((event) => {`,
18729
- ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
18730
- ` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
18731
- `});`,
18732
- ``,
18733
- `listenWalkerProgress((event) => {`,
18734
- ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
18735
- ` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
18736
- ` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
18737
- `});`,
18738
- ``,
18739
- `listenWalkerComplete((results) => {`,
18740
- ` console.log("Walker completed:", results.bestStrategy);`,
18741
- ` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
18742
- `});`,
18743
- ``,
18744
- `listenDoneBacktest((event) => {`,
18745
- ` console.log("Backtest completed:", event.symbol);`,
18746
- ` Backtest.dump(event.symbol, {`,
18747
- ` strategyName: event.strategyName,`,
18748
- ` exchangeName: event.exchangeName,`,
18749
- ` frameName: event.frameName`,
18750
- ` });`,
18751
- `});`,
18752
- ``,
18753
- `listenError((error) => {`,
18754
- ` console.error("Error occurred:", error);`,
18755
- `});`
18756
- ].join("\n");
18757
- };
18758
- /**
18759
- * Generates dumpJson() helper function for debug output.
18760
- * Saves LLM conversations and results to ./dump/strategy/{resultId}/
18761
- *
18762
- * @param symbol - Trading pair symbol (unused, for consistency)
18763
- * @returns Generated async dumpJson() function
18764
- */
18765
- this.getJsonDumpTemplate = async (symbol) => {
18766
- this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
18767
- symbol,
18768
- });
18769
- return [
18770
- `async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
18771
- ` // Extract system messages and system reminders from existing data`,
18772
- ` const systemMessages = history.filter((m) => m.role === "system");`,
18773
- ` const userMessages = history.filter((m) => m.role === "user");`,
18774
- ` const subfolderPath = path.join(outputDir, resultId);`,
18775
- ``,
18776
- ` try {`,
18777
- ` await fs.access(subfolderPath);`,
18778
- ` return;`,
18779
- ` } catch {`,
18780
- ` await fs.mkdir(subfolderPath, { recursive: true });`,
18781
- ` }`,
18782
- ``,
18783
- ` {`,
18784
- ` let summary = "# Outline Result Summary\\n\\n";`,
18785
- ``,
18786
- ` {`,
18787
- ` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
18788
- ` }`,
18789
- ``,
18790
- ` if (result) {`,
18791
- ` summary += "## Output Data\\n\\n";`,
18792
- ` summary += "\`\`\`json\\n";`,
18793
- ` summary += JSON.stringify(result, null, 2);`,
18794
- ` summary += "\\n\`\`\`\\n\\n";`,
18795
- ` }`,
18796
- ``,
18797
- ` // Add system messages to summary`,
18798
- ` if (systemMessages.length > 0) {`,
18799
- ` summary += "## System Messages\\n\\n";`,
18800
- ` systemMessages.forEach((msg, idx) => {`,
18801
- ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
18802
- ` summary += msg.content;`,
18803
- ` summary += "\\n\\n";`,
18804
- ` });`,
18805
- ` }`,
18806
- ``,
18807
- ` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
18808
- ` await fs.writeFile(summaryFile, summary, "utf8");`,
18809
- ` }`,
18810
- ``,
18811
- ` {`,
18812
- ` await Promise.all(`,
18813
- ` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
18814
- ` const messageNum = String(idx + 1).padStart(2, "0");`,
18815
- ` const contentFileName = \`\${messageNum}_user_message.md\`;`,
18816
- ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
18817
- ``,
18818
- ` {`,
18819
- ` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
18820
- ` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
18821
- ` if (messageSizeKb > WARN_KB) {`,
18822
- ` console.warn(`,
18823
- ` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
18824
- ` );`,
18825
- ` }`,
18826
- ` }`,
18827
- ``,
18828
- ` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
18829
- ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
18830
- ` content += message.content;`,
18831
- ` content += "\\n";`,
18832
- ``,
18833
- ` await fs.writeFile(contentFilePath, content, "utf8");`,
18834
- ` })`,
18835
- ` );`,
18836
- ` }`,
18837
- ``,
18838
- ` {`,
18839
- ` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
18840
- ` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
18841
- ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
18842
- ``,
18843
- ` let content = "# Full Outline Result\\n\\n";`,
18844
- ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
18845
- ``,
18846
- ` if (result) {`,
18847
- ` content += "## Output Data\\n\\n";`,
18848
- ` content += "\`\`\`json\\n";`,
18849
- ` content += JSON.stringify(result, null, 2);`,
18850
- ` content += "\\n\`\`\`\\n";`,
18851
- ` }`,
18852
- ``,
18853
- ` await fs.writeFile(contentFilePath, content, "utf8");`,
18854
- ` }`,
18855
- `}`
18856
- ].join("\n");
18432
+ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
18433
+ if (self._states === NEED_FETCH$1) {
18434
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
18435
+ }
18436
+ if (data.id !== self.params.signalId) {
18437
+ throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
18438
+ }
18439
+ let state = self._states.get(data.id);
18440
+ if (!state) {
18441
+ state = {
18442
+ profitLevels: new Set(),
18443
+ lossLevels: new Set(),
18857
18444
  };
18858
- /**
18859
- * Generates text() helper for LLM text generation.
18860
- * Uses Ollama deepseek-v3.1:671b model for market analysis.
18861
- *
18862
- * @param symbol - Trading pair symbol (used in prompt)
18863
- * @returns Generated async text() function
18864
- */
18865
- this.getTextTemplate = async (symbol) => {
18866
- this.loggerService.log("optimizerTemplateService getTextTemplate", {
18445
+ self._states.set(data.id, state);
18446
+ }
18447
+ let shouldPersist = false;
18448
+ for (const level of PROFIT_LEVELS) {
18449
+ if (revenuePercent >= level && !state.profitLevels.has(level)) {
18450
+ state.profitLevels.add(level);
18451
+ shouldPersist = true;
18452
+ self.params.logger.debug("ClientPartial profit level reached", {
18867
18453
  symbol,
18454
+ signalId: data.id,
18455
+ level,
18456
+ revenuePercent,
18457
+ backtest,
18868
18458
  });
18869
- // Escape special characters in symbol to prevent code injection
18870
- const escapedSymbol = String(symbol)
18871
- .replace(/\\/g, '\\\\')
18872
- .replace(/`/g, '\\`')
18873
- .replace(/\$/g, '\\$')
18874
- .toUpperCase();
18875
- return [
18876
- `async function text(messages) {`,
18877
- ` const ollama = new Ollama({`,
18878
- ` host: "https://ollama.com",`,
18879
- ` headers: {`,
18880
- ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
18881
- ` },`,
18882
- ` });`,
18883
- ``,
18884
- ` const response = await ollama.chat({`,
18885
- ` model: "deepseek-v3.1:671b",`,
18886
- ` messages: [`,
18887
- ` {`,
18888
- ` role: "system",`,
18889
- ` content: [`,
18890
- ` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
18891
- ` "только отчёт готовый для копипасты целиком",`,
18892
- ` "",`,
18893
- ` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
18894
- ` ].join("\\n"),`,
18895
- ` },`,
18896
- ` ...messages,`,
18897
- ` {`,
18898
- ` role: "user",`,
18899
- ` content: [`,
18900
- ` "На каких условиях мне купить ${escapedSymbol}?",`,
18901
- ` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
18902
- ` "Какой RR ставить для позиций?",`,
18903
- ` "Предпочтительны LONG или SHORT позиции?",`,
18904
- ` "",`,
18905
- ` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
18906
- ` ].join("\\n")`,
18907
- ` }`,
18908
- ` ]`,
18909
- ` });`,
18910
- ``,
18911
- ` const content = response.message.content.trim();`,
18912
- ` return content`,
18913
- ` .replace(/\\\\/g, '\\\\\\\\')`,
18914
- ` .replace(/\`/g, '\\\\\`')`,
18915
- ` .replace(/\\$/g, '\\\\$')`,
18916
- ` .replace(/"/g, '\\\\"')`,
18917
- ` .replace(/'/g, "\\\\'");`,
18918
- `}`
18919
- ].join("\n");
18459
+ await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
18460
+ }
18461
+ }
18462
+ if (shouldPersist) {
18463
+ await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
18464
+ }
18465
+ };
18466
+ /**
18467
+ * Internal loss handler function for ClientPartial.
18468
+ *
18469
+ * Checks which loss levels have been reached and emits events for new levels only.
18470
+ * Uses Set-based deduplication to prevent duplicate events.
18471
+ * Converts negative lossPercent to absolute value for level comparison.
18472
+ *
18473
+ * @param symbol - Trading pair symbol
18474
+ * @param data - Signal row data
18475
+ * @param currentPrice - Current market price
18476
+ * @param lossPercent - Current loss percentage (negative value)
18477
+ * @param backtest - True if backtest mode
18478
+ * @param when - Event timestamp
18479
+ * @param self - ClientPartial instance reference
18480
+ */
18481
+ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
18482
+ if (self._states === NEED_FETCH$1) {
18483
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
18484
+ }
18485
+ if (data.id !== self.params.signalId) {
18486
+ throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
18487
+ }
18488
+ let state = self._states.get(data.id);
18489
+ if (!state) {
18490
+ state = {
18491
+ profitLevels: new Set(),
18492
+ lossLevels: new Set(),
18920
18493
  };
18921
- /**
18922
- * Generates json() helper for structured LLM output.
18923
- * Uses Ollama with JSON schema for trading signals.
18924
- *
18925
- * Signal schema:
18926
- * - position: "wait" | "long" | "short"
18927
- * - note: strategy explanation
18928
- * - priceOpen: entry price
18929
- * - priceTakeProfit: target price
18930
- * - priceStopLoss: stop price
18931
- * - minuteEstimatedTime: expected duration (max 360 min)
18932
- *
18933
- * @param symbol - Trading pair symbol (unused, for consistency)
18934
- * @returns Generated async json() function with signal schema
18935
- */
18936
- this.getJsonTemplate = async (symbol) => {
18937
- this.loggerService.log("optimizerTemplateService getJsonTemplate", {
18494
+ self._states.set(data.id, state);
18495
+ }
18496
+ const absLoss = Math.abs(lossPercent);
18497
+ let shouldPersist = false;
18498
+ for (const level of LOSS_LEVELS) {
18499
+ if (absLoss >= level && !state.lossLevels.has(level)) {
18500
+ state.lossLevels.add(level);
18501
+ shouldPersist = true;
18502
+ self.params.logger.debug("ClientPartial loss level reached", {
18938
18503
  symbol,
18504
+ signalId: data.id,
18505
+ level,
18506
+ lossPercent,
18507
+ backtest,
18939
18508
  });
18940
- return [
18941
- `async function json(messages) {`,
18942
- ` const ollama = new Ollama({`,
18943
- ` host: "https://ollama.com",`,
18944
- ` headers: {`,
18945
- ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
18946
- ` },`,
18947
- ` });`,
18948
- ``,
18949
- ` const response = await ollama.chat({`,
18950
- ` model: "deepseek-v3.1:671b",`,
18951
- ` messages: [`,
18952
- ` {`,
18953
- ` role: "system",`,
18954
- ` content: [`,
18955
- ` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
18956
- ` "",`,
18957
- ` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
18958
- ` "",`,
18959
- ` "1. ТИПЫ ПОЗИЦИЙ:",`,
18960
- ` " - position='wait': нет четкого сигнала, жди лучших условий",`,
18961
- ` " - position='long': бычий сигнал, цена будет расти",`,
18962
- ` " - position='short': медвежий сигнал, цена будет падать",`,
18963
- ` "",`,
18964
- ` "2. ЦЕНА ВХОДА (priceOpen):",`,
18965
- ` " - Может быть текущей рыночной ценой для немедленного входа",`,
18966
- ` " - Может быть отложенной ценой для входа при достижении уровня",`,
18967
- ` " - Укажи оптимальную цену входа согласно технического анализа",`,
18968
- ` "",`,
18969
- ` "3. УРОВНИ ВЫХОДА:",`,
18970
- ` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
18971
- ` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
18972
- ` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
18973
- ` "",`,
18974
- ` "4. ВРЕМЕННЫЕ РАМКИ:",`,
18975
- ` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
18976
- ` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
18977
- ` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
18978
- ` ].join("\\n"),`,
18979
- ` },`,
18980
- ` ...messages,`,
18981
- ` ],`,
18982
- ` format: {`,
18983
- ` type: "object",`,
18984
- ` properties: {`,
18985
- ` position: {`,
18986
- ` type: "string",`,
18987
- ` enum: ["wait", "long", "short"],`,
18988
- ` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
18989
- ` },`,
18990
- ` note: {`,
18991
- ` type: "string",`,
18992
- ` description: "Professional trading recommendation with price levels",`,
18993
- ` },`,
18994
- ` priceOpen: {`,
18995
- ` type: "number",`,
18996
- ` description: "Entry price (current market price or limit order price)",`,
18997
- ` },`,
18998
- ` priceTakeProfit: {`,
18999
- ` type: "number",`,
19000
- ` description: "Take profit target price",`,
19001
- ` },`,
19002
- ` priceStopLoss: {`,
19003
- ` type: "number",`,
19004
- ` description: "Stop loss exit price",`,
19005
- ` },`,
19006
- ` minuteEstimatedTime: {`,
19007
- ` type: "number",`,
19008
- ` description: "Expected time to reach TP in minutes (max 360)",`,
19009
- ` },`,
19010
- ` },`,
19011
- ` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
19012
- ` },`,
19013
- ` });`,
19014
- ``,
19015
- ` const jsonResponse = JSON.parse(response.message.content.trim());`,
19016
- ` return jsonResponse;`,
19017
- `}`
19018
- ].join("\n");
19019
- };
18509
+ await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
18510
+ }
19020
18511
  }
19021
- }
19022
-
18512
+ if (shouldPersist) {
18513
+ await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
18514
+ }
18515
+ };
19023
18516
  /**
19024
- * Service for managing optimizer schema registration and retrieval.
19025
- * Provides validation and registry management for optimizer configurations.
19026
- *
19027
- * Uses ToolRegistry for immutable schema storage.
19028
- */
19029
- class OptimizerSchemaService {
19030
- constructor() {
19031
- this.loggerService = inject(TYPES.loggerService);
19032
- this._registry = new functoolsKit.ToolRegistry("optimizerSchema");
19033
- /**
19034
- * Registers a new optimizer schema.
19035
- * Validates required fields before registration.
19036
- *
19037
- * @param key - Unique optimizer name
19038
- * @param value - Optimizer schema configuration
19039
- * @throws Error if schema validation fails
19040
- */
19041
- this.register = (key, value) => {
19042
- this.loggerService.log(`optimizerSchemaService register`, { key });
19043
- this.validateShallow(value);
19044
- this._registry = this._registry.register(key, value);
19045
- };
19046
- /**
19047
- * Validates optimizer schema structure.
19048
- * Checks required fields: optimizerName, rangeTrain, source, getPrompt.
19049
- *
19050
- * @param optimizerSchema - Schema to validate
19051
- * @throws Error if validation fails
19052
- */
19053
- this.validateShallow = (optimizerSchema) => {
19054
- this.loggerService.log(`optimizerTemplateService validateShallow`, {
19055
- optimizerSchema,
19056
- });
19057
- if (typeof optimizerSchema.optimizerName !== "string") {
19058
- throw new Error(`optimizer template validation failed: missing optimizerName`);
19059
- }
19060
- if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
19061
- throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
19062
- }
19063
- if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
19064
- throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
19065
- }
19066
- if (typeof optimizerSchema.getPrompt !== "function") {
19067
- throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
19068
- }
19069
- };
19070
- /**
19071
- * Partially overrides an existing optimizer schema.
19072
- * Merges provided values with existing schema.
19073
- *
19074
- * @param key - Optimizer name to override
19075
- * @param value - Partial schema values to merge
19076
- * @returns Updated complete schema
19077
- * @throws Error if optimizer not found
19078
- */
19079
- this.override = (key, value) => {
19080
- this.loggerService.log(`optimizerSchemaService override`, { key });
19081
- this._registry = this._registry.override(key, value);
19082
- return this._registry.get(key);
19083
- };
19084
- /**
19085
- * Retrieves optimizer schema by name.
19086
- *
19087
- * @param key - Optimizer name
19088
- * @returns Complete optimizer schema
19089
- * @throws Error if optimizer not found
19090
- */
19091
- this.get = (key) => {
19092
- this.loggerService.log(`optimizerSchemaService get`, { key });
19093
- return this._registry.get(key);
19094
- };
19095
- }
19096
- }
19097
-
19098
- /**
19099
- * Service for validating optimizer existence and managing optimizer registry.
19100
- * Maintains a Map of registered optimizers for validation purposes.
19101
- *
19102
- * Uses memoization for efficient repeated validation checks.
19103
- */
19104
- class OptimizerValidationService {
19105
- constructor() {
19106
- this.loggerService = inject(TYPES.loggerService);
19107
- this._optimizerMap = new Map();
19108
- /**
19109
- * Adds optimizer to validation registry.
19110
- * Prevents duplicate optimizer names.
19111
- *
19112
- * @param optimizerName - Unique optimizer identifier
19113
- * @param optimizerSchema - Complete optimizer schema
19114
- * @throws Error if optimizer with same name already exists
19115
- */
19116
- this.addOptimizer = (optimizerName, optimizerSchema) => {
19117
- this.loggerService.log("optimizerValidationService addOptimizer", {
19118
- optimizerName,
19119
- optimizerSchema,
19120
- });
19121
- if (this._optimizerMap.has(optimizerName)) {
19122
- throw new Error(`optimizer ${optimizerName} already exist`);
19123
- }
19124
- this._optimizerMap.set(optimizerName, optimizerSchema);
19125
- };
19126
- /**
19127
- * Validates that optimizer exists in registry.
19128
- * Memoized for performance on repeated checks.
19129
- *
19130
- * @param optimizerName - Optimizer name to validate
19131
- * @param source - Source method name for error messages
19132
- * @throws Error if optimizer not found
19133
- */
19134
- this.validate = functoolsKit.memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
19135
- this.loggerService.log("optimizerValidationService validate", {
19136
- optimizerName,
19137
- source,
19138
- });
19139
- const optimizer = this._optimizerMap.get(optimizerName);
19140
- if (!optimizer) {
19141
- throw new Error(`optimizer ${optimizerName} not found source=${source}`);
19142
- }
19143
- return true;
19144
- });
19145
- /**
19146
- * Lists all registered optimizer schemas.
19147
- *
19148
- * @returns Array of all optimizer schemas
19149
- */
19150
- this.list = async () => {
19151
- this.loggerService.log("optimizerValidationService list");
19152
- return Array.from(this._optimizerMap.values());
19153
- };
19154
- }
19155
- }
19156
-
19157
- const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
19158
- const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
19159
- const METHOD_NAME_DUMP = "optimizerGlobalService dump";
19160
- /**
19161
- * Global service for optimizer operations with validation.
19162
- * Entry point for public API, performs validation before delegating to ConnectionService.
19163
- *
19164
- * Workflow:
19165
- * 1. Log operation
19166
- * 2. Validate optimizer exists
19167
- * 3. Delegate to OptimizerConnectionService
19168
- */
19169
- class OptimizerGlobalService {
19170
- constructor() {
19171
- this.loggerService = inject(TYPES.loggerService);
19172
- this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
19173
- this.optimizerValidationService = inject(TYPES.optimizerValidationService);
19174
- /**
19175
- * Fetches data from all sources and generates strategy metadata.
19176
- * Validates optimizer existence before execution.
19177
- *
19178
- * @param symbol - Trading pair symbol
19179
- * @param optimizerName - Optimizer identifier
19180
- * @returns Array of generated strategies with conversation context
19181
- * @throws Error if optimizer not found
19182
- */
19183
- this.getData = async (symbol, optimizerName) => {
19184
- this.loggerService.log(METHOD_NAME_GET_DATA, {
19185
- symbol,
19186
- optimizerName,
19187
- });
19188
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
19189
- return await this.optimizerConnectionService.getData(symbol, optimizerName);
19190
- };
19191
- /**
19192
- * Generates complete executable strategy code.
19193
- * Validates optimizer existence before execution.
19194
- *
19195
- * @param symbol - Trading pair symbol
19196
- * @param optimizerName - Optimizer identifier
19197
- * @returns Generated TypeScript/JavaScript code as string
19198
- * @throws Error if optimizer not found
19199
- */
19200
- this.getCode = async (symbol, optimizerName) => {
19201
- this.loggerService.log(METHOD_NAME_GET_CODE, {
19202
- symbol,
19203
- optimizerName,
19204
- });
19205
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
19206
- return await this.optimizerConnectionService.getCode(symbol, optimizerName);
19207
- };
19208
- /**
19209
- * Generates and saves strategy code to file.
19210
- * Validates optimizer existence before execution.
19211
- *
19212
- * @param symbol - Trading pair symbol
19213
- * @param optimizerName - Optimizer identifier
19214
- * @param path - Output directory path (optional)
19215
- * @throws Error if optimizer not found
19216
- */
19217
- this.dump = async (symbol, optimizerName, path) => {
19218
- this.loggerService.log(METHOD_NAME_DUMP, {
19219
- symbol,
19220
- optimizerName,
19221
- path,
19222
- });
19223
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
19224
- return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
19225
- };
19226
- }
19227
- }
19228
-
19229
- const ITERATION_LIMIT = 25;
19230
- const DEFAULT_SOURCE_NAME = "unknown";
19231
- const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
19232
- /**
19233
- * Wrapper to call onSourceData callback with error handling.
19234
- * Catches and logs any errors thrown by the user-provided callback.
19235
- */
19236
- const CALL_SOURCE_DATA_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, name, data, startDate, endDate) => {
19237
- if (self.params.callbacks?.onSourceData) {
19238
- await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
19239
- }
19240
- }, {
19241
- fallback: (error) => {
19242
- const message = "ClientOptimizer CALL_SOURCE_DATA_CALLBACKS_FN thrown";
19243
- const payload = {
19244
- error: functoolsKit.errorData(error),
19245
- message: functoolsKit.getErrorMessage(error),
19246
- };
19247
- bt.loggerService.warn(message, payload);
19248
- console.warn(message, payload);
19249
- errorEmitter.next(error);
19250
- },
19251
- });
19252
- /**
19253
- * Wrapper to call onData callback with error handling.
19254
- * Catches and logs any errors thrown by the user-provided callback.
19255
- */
19256
- const CALL_DATA_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, strategyList) => {
19257
- if (self.params.callbacks?.onData) {
19258
- await self.params.callbacks.onData(symbol, strategyList);
19259
- }
19260
- }, {
19261
- fallback: (error) => {
19262
- const message = "ClientOptimizer CALL_DATA_CALLBACKS_FN thrown";
19263
- const payload = {
19264
- error: functoolsKit.errorData(error),
19265
- message: functoolsKit.getErrorMessage(error),
19266
- };
19267
- bt.loggerService.warn(message, payload);
19268
- console.warn(message, payload);
19269
- errorEmitter.next(error);
19270
- },
19271
- });
19272
- /**
19273
- * Wrapper to call onCode callback with error handling.
19274
- * Catches and logs any errors thrown by the user-provided callback.
19275
- */
19276
- const CALL_CODE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, code) => {
19277
- if (self.params.callbacks?.onCode) {
19278
- await self.params.callbacks.onCode(symbol, code);
19279
- }
19280
- }, {
19281
- fallback: (error) => {
19282
- const message = "ClientOptimizer CALL_CODE_CALLBACKS_FN thrown";
19283
- const payload = {
19284
- error: functoolsKit.errorData(error),
19285
- message: functoolsKit.getErrorMessage(error),
19286
- };
19287
- bt.loggerService.warn(message, payload);
19288
- console.warn(message, payload);
19289
- errorEmitter.next(error);
19290
- },
19291
- });
19292
- /**
19293
- * Wrapper to call onDump callback with error handling.
19294
- * Catches and logs any errors thrown by the user-provided callback.
19295
- */
19296
- const CALL_DUMP_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, filepath) => {
19297
- if (self.params.callbacks?.onDump) {
19298
- await self.params.callbacks.onDump(symbol, filepath);
19299
- }
19300
- }, {
19301
- fallback: (error) => {
19302
- const message = "ClientOptimizer CALL_DUMP_CALLBACKS_FN thrown";
19303
- const payload = {
19304
- error: functoolsKit.errorData(error),
19305
- message: functoolsKit.getErrorMessage(error),
19306
- };
19307
- bt.loggerService.warn(message, payload);
19308
- console.warn(message, payload);
19309
- errorEmitter.next(error);
19310
- },
19311
- });
19312
- /**
19313
- * Default user message formatter.
19314
- * Delegates to template's getUserMessage method.
19315
- *
19316
- * @param symbol - Trading pair symbol
19317
- * @param data - Fetched data array
19318
- * @param name - Source name
19319
- * @param self - ClientOptimizer instance
19320
- * @returns Formatted user message content
19321
- */
19322
- const DEFAULT_USER_FN = async (symbol, data, name, self) => {
19323
- return await self.params.template.getUserMessage(symbol, data, name);
19324
- };
19325
- /**
19326
- * Default assistant message formatter.
19327
- * Delegates to template's getAssistantMessage method.
19328
- *
19329
- * @param symbol - Trading pair symbol
19330
- * @param data - Fetched data array
19331
- * @param name - Source name
19332
- * @param self - ClientOptimizer instance
19333
- * @returns Formatted assistant message content
19334
- */
19335
- const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
19336
- return await self.params.template.getAssistantMessage(symbol, data, name);
19337
- };
19338
- /**
19339
- * Resolves paginated data from source with deduplication.
19340
- * Uses iterateDocuments to handle pagination automatically.
19341
- *
19342
- * @param fetch - Source fetch function
19343
- * @param filterData - Filter arguments (symbol, dates)
19344
- * @returns Deduplicated array of all fetched data
19345
- */
19346
- const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
19347
- const iterator = functoolsKit.iterateDocuments({
19348
- limit: ITERATION_LIMIT,
19349
- async createRequest({ limit, offset }) {
19350
- return await fetch({
19351
- symbol: filterData.symbol,
19352
- startDate: filterData.startDate,
19353
- endDate: filterData.endDate,
19354
- limit,
19355
- offset,
19356
- });
19357
- },
19358
- });
19359
- const distinct = functoolsKit.distinctDocuments(iterator, (data) => data.id);
19360
- return await functoolsKit.resolveDocuments(distinct);
19361
- };
19362
- /**
19363
- * Collects data from all sources and generates strategy metadata.
19364
- * Iterates through training ranges, fetches data from each source,
19365
- * builds LLM conversation history, and generates strategy prompts.
19366
- *
19367
- * @param symbol - Trading pair symbol
19368
- * @param self - ClientOptimizer instance
19369
- * @returns Array of generated strategies with conversation context
19370
- */
19371
- const GET_STRATEGY_DATA_FN = async (symbol, self) => {
19372
- const strategyList = [];
19373
- const totalSources = self.params.rangeTrain.length * self.params.source.length;
19374
- let processedSources = 0;
19375
- for (const { startDate, endDate } of self.params.rangeTrain) {
19376
- const messageList = [];
19377
- for (const source of self.params.source) {
19378
- // Emit progress event at the start of processing each source
19379
- await self.onProgress({
19380
- optimizerName: self.params.optimizerName,
19381
- symbol,
19382
- totalSources,
19383
- processedSources,
19384
- progress: totalSources > 0 ? processedSources / totalSources : 0,
19385
- });
19386
- if (typeof source === "function") {
19387
- const data = await RESOLVE_PAGINATION_FN(source, {
19388
- symbol,
19389
- startDate,
19390
- endDate,
19391
- });
19392
- await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
19393
- const [userContent, assistantContent] = await Promise.all([
19394
- DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
19395
- DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
19396
- ]);
19397
- messageList.push({
19398
- role: "user",
19399
- content: userContent,
19400
- }, {
19401
- role: "assistant",
19402
- content: assistantContent,
19403
- });
19404
- processedSources++;
19405
- }
19406
- else {
19407
- const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
19408
- const data = await RESOLVE_PAGINATION_FN(fetch, {
19409
- symbol,
19410
- startDate,
19411
- endDate,
19412
- });
19413
- await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, name, data, startDate, endDate);
19414
- const [userContent, assistantContent] = await Promise.all([
19415
- user(symbol, data, name, self),
19416
- assistant(symbol, data, name, self),
19417
- ]);
19418
- messageList.push({
19419
- role: "user",
19420
- content: userContent,
19421
- }, {
19422
- role: "assistant",
19423
- content: assistantContent,
19424
- });
19425
- processedSources++;
19426
- }
19427
- const name = "name" in source
19428
- ? source.name || DEFAULT_SOURCE_NAME
19429
- : DEFAULT_SOURCE_NAME;
19430
- strategyList.push({
19431
- symbol,
19432
- name,
19433
- messages: messageList,
19434
- strategy: await self.params.getPrompt(symbol, messageList),
19435
- });
19436
- }
19437
- }
19438
- // Emit final progress event (100%)
19439
- await self.onProgress({
19440
- optimizerName: self.params.optimizerName,
19441
- symbol,
19442
- totalSources,
19443
- processedSources: totalSources,
19444
- progress: 1.0,
19445
- });
19446
- await CALL_DATA_CALLBACKS_FN(self, symbol, strategyList);
19447
- return strategyList;
19448
- };
19449
- /**
19450
- * Generates complete executable strategy code.
19451
- * Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
19452
- *
19453
- * @param symbol - Trading pair symbol
19454
- * @param self - ClientOptimizer instance
19455
- * @returns Generated TypeScript/JavaScript code as string
19456
- */
19457
- const GET_STRATEGY_CODE_FN = async (symbol, self) => {
19458
- const strategyData = await self.getData(symbol);
19459
- const prefix = CREATE_PREFIX_FN();
19460
- const sections = [];
19461
- const exchangeName = `${prefix}_exchange`;
19462
- // 1. Top banner with imports
19463
- {
19464
- sections.push(await self.params.template.getTopBanner(symbol));
19465
- sections.push("");
19466
- }
19467
- // 2. JSON dump helper function
19468
- {
19469
- sections.push(await self.params.template.getJsonDumpTemplate(symbol));
19470
- sections.push("");
19471
- }
19472
- // 3. Helper functions (text and json)
19473
- {
19474
- sections.push(await self.params.template.getTextTemplate(symbol));
19475
- sections.push("");
19476
- }
19477
- {
19478
- sections.push(await self.params.template.getJsonTemplate(symbol));
19479
- sections.push("");
19480
- }
19481
- // 4. Exchange template (assuming first strategy has exchange info)
19482
- {
19483
- sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
19484
- sections.push("");
19485
- }
19486
- // 5. Train frame templates
19487
- {
19488
- for (let i = 0; i < self.params.rangeTrain.length; i++) {
19489
- const range = self.params.rangeTrain[i];
19490
- const frameName = `${prefix}_train_frame-${i + 1}`;
19491
- sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
19492
- range.startDate, range.endDate));
19493
- sections.push("");
19494
- }
19495
- }
19496
- // 6. Test frame template
19497
- {
19498
- const testFrameName = `${prefix}_test_frame`;
19499
- sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
19500
- self.params.rangeTest.startDate, self.params.rangeTest.endDate));
19501
- sections.push("");
19502
- }
19503
- // 7. Strategy templates for each generated strategy
19504
- {
19505
- for (let i = 0; i < strategyData.length; i++) {
19506
- const strategy = strategyData[i];
19507
- const strategyName = `${prefix}_strategy-${i + 1}`;
19508
- const interval = "5m"; // default interval
19509
- sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
19510
- sections.push("");
19511
- }
19512
- }
19513
- // 8. Walker template (uses test frame for validation)
19514
- {
19515
- const walkerName = `${prefix}_walker`;
19516
- const testFrameName = `${prefix}_test_frame`;
19517
- const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
19518
- sections.push(await self.params.template.getWalkerTemplate(walkerName, `${exchangeName}`, testFrameName, strategies));
19519
- sections.push("");
19520
- }
19521
- // 9. Launcher template
19522
- {
19523
- const walkerName = `${prefix}_walker`;
19524
- sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
19525
- sections.push("");
19526
- }
19527
- const code = sections.join("\n");
19528
- await CALL_CODE_CALLBACKS_FN(self, symbol, code);
19529
- return code;
19530
- };
19531
- /**
19532
- * Saves generated strategy code to file.
19533
- * Creates directory if needed, writes .mjs file with generated code.
19534
- *
19535
- * @param symbol - Trading pair symbol
19536
- * @param path - Output directory path
19537
- * @param self - ClientOptimizer instance
19538
- */
19539
- const GET_STRATEGY_DUMP_FN = async (symbol, path$1, self) => {
19540
- const report = await self.getCode(symbol);
19541
- try {
19542
- const dir = path.join(process.cwd(), path$1);
19543
- await fs.mkdir(dir, { recursive: true });
19544
- const filename = `${self.params.optimizerName}_${symbol}.mjs`;
19545
- const filepath = path.join(dir, filename);
19546
- await fs.writeFile(filepath, report, "utf-8");
19547
- self.params.logger.info(`Optimizer report saved: ${filepath}`);
19548
- await CALL_DUMP_CALLBACKS_FN(self, symbol, filepath);
19549
- }
19550
- catch (error) {
19551
- self.params.logger.warn(`Failed to save optimizer report:`, error);
19552
- throw error;
19553
- }
19554
- };
19555
- /**
19556
- * Client implementation for optimizer operations.
19557
- *
19558
- * Features:
19559
- * - Data collection from multiple sources with pagination
19560
- * - LLM conversation history building
19561
- * - Strategy code generation with templates
19562
- * - File export with callbacks
19563
- *
19564
- * Used by OptimizerConnectionService to create optimizer instances.
19565
- */
19566
- class ClientOptimizer {
19567
- constructor(params, onProgress) {
19568
- this.params = params;
19569
- this.onProgress = onProgress;
19570
- /**
19571
- * Fetches data from all sources and generates strategy metadata.
19572
- * Processes each training range and builds LLM conversation history.
19573
- *
19574
- * @param symbol - Trading pair symbol
19575
- * @returns Array of generated strategies with conversation context
19576
- */
19577
- this.getData = async (symbol) => {
19578
- this.params.logger.debug("ClientOptimizer getData", {
19579
- symbol,
19580
- });
19581
- return await GET_STRATEGY_DATA_FN(symbol, this);
19582
- };
19583
- /**
19584
- * Generates complete executable strategy code.
19585
- * Includes imports, helpers, strategies, walker, and launcher.
19586
- *
19587
- * @param symbol - Trading pair symbol
19588
- * @returns Generated TypeScript/JavaScript code as string
19589
- */
19590
- this.getCode = async (symbol) => {
19591
- this.params.logger.debug("ClientOptimizer getCode", {
19592
- symbol,
19593
- });
19594
- return await GET_STRATEGY_CODE_FN(symbol, this);
19595
- };
19596
- /**
19597
- * Generates and saves strategy code to file.
19598
- * Creates directory if needed, writes .mjs file.
19599
- *
19600
- * @param symbol - Trading pair symbol
19601
- * @param path - Output directory path (default: "./")
19602
- */
19603
- this.dump = async (symbol, path = "./") => {
19604
- this.params.logger.debug("ClientOptimizer dump", {
19605
- symbol,
19606
- path,
19607
- });
19608
- return await GET_STRATEGY_DUMP_FN(symbol, path, this);
19609
- };
19610
- }
19611
- }
19612
-
19613
- /**
19614
- * Callback function for emitting progress events to progressOptimizerEmitter.
19615
- */
19616
- const COMMIT_PROGRESS_FN = async (progress) => progressOptimizerEmitter.next(progress);
19617
- /**
19618
- * Service for creating and caching optimizer client instances.
19619
- * Handles dependency injection and template merging.
19620
- *
19621
- * Features:
19622
- * - Memoized optimizer instances (one per optimizerName)
19623
- * - Template merging (custom + defaults)
19624
- * - Logger injection
19625
- * - Delegates to ClientOptimizer for actual operations
19626
- */
19627
- class OptimizerConnectionService {
19628
- constructor() {
19629
- this.loggerService = inject(TYPES.loggerService);
19630
- this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
19631
- this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
19632
- /**
19633
- * Creates or retrieves cached optimizer instance.
19634
- * Memoized by optimizerName for performance.
19635
- *
19636
- * Merges custom templates from schema with defaults from OptimizerTemplateService.
19637
- *
19638
- * @param optimizerName - Unique optimizer identifier
19639
- * @returns ClientOptimizer instance with resolved dependencies
19640
- */
19641
- this.getOptimizer = functoolsKit.memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
19642
- const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks, } = this.optimizerSchemaService.get(optimizerName);
19643
- 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;
19644
- const template = {
19645
- getAssistantMessage,
19646
- getExchangeTemplate,
19647
- getFrameTemplate,
19648
- getJsonDumpTemplate,
19649
- getJsonTemplate,
19650
- getLauncherTemplate,
19651
- getStrategyTemplate,
19652
- getTextTemplate,
19653
- getWalkerTemplate,
19654
- getTopBanner,
19655
- getUserMessage,
19656
- };
19657
- return new ClientOptimizer({
19658
- optimizerName,
19659
- logger: this.loggerService,
19660
- getPrompt,
19661
- rangeTest,
19662
- rangeTrain,
19663
- source,
19664
- template,
19665
- callbacks,
19666
- }, COMMIT_PROGRESS_FN);
19667
- });
19668
- /**
19669
- * Fetches data from all sources and generates strategy metadata.
19670
- *
19671
- * @param symbol - Trading pair symbol
19672
- * @param optimizerName - Optimizer identifier
19673
- * @returns Array of generated strategies with conversation context
19674
- */
19675
- this.getData = async (symbol, optimizerName) => {
19676
- this.loggerService.log("optimizerConnectionService getData", {
19677
- symbol,
19678
- optimizerName,
19679
- });
19680
- const optimizer = this.getOptimizer(optimizerName);
19681
- return await optimizer.getData(symbol);
19682
- };
19683
- /**
19684
- * Generates complete executable strategy code.
19685
- *
19686
- * @param symbol - Trading pair symbol
19687
- * @param optimizerName - Optimizer identifier
19688
- * @returns Generated TypeScript/JavaScript code as string
19689
- */
19690
- this.getCode = async (symbol, optimizerName) => {
19691
- this.loggerService.log("optimizerConnectionService getCode", {
19692
- symbol,
19693
- optimizerName,
19694
- });
19695
- const optimizer = this.getOptimizer(optimizerName);
19696
- return await optimizer.getCode(symbol);
19697
- };
19698
- /**
19699
- * Generates and saves strategy code to file.
19700
- *
19701
- * @param symbol - Trading pair symbol
19702
- * @param optimizerName - Optimizer identifier
19703
- * @param path - Output directory path (optional)
19704
- */
19705
- this.dump = async (symbol, optimizerName, path) => {
19706
- this.loggerService.log("optimizerConnectionService getCode", {
19707
- symbol,
19708
- optimizerName,
19709
- });
19710
- const optimizer = this.getOptimizer(optimizerName);
19711
- return await optimizer.dump(symbol, path);
19712
- };
19713
- }
19714
- }
19715
-
19716
- /**
19717
- * Symbol marker indicating that partial state needs initialization.
19718
- * Used as sentinel value for _states before waitForInit() is called.
19719
- */
19720
- const NEED_FETCH$1 = Symbol("need_fetch");
19721
- /**
19722
- * Array of profit level milestones to track (10%, 20%, ..., 100%).
19723
- * Each level is checked during profit() method to emit events for newly reached levels.
19724
- */
19725
- const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
19726
- /**
19727
- * Array of loss level milestones to track (-10%, -20%, ..., -100%).
19728
- * Each level is checked during loss() method to emit events for newly reached levels.
19729
- */
19730
- const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
19731
- /**
19732
- * Internal profit handler function for ClientPartial.
19733
- *
19734
- * Checks which profit levels have been reached and emits events for new levels only.
19735
- * Uses Set-based deduplication to prevent duplicate events.
19736
- *
19737
- * @param symbol - Trading pair symbol
19738
- * @param data - Signal row data
19739
- * @param currentPrice - Current market price
19740
- * @param revenuePercent - Current profit percentage (positive value)
19741
- * @param backtest - True if backtest mode
19742
- * @param when - Event timestamp
19743
- * @param self - ClientPartial instance reference
19744
- */
19745
- const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
19746
- if (self._states === NEED_FETCH$1) {
19747
- throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
19748
- }
19749
- if (data.id !== self.params.signalId) {
19750
- throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
19751
- }
19752
- let state = self._states.get(data.id);
19753
- if (!state) {
19754
- state = {
19755
- profitLevels: new Set(),
19756
- lossLevels: new Set(),
19757
- };
19758
- self._states.set(data.id, state);
19759
- }
19760
- let shouldPersist = false;
19761
- for (const level of PROFIT_LEVELS) {
19762
- if (revenuePercent >= level && !state.profitLevels.has(level)) {
19763
- state.profitLevels.add(level);
19764
- shouldPersist = true;
19765
- self.params.logger.debug("ClientPartial profit level reached", {
19766
- symbol,
19767
- signalId: data.id,
19768
- level,
19769
- revenuePercent,
19770
- backtest,
19771
- });
19772
- await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
19773
- }
19774
- }
19775
- if (shouldPersist) {
19776
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
19777
- }
19778
- };
19779
- /**
19780
- * Internal loss handler function for ClientPartial.
19781
- *
19782
- * Checks which loss levels have been reached and emits events for new levels only.
19783
- * Uses Set-based deduplication to prevent duplicate events.
19784
- * Converts negative lossPercent to absolute value for level comparison.
19785
- *
19786
- * @param symbol - Trading pair symbol
19787
- * @param data - Signal row data
19788
- * @param currentPrice - Current market price
19789
- * @param lossPercent - Current loss percentage (negative value)
19790
- * @param backtest - True if backtest mode
19791
- * @param when - Event timestamp
19792
- * @param self - ClientPartial instance reference
19793
- */
19794
- const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
19795
- if (self._states === NEED_FETCH$1) {
19796
- throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
19797
- }
19798
- if (data.id !== self.params.signalId) {
19799
- throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
19800
- }
19801
- let state = self._states.get(data.id);
19802
- if (!state) {
19803
- state = {
19804
- profitLevels: new Set(),
19805
- lossLevels: new Set(),
19806
- };
19807
- self._states.set(data.id, state);
19808
- }
19809
- const absLoss = Math.abs(lossPercent);
19810
- let shouldPersist = false;
19811
- for (const level of LOSS_LEVELS) {
19812
- if (absLoss >= level && !state.lossLevels.has(level)) {
19813
- state.lossLevels.add(level);
19814
- shouldPersist = true;
19815
- self.params.logger.debug("ClientPartial loss level reached", {
19816
- symbol,
19817
- signalId: data.id,
19818
- level,
19819
- lossPercent,
19820
- backtest,
19821
- });
19822
- await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
19823
- }
19824
- }
19825
- if (shouldPersist) {
19826
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
19827
- }
19828
- };
19829
- /**
19830
- * Internal initialization function for ClientPartial.
18517
+ * Internal initialization function for ClientPartial.
19831
18518
  *
19832
18519
  * Loads persisted partial state from disk and restores in-memory Maps.
19833
18520
  * Converts serialized arrays back to Sets for O(1) lookups.
@@ -22010,162 +20697,6 @@ class BreakevenGlobalService {
22010
20697
  }
22011
20698
  }
22012
20699
 
22013
- /**
22014
- * Warning threshold for message size in kilobytes.
22015
- * Messages exceeding this size trigger console warnings.
22016
- */
22017
- const WARN_KB = 30;
22018
- /**
22019
- * Internal function for dumping signal data to markdown files.
22020
- * Creates a directory structure with system prompts, user messages, and LLM output.
22021
- *
22022
- * @param signalId - Unique identifier for the result
22023
- * @param history - Array of message models from LLM conversation
22024
- * @param signal - Signal DTO with trade parameters
22025
- * @param outputDir - Output directory path (default: "./dump/strategy")
22026
- * @returns Promise that resolves when all files are written
22027
- */
22028
- const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/outline") => {
22029
- // Extract system messages and system reminders from existing data
22030
- const systemMessages = history.filter((m) => m.role === "system");
22031
- const userMessages = history.filter((m) => m.role === "user");
22032
- const subfolderPath = path.join(outputDir, String(signalId));
22033
- // Generate system prompt markdown
22034
- {
22035
- let summary = "# Outline Result Summary\n";
22036
- {
22037
- summary += "\n";
22038
- summary += `**ResultId**: ${String(signalId)}\n`;
22039
- summary += "\n";
22040
- }
22041
- if (signal) {
22042
- summary += "## Output Data\n\n";
22043
- summary += "```json\n";
22044
- summary += JSON.stringify(signal, null, 2);
22045
- summary += "\n```\n\n";
22046
- }
22047
- // Add system messages to summary
22048
- if (systemMessages.length > 0) {
22049
- summary += "## System Messages\n\n";
22050
- systemMessages.forEach((msg, idx) => {
22051
- summary += `### System Message ${idx + 1}\n\n`;
22052
- summary += msg.content;
22053
- summary += "\n";
22054
- });
22055
- }
22056
- await Markdown.writeData("outline", summary, {
22057
- path: subfolderPath,
22058
- file: "00_system_prompt.md",
22059
- symbol: "",
22060
- signalId: String(signalId),
22061
- strategyName: "",
22062
- exchangeName: "",
22063
- frameName: ""
22064
- });
22065
- }
22066
- // Generate user messages
22067
- {
22068
- await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
22069
- const messageNum = String(idx + 1).padStart(2, "0");
22070
- const contentFileName = `${messageNum}_user_message.md`;
22071
- {
22072
- const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
22073
- const messageSizeKb = Math.floor(messageSizeBytes / 1024);
22074
- if (messageSizeKb > WARN_KB) {
22075
- console.warn(`User message ${idx + 1} is ${messageSizeBytes} bytes (${messageSizeKb}kb), which exceeds warning limit`);
22076
- }
22077
- }
22078
- let content = `# User Input ${idx + 1}\n\n`;
22079
- content += `**ResultId**: ${String(signalId)}\n\n`;
22080
- content += message.content;
22081
- content += "\n";
22082
- await Markdown.writeData("outline", content, {
22083
- path: subfolderPath,
22084
- file: contentFileName,
22085
- signalId: String(signalId),
22086
- symbol: "",
22087
- strategyName: "",
22088
- exchangeName: "",
22089
- frameName: ""
22090
- });
22091
- }));
22092
- }
22093
- // Generate LLM output
22094
- {
22095
- const messageNum = String(userMessages.length + 1).padStart(2, "0");
22096
- const contentFileName = `${messageNum}_llm_output.md`;
22097
- let content = "# Full Outline Result\n\n";
22098
- content += `**ResultId**: ${String(signalId)}\n\n`;
22099
- if (signal) {
22100
- content += "## Output Data\n\n";
22101
- content += "```json\n";
22102
- content += JSON.stringify(signal, null, 2);
22103
- content += "\n```\n";
22104
- }
22105
- await Markdown.writeData("outline", content, {
22106
- path: subfolderPath,
22107
- file: contentFileName,
22108
- symbol: "",
22109
- signalId: String(signalId),
22110
- strategyName: "",
22111
- exchangeName: "",
22112
- frameName: ""
22113
- });
22114
- }
22115
- };
22116
- /**
22117
- * Service for generating markdown documentation from LLM outline results.
22118
- * Used by AI Strategy Optimizer to save debug logs and conversation history.
22119
- *
22120
- * Creates directory structure:
22121
- * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
22122
- * - ./dump/strategy/{signalId}/01_user_message.md - First user input
22123
- * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
22124
- * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
22125
- */
22126
- class OutlineMarkdownService {
22127
- constructor() {
22128
- /** Logger service injected via DI */
22129
- this.loggerService = inject(TYPES.loggerService);
22130
- /**
22131
- * Dumps signal data and conversation history to markdown files.
22132
- * Skips if directory already exists to avoid overwriting previous results.
22133
- *
22134
- * Generated files:
22135
- * - 00_system_prompt.md - System messages and output summary
22136
- * - XX_user_message.md - Each user message in separate file (numbered)
22137
- * - XX_llm_output.md - Final LLM output with signal data
22138
- *
22139
- * @param signalId - Unique identifier for the result (used as directory name)
22140
- * @param history - Array of message models from LLM conversation
22141
- * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
22142
- * @param outputDir - Output directory path (default: "./dump/strategy")
22143
- * @returns Promise that resolves when all files are written
22144
- *
22145
- * @example
22146
- * ```typescript
22147
- * await outlineService.dumpSignal(
22148
- * "strategy-1",
22149
- * conversationHistory,
22150
- * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
22151
- * );
22152
- * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
22153
- * // ./dump/strategy/strategy-1/01_user_message.md
22154
- * // ./dump/strategy/strategy-1/02_llm_output.md
22155
- * ```
22156
- */
22157
- this.dumpSignal = async (signalId, history, signal, outputDir = "./dump/strategy") => {
22158
- this.loggerService.log("outlineMarkdownService dumpSignal", {
22159
- signalId,
22160
- history,
22161
- signal,
22162
- outputDir,
22163
- });
22164
- return await DUMP_SIGNAL_FN(signalId, history, signal, outputDir);
22165
- };
22166
- }
22167
- }
22168
-
22169
20700
  /**
22170
20701
  * Service for validating GLOBAL_CONFIG parameters to ensure mathematical correctness
22171
20702
  * and prevent unprofitable trading configurations.
@@ -24551,111 +23082,6 @@ class RiskReportService {
24551
23082
  }
24552
23083
  }
24553
23084
 
24554
- const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
24555
- /**
24556
- * Default fallback prompt configuration.
24557
- * Used when signal.prompt.cjs file is not found.
24558
- */
24559
- const DEFAULT_PROMPT = {
24560
- user: "",
24561
- system: [],
24562
- };
24563
- /**
24564
- * Lazy-loads and caches signal prompt configuration.
24565
- * Attempts to load from config/prompt/signal.prompt.cjs, falls back to DEFAULT_PROMPT if not found.
24566
- * Uses singleshot pattern to ensure configuration is loaded only once.
24567
- * @returns Prompt configuration with system and user prompts
24568
- */
24569
- const GET_PROMPT_FN = functoolsKit.singleshot(() => {
24570
- try {
24571
- const modulePath = require$1.resolve(path.join(process.cwd(), `./config/prompt/signal.prompt.cjs`));
24572
- console.log(`Using ${modulePath} implementation as signal.prompt.cjs`);
24573
- return require$1(modulePath);
24574
- }
24575
- catch (error) {
24576
- console.log(`Using empty fallback for signal.prompt.cjs`, error);
24577
- return DEFAULT_PROMPT;
24578
- }
24579
- });
24580
- /**
24581
- * Service for managing signal prompts for AI/LLM integrations.
24582
- *
24583
- * Provides access to system and user prompts configured in signal.prompt.cjs.
24584
- * Supports both static prompt arrays and dynamic prompt functions.
24585
- *
24586
- * Key responsibilities:
24587
- * - Lazy-loads prompt configuration from config/prompt/signal.prompt.cjs
24588
- * - Resolves system prompts (static arrays or async functions)
24589
- * - Provides user prompt strings
24590
- * - Falls back to empty prompts if configuration is missing
24591
- *
24592
- * Used for AI-powered signal analysis and strategy recommendations.
24593
- */
24594
- class SignalPromptService {
24595
- constructor() {
24596
- this.loggerService = inject(TYPES.loggerService);
24597
- /**
24598
- * Retrieves system prompts for AI context.
24599
- *
24600
- * System prompts can be:
24601
- * - Static array of strings (returned directly)
24602
- * - Async/sync function returning string array (executed and awaited)
24603
- * - Undefined (returns empty array)
24604
- *
24605
- * @param symbol - Trading symbol (e.g., "BTCUSDT")
24606
- * @param strategyName - Strategy identifier
24607
- * @param exchangeName - Exchange identifier
24608
- * @param frameName - Timeframe identifier
24609
- * @param backtest - Whether running in backtest mode
24610
- * @returns Promise resolving to array of system prompt strings
24611
- */
24612
- this.getSystemPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
24613
- this.loggerService.log("signalPromptService getSystemPrompt", {
24614
- symbol,
24615
- strategyName,
24616
- exchangeName,
24617
- frameName,
24618
- backtest,
24619
- });
24620
- const { system } = GET_PROMPT_FN();
24621
- if (Array.isArray(system)) {
24622
- return system;
24623
- }
24624
- if (typeof system === "function") {
24625
- return await system(symbol, strategyName, exchangeName, frameName, backtest);
24626
- }
24627
- return [];
24628
- };
24629
- /**
24630
- * Retrieves user prompt string for AI input.
24631
- *
24632
- * @param symbol - Trading symbol (e.g., "BTCUSDT")
24633
- * @param strategyName - Strategy identifier
24634
- * @param exchangeName - Exchange identifier
24635
- * @param frameName - Timeframe identifier
24636
- * @param backtest - Whether running in backtest mode
24637
- * @returns Promise resolving to user prompt string
24638
- */
24639
- this.getUserPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
24640
- this.loggerService.log("signalPromptService getUserPrompt", {
24641
- symbol,
24642
- strategyName,
24643
- exchangeName,
24644
- frameName,
24645
- backtest,
24646
- });
24647
- const { user } = GET_PROMPT_FN();
24648
- if (typeof user === "string") {
24649
- return user;
24650
- }
24651
- if (typeof user === "function") {
24652
- return await user(symbol, strategyName, exchangeName, frameName, backtest);
24653
- }
24654
- return "";
24655
- };
24656
- }
24657
- }
24658
-
24659
23085
  {
24660
23086
  provide(TYPES.loggerService, () => new LoggerService());
24661
23087
  }
@@ -24670,7 +23096,6 @@ class SignalPromptService {
24670
23096
  provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
24671
23097
  provide(TYPES.riskConnectionService, () => new RiskConnectionService());
24672
23098
  provide(TYPES.actionConnectionService, () => new ActionConnectionService());
24673
- provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
24674
23099
  provide(TYPES.partialConnectionService, () => new PartialConnectionService());
24675
23100
  provide(TYPES.breakevenConnectionService, () => new BreakevenConnectionService());
24676
23101
  }
@@ -24682,7 +23107,6 @@ class SignalPromptService {
24682
23107
  provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
24683
23108
  provide(TYPES.riskSchemaService, () => new RiskSchemaService());
24684
23109
  provide(TYPES.actionSchemaService, () => new ActionSchemaService());
24685
- provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
24686
23110
  }
24687
23111
  {
24688
23112
  provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
@@ -24693,7 +23117,6 @@ class SignalPromptService {
24693
23117
  {
24694
23118
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
24695
23119
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
24696
- provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
24697
23120
  provide(TYPES.partialGlobalService, () => new PartialGlobalService());
24698
23121
  provide(TYPES.breakevenGlobalService, () => new BreakevenGlobalService());
24699
23122
  }
@@ -24721,7 +23144,6 @@ class SignalPromptService {
24721
23144
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
24722
23145
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
24723
23146
  provide(TYPES.breakevenMarkdownService, () => new BreakevenMarkdownService());
24724
- provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
24725
23147
  provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
24726
23148
  }
24727
23149
  {
@@ -24743,16 +23165,9 @@ class SignalPromptService {
24743
23165
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
24744
23166
  provide(TYPES.riskValidationService, () => new RiskValidationService());
24745
23167
  provide(TYPES.actionValidationService, () => new ActionValidationService());
24746
- provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
24747
23168
  provide(TYPES.configValidationService, () => new ConfigValidationService());
24748
23169
  provide(TYPES.columnValidationService, () => new ColumnValidationService());
24749
23170
  }
24750
- {
24751
- provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
24752
- }
24753
- {
24754
- provide(TYPES.signalPromptService, () => new SignalPromptService());
24755
- }
24756
23171
 
24757
23172
  const baseServices = {
24758
23173
  loggerService: inject(TYPES.loggerService),
@@ -24768,7 +23183,6 @@ const connectionServices = {
24768
23183
  sizingConnectionService: inject(TYPES.sizingConnectionService),
24769
23184
  riskConnectionService: inject(TYPES.riskConnectionService),
24770
23185
  actionConnectionService: inject(TYPES.actionConnectionService),
24771
- optimizerConnectionService: inject(TYPES.optimizerConnectionService),
24772
23186
  partialConnectionService: inject(TYPES.partialConnectionService),
24773
23187
  breakevenConnectionService: inject(TYPES.breakevenConnectionService),
24774
23188
  };
@@ -24780,7 +23194,6 @@ const schemaServices = {
24780
23194
  sizingSchemaService: inject(TYPES.sizingSchemaService),
24781
23195
  riskSchemaService: inject(TYPES.riskSchemaService),
24782
23196
  actionSchemaService: inject(TYPES.actionSchemaService),
24783
- optimizerSchemaService: inject(TYPES.optimizerSchemaService),
24784
23197
  };
24785
23198
  const coreServices = {
24786
23199
  exchangeCoreService: inject(TYPES.exchangeCoreService),
@@ -24791,7 +23204,6 @@ const coreServices = {
24791
23204
  const globalServices = {
24792
23205
  sizingGlobalService: inject(TYPES.sizingGlobalService),
24793
23206
  riskGlobalService: inject(TYPES.riskGlobalService),
24794
- optimizerGlobalService: inject(TYPES.optimizerGlobalService),
24795
23207
  partialGlobalService: inject(TYPES.partialGlobalService),
24796
23208
  breakevenGlobalService: inject(TYPES.breakevenGlobalService),
24797
23209
  };
@@ -24819,7 +23231,6 @@ const markdownServices = {
24819
23231
  heatMarkdownService: inject(TYPES.heatMarkdownService),
24820
23232
  partialMarkdownService: inject(TYPES.partialMarkdownService),
24821
23233
  breakevenMarkdownService: inject(TYPES.breakevenMarkdownService),
24822
- outlineMarkdownService: inject(TYPES.outlineMarkdownService),
24823
23234
  riskMarkdownService: inject(TYPES.riskMarkdownService),
24824
23235
  };
24825
23236
  const reportServices = {
@@ -24841,16 +23252,9 @@ const validationServices = {
24841
23252
  sizingValidationService: inject(TYPES.sizingValidationService),
24842
23253
  riskValidationService: inject(TYPES.riskValidationService),
24843
23254
  actionValidationService: inject(TYPES.actionValidationService),
24844
- optimizerValidationService: inject(TYPES.optimizerValidationService),
24845
23255
  configValidationService: inject(TYPES.configValidationService),
24846
23256
  columnValidationService: inject(TYPES.columnValidationService),
24847
23257
  };
24848
- const templateServices = {
24849
- optimizerTemplateService: inject(TYPES.optimizerTemplateService),
24850
- };
24851
- const promptServices = {
24852
- signalPromptService: inject(TYPES.signalPromptService),
24853
- };
24854
23258
  const backtest = {
24855
23259
  ...baseServices,
24856
23260
  ...contextServices,
@@ -24864,8 +23268,6 @@ const backtest = {
24864
23268
  ...markdownServices,
24865
23269
  ...reportServices,
24866
23270
  ...validationServices,
24867
- ...templateServices,
24868
- ...promptServices,
24869
23271
  };
24870
23272
  init();
24871
23273
  var bt = backtest;
@@ -24964,18 +23366,6 @@ const getSizingMap = async () => {
24964
23366
  }
24965
23367
  return sizingMap;
24966
23368
  };
24967
- /**
24968
- * Retrieves all registered optimizers as a map
24969
- * @private
24970
- * @returns Map of optimizer names
24971
- */
24972
- const getOptimizerMap = async () => {
24973
- const optimizerMap = {};
24974
- for (const { optimizerName } of await bt.optimizerValidationService.list()) {
24975
- Object.assign(optimizerMap, { [optimizerName]: optimizerName });
24976
- }
24977
- return optimizerMap;
24978
- };
24979
23369
  /**
24980
23370
  * Retrieves all registered walkers as a map
24981
23371
  * @private
@@ -25002,7 +23392,7 @@ const getWalkerMap = async () => {
25002
23392
  * @throws {Error} If any entity name is not found in its registry
25003
23393
  */
25004
23394
  const validateInternal = async (args) => {
25005
- const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), OptimizerName = await getOptimizerMap(), WalkerName = await getWalkerMap(), } = args;
23395
+ const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
25006
23396
  for (const exchangeName of Object.values(ExchangeName)) {
25007
23397
  bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
25008
23398
  }
@@ -25021,9 +23411,6 @@ const validateInternal = async (args) => {
25021
23411
  for (const sizingName of Object.values(SizingName)) {
25022
23412
  bt.sizingValidationService.validate(sizingName, METHOD_NAME);
25023
23413
  }
25024
- for (const optimizerName of Object.values(OptimizerName)) {
25025
- bt.optimizerValidationService.validate(optimizerName, METHOD_NAME);
25026
- }
25027
23414
  for (const walkerName of Object.values(WalkerName)) {
25028
23415
  bt.walkerValidationService.validate(walkerName, METHOD_NAME);
25029
23416
  }
@@ -25032,7 +23419,7 @@ const validateInternal = async (args) => {
25032
23419
  * Validates the existence of all provided entity names across validation services.
25033
23420
  *
25034
23421
  * This function accepts enum objects for various entity types (exchanges, frames,
25035
- * strategies, risks, sizings, optimizers, walkers) and validates that each entity
23422
+ * strategies, risks, sizings, walkers) and validates that each entity
25036
23423
  * name exists in its respective registry. Validation results are memoized for performance.
25037
23424
  *
25038
23425
  * If no arguments are provided (or specific entity types are omitted), the function
@@ -25092,7 +23479,6 @@ const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
25092
23479
  const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
25093
23480
  const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
25094
23481
  const GET_RISK_METHOD_NAME = "get.getRiskSchema";
25095
- const GET_OPTIMIZER_METHOD_NAME = "get.getOptimizerSchema";
25096
23482
  const GET_ACTION_METHOD_NAME = "get.getActionSchema";
25097
23483
  /**
25098
23484
  * Retrieves a registered strategy schema by name.
@@ -25224,29 +23610,6 @@ function getRiskSchema(riskName) {
25224
23610
  bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
25225
23611
  return bt.riskSchemaService.get(riskName);
25226
23612
  }
25227
- /**
25228
- * Retrieves a registered optimizer schema by name.
25229
- *
25230
- * @param optimizerName - Unique optimizer identifier
25231
- * @returns The optimizer schema configuration object
25232
- * @throws Error if optimizer is not registered
25233
- *
25234
- * @example
25235
- * ```typescript
25236
- * const optimizer = getOptimizer("llm-strategy-generator");
25237
- * console.log(optimizer.rangeTrain); // Array of training ranges
25238
- * console.log(optimizer.rangeTest); // Testing range
25239
- * console.log(optimizer.source); // Array of data sources
25240
- * console.log(optimizer.getPrompt); // async function
25241
- * ```
25242
- */
25243
- function getOptimizerSchema(optimizerName) {
25244
- bt.loggerService.log(GET_OPTIMIZER_METHOD_NAME, {
25245
- optimizerName,
25246
- });
25247
- bt.optimizerValidationService.validate(optimizerName, GET_OPTIMIZER_METHOD_NAME);
25248
- return bt.optimizerSchemaService.get(optimizerName);
25249
- }
25250
23613
  /**
25251
23614
  * Retrieves a registered action schema by name.
25252
23615
  *
@@ -25279,6 +23642,7 @@ const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
25279
23642
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25280
23643
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25281
23644
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
23645
+ const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25282
23646
  /**
25283
23647
  * Checks if trade context is active (execution and method contexts).
25284
23648
  *
@@ -25519,22 +23883,69 @@ async function getContext() {
25519
23883
  * console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
25520
23884
  * console.log(orderBook.asks); // [{ price: "50001.00", quantity: "0.3" }, ...]
25521
23885
  *
25522
- * // Fetch deeper order book
25523
- * const deepBook = await getOrderBook("BTCUSDT", 100);
23886
+ * // Fetch deeper order book
23887
+ * const deepBook = await getOrderBook("BTCUSDT", 100);
23888
+ * ```
23889
+ */
23890
+ async function getOrderBook(symbol, depth) {
23891
+ bt.loggerService.info(GET_ORDER_BOOK_METHOD_NAME, {
23892
+ symbol,
23893
+ depth,
23894
+ });
23895
+ if (!ExecutionContextService.hasContext()) {
23896
+ throw new Error("getOrderBook requires an execution context");
23897
+ }
23898
+ if (!MethodContextService.hasContext()) {
23899
+ throw new Error("getOrderBook requires a method context");
23900
+ }
23901
+ return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
23902
+ }
23903
+ /**
23904
+ * Fetches raw candles with flexible date/limit parameters.
23905
+ *
23906
+ * All modes respect execution context and prevent look-ahead bias.
23907
+ *
23908
+ * Parameter combinations:
23909
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
23910
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
23911
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
23912
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
23913
+ * 5. Only limit: uses execution.context.when as reference (backward)
23914
+ *
23915
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
23916
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
23917
+ * @param limit - Optional number of candles to fetch
23918
+ * @param sDate - Optional start date in milliseconds
23919
+ * @param eDate - Optional end date in milliseconds
23920
+ * @returns Promise resolving to array of candle data
23921
+ *
23922
+ * @example
23923
+ * ```typescript
23924
+ * // Fetch 100 candles backward from current context time
23925
+ * const candles = await getRawCandles("BTCUSDT", "1m", 100);
23926
+ *
23927
+ * // Fetch candles for specific date range
23928
+ * const rangeCandles = await getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
23929
+ *
23930
+ * // Fetch with all parameters specified
23931
+ * const exactCandles = await getRawCandles("BTCUSDT", "1m", 100, startMs, endMs);
25524
23932
  * ```
25525
23933
  */
25526
- async function getOrderBook(symbol, depth) {
25527
- bt.loggerService.info(GET_ORDER_BOOK_METHOD_NAME, {
23934
+ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
23935
+ bt.loggerService.info(GET_RAW_CANDLES_METHOD_NAME, {
25528
23936
  symbol,
25529
- depth,
23937
+ interval,
23938
+ limit,
23939
+ sDate,
23940
+ eDate,
25530
23941
  });
25531
23942
  if (!ExecutionContextService.hasContext()) {
25532
- throw new Error("getOrderBook requires an execution context");
23943
+ throw new Error("getRawCandles requires an execution context");
25533
23944
  }
25534
23945
  if (!MethodContextService.hasContext()) {
25535
- throw new Error("getOrderBook requires a method context");
23946
+ throw new Error("getRawCandles requires a method context");
25536
23947
  }
25537
- return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
23948
+ return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25538
23949
  }
25539
23950
 
25540
23951
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
@@ -26059,7 +24470,6 @@ const ADD_FRAME_METHOD_NAME = "add.addFrameSchema";
26059
24470
  const ADD_WALKER_METHOD_NAME = "add.addWalkerSchema";
26060
24471
  const ADD_SIZING_METHOD_NAME = "add.addSizingSchema";
26061
24472
  const ADD_RISK_METHOD_NAME = "add.addRiskSchema";
26062
- const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizerSchema";
26063
24473
  const ADD_ACTION_METHOD_NAME = "add.addActionSchema";
26064
24474
  /**
26065
24475
  * Registers a trading strategy in the framework.
@@ -26352,100 +24762,6 @@ function addRiskSchema(riskSchema) {
26352
24762
  bt.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
26353
24763
  bt.riskSchemaService.register(riskSchema.riskName, riskSchema);
26354
24764
  }
26355
- /**
26356
- * Registers an optimizer configuration in the framework.
26357
- *
26358
- * The optimizer generates trading strategies by:
26359
- * - Collecting data from multiple sources across training periods
26360
- * - Building LLM conversation history with fetched data
26361
- * - Generating strategy prompts using getPrompt()
26362
- * - Creating executable backtest code with templates
26363
- *
26364
- * The optimizer produces a complete .mjs file containing:
26365
- * - Exchange, Frame, Strategy, and Walker configurations
26366
- * - Multi-timeframe analysis logic
26367
- * - LLM integration for signal generation
26368
- * - Event listeners for progress tracking
26369
- *
26370
- * @param optimizerSchema - Optimizer configuration object
26371
- * @param optimizerSchema.optimizerName - Unique optimizer identifier
26372
- * @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
26373
- * @param optimizerSchema.rangeTest - Testing time range for strategy validation
26374
- * @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
26375
- * @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
26376
- * @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
26377
- * @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
26378
- *
26379
- * @example
26380
- * ```typescript
26381
- * // Basic optimizer with single data source
26382
- * addOptimizer({
26383
- * optimizerName: "llm-strategy-generator",
26384
- * rangeTrain: [
26385
- * {
26386
- * note: "Bull market period",
26387
- * startDate: new Date("2024-01-01"),
26388
- * endDate: new Date("2024-01-31"),
26389
- * },
26390
- * {
26391
- * note: "Bear market period",
26392
- * startDate: new Date("2024-02-01"),
26393
- * endDate: new Date("2024-02-28"),
26394
- * },
26395
- * ],
26396
- * rangeTest: {
26397
- * note: "Validation period",
26398
- * startDate: new Date("2024-03-01"),
26399
- * endDate: new Date("2024-03-31"),
26400
- * },
26401
- * source: [
26402
- * {
26403
- * name: "historical-backtests",
26404
- * fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
26405
- * // Fetch historical backtest results from database
26406
- * return await db.backtests.find({
26407
- * symbol,
26408
- * date: { $gte: startDate, $lte: endDate },
26409
- * })
26410
- * .skip(offset)
26411
- * .limit(limit);
26412
- * },
26413
- * user: async (symbol, data, name) => {
26414
- * return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
26415
- * },
26416
- * assistant: async (symbol, data, name) => {
26417
- * return "Historical data analyzed successfully";
26418
- * },
26419
- * },
26420
- * ],
26421
- * getPrompt: async (symbol, messages) => {
26422
- * // Generate strategy prompt from conversation
26423
- * return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
26424
- * },
26425
- * callbacks: {
26426
- * onData: (symbol, strategyData) => {
26427
- * console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
26428
- * },
26429
- * onCode: (symbol, code) => {
26430
- * console.log(`Generated ${code.length} characters of code for ${symbol}`);
26431
- * },
26432
- * onDump: (symbol, filepath) => {
26433
- * console.log(`Saved strategy to ${filepath}`);
26434
- * },
26435
- * onSourceData: (symbol, sourceName, data, startDate, endDate) => {
26436
- * console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
26437
- * },
26438
- * },
26439
- * });
26440
- * ```
26441
- */
26442
- function addOptimizerSchema(optimizerSchema) {
26443
- bt.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
26444
- optimizerSchema,
26445
- });
26446
- bt.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
26447
- bt.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
26448
- }
26449
24765
  /**
26450
24766
  * Registers an action handler in the framework.
26451
24767
  *
@@ -26528,7 +24844,6 @@ const METHOD_NAME_OVERRIDE_FRAME = "function.override.overrideFrameSchema";
26528
24844
  const METHOD_NAME_OVERRIDE_WALKER = "function.override.overrideWalkerSchema";
26529
24845
  const METHOD_NAME_OVERRIDE_SIZING = "function.override.overrideSizingSchema";
26530
24846
  const METHOD_NAME_OVERRIDE_RISK = "function.override.overrideRiskSchema";
26531
- const METHOD_NAME_OVERRIDE_OPTIMIZER = "function.override.overrideOptimizerSchema";
26532
24847
  const METHOD_NAME_OVERRIDE_ACTION = "function.override.overrideActionSchema";
26533
24848
  /**
26534
24849
  * Overrides an existing trading strategy in the framework.
@@ -26701,40 +25016,6 @@ async function overrideRiskSchema(riskSchema) {
26701
25016
  await bt.riskValidationService.validate(riskSchema.riskName, METHOD_NAME_OVERRIDE_RISK);
26702
25017
  return bt.riskSchemaService.override(riskSchema.riskName, riskSchema);
26703
25018
  }
26704
- /**
26705
- * Overrides an existing optimizer configuration in the framework.
26706
- *
26707
- * This function partially updates a previously registered optimizer with new configuration.
26708
- * Only the provided fields will be updated, other fields remain unchanged.
26709
- *
26710
- * @param optimizerSchema - Partial optimizer configuration object
26711
- * @param optimizerSchema.optimizerName - Unique optimizer identifier (must exist)
26712
- * @param optimizerSchema.rangeTrain - Optional: Array of training time ranges
26713
- * @param optimizerSchema.rangeTest - Optional: Testing time range
26714
- * @param optimizerSchema.source - Optional: Array of data sources
26715
- * @param optimizerSchema.getPrompt - Optional: Function to generate strategy prompt
26716
- * @param optimizerSchema.template - Optional: Custom template overrides
26717
- * @param optimizerSchema.callbacks - Optional: Lifecycle callbacks
26718
- *
26719
- * @example
26720
- * ```typescript
26721
- * overrideOptimizer({
26722
- * optimizerName: "llm-strategy-generator",
26723
- * rangeTest: {
26724
- * note: "Updated validation period",
26725
- * startDate: new Date("2024-04-01"),
26726
- * endDate: new Date("2024-04-30"),
26727
- * },
26728
- * });
26729
- * ```
26730
- */
26731
- async function overrideOptimizerSchema(optimizerSchema) {
26732
- bt.loggerService.log(METHOD_NAME_OVERRIDE_OPTIMIZER, {
26733
- optimizerSchema,
26734
- });
26735
- await bt.optimizerValidationService.validate(optimizerSchema.optimizerName, METHOD_NAME_OVERRIDE_OPTIMIZER);
26736
- return bt.optimizerSchemaService.override(optimizerSchema.optimizerName, optimizerSchema);
26737
- }
26738
25019
  /**
26739
25020
  * Overrides an existing action handler configuration in the framework.
26740
25021
  *
@@ -26809,7 +25090,6 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrameSchema";
26809
25090
  const LIST_WALKERS_METHOD_NAME = "list.listWalkerSchema";
26810
25091
  const LIST_SIZINGS_METHOD_NAME = "list.listSizingSchema";
26811
25092
  const LIST_RISKS_METHOD_NAME = "list.listRiskSchema";
26812
- const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizerSchema";
26813
25093
  /**
26814
25094
  * Returns a list of all registered exchange schemas.
26815
25095
  *
@@ -27007,46 +25287,6 @@ async function listRiskSchema() {
27007
25287
  bt.loggerService.log(LIST_RISKS_METHOD_NAME);
27008
25288
  return await bt.riskValidationService.list();
27009
25289
  }
27010
- /**
27011
- * Returns a list of all registered optimizer schemas.
27012
- *
27013
- * Retrieves all optimizers that have been registered via addOptimizer().
27014
- * Useful for debugging, documentation, or building dynamic UIs.
27015
- *
27016
- * @returns Array of optimizer schemas with their configurations
27017
- *
27018
- * @example
27019
- * ```typescript
27020
- * import { listOptimizers, addOptimizer } from "backtest-kit";
27021
- *
27022
- * addOptimizer({
27023
- * optimizerName: "llm-strategy-generator",
27024
- * note: "Generates trading strategies using LLM",
27025
- * rangeTrain: [
27026
- * {
27027
- * note: "Training period 1",
27028
- * startDate: new Date("2024-01-01"),
27029
- * endDate: new Date("2024-01-31"),
27030
- * },
27031
- * ],
27032
- * rangeTest: {
27033
- * note: "Testing period",
27034
- * startDate: new Date("2024-02-01"),
27035
- * endDate: new Date("2024-02-28"),
27036
- * },
27037
- * source: [],
27038
- * getPrompt: async (symbol, messages) => "Generate strategy",
27039
- * });
27040
- *
27041
- * const optimizers = listOptimizers();
27042
- * console.log(optimizers);
27043
- * // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
27044
- * ```
27045
- */
27046
- async function listOptimizerSchema() {
27047
- bt.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
27048
- return await bt.optimizerValidationService.list();
27049
- }
27050
25290
 
27051
25291
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
27052
25292
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
@@ -27064,7 +25304,6 @@ const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
27064
25304
  const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
27065
25305
  const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
27066
25306
  const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
27067
- const LISTEN_PROGRESS_OPTIMIZER_METHOD_NAME = "event.listenOptimizerProgress";
27068
25307
  const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
27069
25308
  const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
27070
25309
  const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
@@ -27552,34 +25791,6 @@ function listenWalkerProgress(fn) {
27552
25791
  bt.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
27553
25792
  return progressWalkerEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
27554
25793
  }
27555
- /**
27556
- * Subscribes to optimizer progress events with queued async processing.
27557
- *
27558
- * Emits during optimizer execution to track data source processing progress.
27559
- * Events are processed sequentially in order received, even if callback is async.
27560
- * Uses queued wrapper to prevent concurrent execution of the callback.
27561
- *
27562
- * @param fn - Callback function to handle optimizer progress events
27563
- * @returns Unsubscribe function to stop listening to events
27564
- *
27565
- * @example
27566
- * ```typescript
27567
- * import { listenOptimizerProgress } from "backtest-kit";
27568
- *
27569
- * const unsubscribe = listenOptimizerProgress((event) => {
27570
- * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
27571
- * console.log(`${event.processedSources} / ${event.totalSources} sources`);
27572
- * console.log(`Optimizer: ${event.optimizerName}, Symbol: ${event.symbol}`);
27573
- * });
27574
- *
27575
- * // Later: stop listening
27576
- * unsubscribe();
27577
- * ```
27578
- */
27579
- function listenOptimizerProgress(fn) {
27580
- bt.loggerService.log(LISTEN_PROGRESS_OPTIMIZER_METHOD_NAME);
27581
- return progressOptimizerEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
27582
- }
27583
25794
  /**
27584
25795
  * Subscribes to performance metric events with queued async processing.
27585
25796
  *
@@ -28140,138 +26351,6 @@ function listenActivePingOnce(filterFn, fn) {
28140
26351
  return activePingSubject.filter(filterFn).once(fn);
28141
26352
  }
28142
26353
 
28143
- const METHOD_NAME_SIGNAL = "history.commitSignalPromptHistory";
28144
- /**
28145
- * Commits signal prompt history to the message array.
28146
- *
28147
- * Extracts trading context from ExecutionContext and MethodContext,
28148
- * then adds signal-specific system prompts at the beginning and user prompt
28149
- * at the end of the history array if they are not empty.
28150
- *
28151
- * Context extraction:
28152
- * - symbol: Provided as parameter for debugging convenience
28153
- * - backtest mode: From ExecutionContext
28154
- * - strategyName, exchangeName, frameName: From MethodContext
28155
- *
28156
- * @param symbol - Trading symbol (e.g., "BTCUSDT") for debugging convenience
28157
- * @param history - Message array to append prompts to
28158
- * @returns Promise that resolves when prompts are added
28159
- * @throws Error if ExecutionContext or MethodContext is not active
28160
- *
28161
- * @example
28162
- * ```typescript
28163
- * const messages: MessageModel[] = [];
28164
- * await commitSignalPromptHistory("BTCUSDT", messages);
28165
- * // messages now contains system prompts at start and user prompt at end
28166
- * ```
28167
- */
28168
- async function commitSignalPromptHistory(symbol, history) {
28169
- bt.loggerService.log(METHOD_NAME_SIGNAL, {
28170
- symbol,
28171
- });
28172
- if (!ExecutionContextService.hasContext()) {
28173
- throw new Error("commitSignalPromptHistory requires an execution context");
28174
- }
28175
- if (!MethodContextService.hasContext()) {
28176
- throw new Error("commitSignalPromptHistory requires a method context");
28177
- }
28178
- const { backtest: isBacktest } = bt.executionContextService.context;
28179
- const { strategyName, exchangeName, frameName } = bt.methodContextService.context;
28180
- const systemPrompts = await bt.signalPromptService.getSystemPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
28181
- const userPrompt = await bt.signalPromptService.getUserPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
28182
- if (systemPrompts.length > 0) {
28183
- for (const content of systemPrompts) {
28184
- history.unshift({
28185
- role: "system",
28186
- content,
28187
- });
28188
- }
28189
- }
28190
- if (userPrompt && userPrompt.trim() !== "") {
28191
- history.push({
28192
- role: "user",
28193
- content: userPrompt,
28194
- });
28195
- }
28196
- }
28197
-
28198
- const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
28199
- /**
28200
- * Dumps signal data and LLM conversation history to markdown files.
28201
- * Used by AI-powered strategies to save debug logs for analysis.
28202
- *
28203
- * Creates a directory structure with:
28204
- * - 00_system_prompt.md - System messages and output summary
28205
- * - XX_user_message.md - Each user message in separate file (numbered)
28206
- * - XX_llm_output.md - Final LLM output with signal data
28207
- *
28208
- * Skips if directory already exists to avoid overwriting previous results.
28209
- *
28210
- * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
28211
- * @param history - Array of message models from LLM conversation
28212
- * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
28213
- * @param outputDir - Output directory path (default: "./dump/strategy")
28214
- * @returns Promise that resolves when all files are written
28215
- *
28216
- * @example
28217
- * ```typescript
28218
- * import { dumpSignal, getCandles } from "backtest-kit";
28219
- * import { v4 as uuid } from "uuid";
28220
- *
28221
- * addStrategy({
28222
- * strategyName: "llm-strategy",
28223
- * interval: "5m",
28224
- * getSignal: async (symbol) => {
28225
- * const messages = [];
28226
- *
28227
- * // Build multi-timeframe analysis conversation
28228
- * const candles1h = await getCandles(symbol, "1h", 24);
28229
- * messages.push(
28230
- * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
28231
- * { role: "assistant", content: "Trend analyzed" }
28232
- * );
28233
- *
28234
- * const candles5m = await getCandles(symbol, "5m", 24);
28235
- * messages.push(
28236
- * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
28237
- * { role: "assistant", content: "Structure analyzed" }
28238
- * );
28239
- *
28240
- * // Request signal
28241
- * messages.push({
28242
- * role: "user",
28243
- * content: "Generate trading signal. Use position: 'wait' if uncertain."
28244
- * });
28245
- *
28246
- * const resultId = uuid();
28247
- * const signal = await llmRequest(messages);
28248
- *
28249
- * // Save conversation and result for debugging
28250
- * await dumpSignal(resultId, messages, signal);
28251
- *
28252
- * return signal;
28253
- * }
28254
- * });
28255
- *
28256
- * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
28257
- * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
28258
- * // ./dump/strategy/{uuid}/02_assistant_message.md
28259
- * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
28260
- * // ./dump/strategy/{uuid}/04_assistant_message.md
28261
- * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
28262
- * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
28263
- * ```
28264
- */
28265
- async function dumpSignalData(signalId, history, signal, outputDir = "./dump/strategy") {
28266
- bt.loggerService.info(DUMP_SIGNAL_METHOD_NAME, {
28267
- signalId,
28268
- history,
28269
- signal,
28270
- outputDir,
28271
- });
28272
- return await bt.outlineMarkdownService.dumpSignal(signalId, history, signal, outputDir);
28273
- }
28274
-
28275
26354
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
28276
26355
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
28277
26356
  const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
@@ -31431,104 +29510,6 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
31431
29510
  };
31432
29511
  const PositionSize = PositionSizeUtils;
31433
29512
 
31434
- const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
31435
- const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
31436
- const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
31437
- /**
31438
- * Public API utilities for optimizer operations.
31439
- * Provides high-level methods for strategy generation and code export.
31440
- *
31441
- * Usage:
31442
- * ```typescript
31443
- * import { Optimizer } from "backtest-kit";
31444
- *
31445
- * // Get strategy data
31446
- * const strategies = await Optimizer.getData("BTCUSDT", {
31447
- * optimizerName: "my-optimizer"
31448
- * });
31449
- *
31450
- * // Generate code
31451
- * const code = await Optimizer.getCode("BTCUSDT", {
31452
- * optimizerName: "my-optimizer"
31453
- * });
31454
- *
31455
- * // Save to file
31456
- * await Optimizer.dump("BTCUSDT", {
31457
- * optimizerName: "my-optimizer"
31458
- * }, "./output");
31459
- * ```
31460
- */
31461
- class OptimizerUtils {
31462
- constructor() {
31463
- /**
31464
- * Fetches data from all sources and generates strategy metadata.
31465
- * Processes each training range and builds LLM conversation history.
31466
- *
31467
- * @param symbol - Trading pair symbol
31468
- * @param context - Context with optimizerName
31469
- * @returns Array of generated strategies with conversation context
31470
- * @throws Error if optimizer not found
31471
- */
31472
- this.getData = async (symbol, context) => {
31473
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
31474
- symbol,
31475
- context,
31476
- });
31477
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_DATA);
31478
- return await bt.optimizerGlobalService.getData(symbol, context.optimizerName);
31479
- };
31480
- /**
31481
- * Generates complete executable strategy code.
31482
- * Includes imports, helpers, strategies, walker, and launcher.
31483
- *
31484
- * @param symbol - Trading pair symbol
31485
- * @param context - Context with optimizerName
31486
- * @returns Generated TypeScript/JavaScript code as string
31487
- * @throws Error if optimizer not found
31488
- */
31489
- this.getCode = async (symbol, context) => {
31490
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
31491
- symbol,
31492
- context,
31493
- });
31494
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_CODE);
31495
- return await bt.optimizerGlobalService.getCode(symbol, context.optimizerName);
31496
- };
31497
- /**
31498
- * Generates and saves strategy code to file.
31499
- * Creates directory if needed, writes .mjs file.
31500
- *
31501
- * Format: `{optimizerName}_{symbol}.mjs`
31502
- *
31503
- * @param symbol - Trading pair symbol
31504
- * @param context - Context with optimizerName
31505
- * @param path - Output directory path (default: "./")
31506
- * @throws Error if optimizer not found or file write fails
31507
- */
31508
- this.dump = async (symbol, context, path) => {
31509
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
31510
- symbol,
31511
- context,
31512
- path,
31513
- });
31514
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_DUMP);
31515
- await bt.optimizerGlobalService.dump(symbol, context.optimizerName, path);
31516
- };
31517
- }
31518
- }
31519
- /**
31520
- * Singleton instance of OptimizerUtils.
31521
- * Public API for optimizer operations.
31522
- *
31523
- * @example
31524
- * ```typescript
31525
- * import { Optimizer } from "backtest-kit";
31526
- *
31527
- * await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
31528
- * ```
31529
- */
31530
- const Optimizer = new OptimizerUtils();
31531
-
31532
29513
  const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
31533
29514
  const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
31534
29515
  const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
@@ -31807,6 +29788,8 @@ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
31807
29788
  const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
31808
29789
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31809
29790
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
29791
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
29792
+ const MS_PER_MINUTE = 60000;
31810
29793
  /**
31811
29794
  * Gets backtest mode flag from execution context if available.
31812
29795
  * Returns false if no execution context exists (live mode).
@@ -32160,6 +30143,151 @@ class ExchangeInstance {
32160
30143
  const isBacktest = await GET_BACKTEST_FN();
32161
30144
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32162
30145
  };
30146
+ /**
30147
+ * Fetches raw candles with flexible date/limit parameters.
30148
+ *
30149
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
30150
+ *
30151
+ * Parameter combinations:
30152
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
30153
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
30154
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= now
30155
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
30156
+ * 5. Only limit: uses Date.now() as reference (backward)
30157
+ *
30158
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
30159
+ * @param interval - Candle interval (e.g., "1m", "1h")
30160
+ * @param limit - Optional number of candles to fetch
30161
+ * @param sDate - Optional start date in milliseconds
30162
+ * @param eDate - Optional end date in milliseconds
30163
+ * @returns Promise resolving to array of candle data
30164
+ *
30165
+ * @example
30166
+ * ```typescript
30167
+ * const instance = new ExchangeInstance("binance");
30168
+ *
30169
+ * // Fetch 100 candles backward from now
30170
+ * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
30171
+ *
30172
+ * // Fetch candles for specific date range
30173
+ * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
30174
+ * ```
30175
+ */
30176
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
30177
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
30178
+ exchangeName: this.exchangeName,
30179
+ symbol,
30180
+ interval,
30181
+ limit,
30182
+ sDate,
30183
+ eDate,
30184
+ });
30185
+ const step = INTERVAL_MINUTES$1[interval];
30186
+ if (!step) {
30187
+ throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
30188
+ }
30189
+ const nowTimestamp = Date.now();
30190
+ let sinceTimestamp;
30191
+ let untilTimestamp;
30192
+ let calculatedLimit;
30193
+ // Case 1: all three parameters provided
30194
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
30195
+ if (sDate >= eDate) {
30196
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
30197
+ }
30198
+ if (eDate > nowTimestamp) {
30199
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30200
+ }
30201
+ sinceTimestamp = sDate;
30202
+ untilTimestamp = eDate;
30203
+ calculatedLimit = limit;
30204
+ }
30205
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
30206
+ else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
30207
+ if (sDate >= eDate) {
30208
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
30209
+ }
30210
+ if (eDate > nowTimestamp) {
30211
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30212
+ }
30213
+ sinceTimestamp = sDate;
30214
+ untilTimestamp = eDate;
30215
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE));
30216
+ if (calculatedLimit <= 0) {
30217
+ throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
30218
+ }
30219
+ }
30220
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
30221
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
30222
+ if (eDate > nowTimestamp) {
30223
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30224
+ }
30225
+ untilTimestamp = eDate;
30226
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE;
30227
+ calculatedLimit = limit;
30228
+ }
30229
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
30230
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
30231
+ sinceTimestamp = sDate;
30232
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE;
30233
+ if (untilTimestamp > nowTimestamp) {
30234
+ throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30235
+ }
30236
+ calculatedLimit = limit;
30237
+ }
30238
+ // Case 5: Only limit - use Date.now() as reference (backward)
30239
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
30240
+ untilTimestamp = nowTimestamp;
30241
+ sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
30242
+ calculatedLimit = limit;
30243
+ }
30244
+ // Invalid: no parameters or only sDate or only eDate
30245
+ else {
30246
+ throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
30247
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
30248
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
30249
+ }
30250
+ // Try to read from cache first
30251
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
30252
+ if (cachedCandles !== null) {
30253
+ return cachedCandles;
30254
+ }
30255
+ // Fetch candles
30256
+ const since = new Date(sinceTimestamp);
30257
+ let allData = [];
30258
+ const isBacktest = await GET_BACKTEST_FN();
30259
+ const getCandles = this._methods.getCandles;
30260
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
30261
+ let remaining = calculatedLimit;
30262
+ let currentSince = new Date(since.getTime());
30263
+ while (remaining > 0) {
30264
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
30265
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
30266
+ allData.push(...chunkData);
30267
+ remaining -= chunkLimit;
30268
+ if (remaining > 0) {
30269
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
30270
+ }
30271
+ }
30272
+ }
30273
+ else {
30274
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
30275
+ }
30276
+ // Filter candles to strictly match the requested range
30277
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
30278
+ candle.timestamp < untilTimestamp);
30279
+ // Apply distinct by timestamp to remove duplicates
30280
+ const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
30281
+ if (filteredData.length !== uniqueData.length) {
30282
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
30283
+ }
30284
+ if (uniqueData.length < calculatedLimit) {
30285
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`);
30286
+ }
30287
+ // Write to cache after successful fetch
30288
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
30289
+ return uniqueData;
30290
+ };
32163
30291
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
32164
30292
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
32165
30293
  }
@@ -32264,6 +30392,24 @@ class ExchangeUtils {
32264
30392
  const instance = this._getInstance(context.exchangeName);
32265
30393
  return await instance.getOrderBook(symbol, depth);
32266
30394
  };
30395
+ /**
30396
+ * Fetches raw candles with flexible date/limit parameters.
30397
+ *
30398
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
30399
+ *
30400
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
30401
+ * @param interval - Candle interval (e.g., "1m", "1h")
30402
+ * @param context - Execution context with exchange name
30403
+ * @param limit - Optional number of candles to fetch
30404
+ * @param sDate - Optional start date in milliseconds
30405
+ * @param eDate - Optional end date in milliseconds
30406
+ * @returns Promise resolving to array of candle data
30407
+ */
30408
+ this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
30409
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
30410
+ const instance = this._getInstance(context.exchangeName);
30411
+ return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
30412
+ };
32267
30413
  }
32268
30414
  }
32269
30415
  /**
@@ -33409,7 +31555,6 @@ exports.MarkdownFileBase = MarkdownFileBase;
33409
31555
  exports.MarkdownFolderBase = MarkdownFolderBase;
33410
31556
  exports.MethodContextService = MethodContextService;
33411
31557
  exports.Notification = Notification;
33412
- exports.Optimizer = Optimizer;
33413
31558
  exports.Partial = Partial;
33414
31559
  exports.Performance = Performance;
33415
31560
  exports.PersistBase = PersistBase;
@@ -33428,7 +31573,6 @@ exports.Walker = Walker;
33428
31573
  exports.addActionSchema = addActionSchema;
33429
31574
  exports.addExchangeSchema = addExchangeSchema;
33430
31575
  exports.addFrameSchema = addFrameSchema;
33431
- exports.addOptimizerSchema = addOptimizerSchema;
33432
31576
  exports.addRiskSchema = addRiskSchema;
33433
31577
  exports.addSizingSchema = addSizingSchema;
33434
31578
  exports.addStrategySchema = addStrategySchema;
@@ -33438,10 +31582,8 @@ exports.commitCancelScheduled = commitCancelScheduled;
33438
31582
  exports.commitClosePending = commitClosePending;
33439
31583
  exports.commitPartialLoss = commitPartialLoss;
33440
31584
  exports.commitPartialProfit = commitPartialProfit;
33441
- exports.commitSignalPromptHistory = commitSignalPromptHistory;
33442
31585
  exports.commitTrailingStop = commitTrailingStop;
33443
31586
  exports.commitTrailingTake = commitTrailingTake;
33444
- exports.dumpSignalData = dumpSignalData;
33445
31587
  exports.emitters = emitters;
33446
31588
  exports.formatPrice = formatPrice;
33447
31589
  exports.formatQuantity = formatQuantity;
@@ -33459,8 +31601,8 @@ exports.getDefaultConfig = getDefaultConfig;
33459
31601
  exports.getExchangeSchema = getExchangeSchema;
33460
31602
  exports.getFrameSchema = getFrameSchema;
33461
31603
  exports.getMode = getMode;
33462
- exports.getOptimizerSchema = getOptimizerSchema;
33463
31604
  exports.getOrderBook = getOrderBook;
31605
+ exports.getRawCandles = getRawCandles;
33464
31606
  exports.getRiskSchema = getRiskSchema;
33465
31607
  exports.getSizingSchema = getSizingSchema;
33466
31608
  exports.getStrategySchema = getStrategySchema;
@@ -33470,7 +31612,6 @@ exports.hasTradeContext = hasTradeContext;
33470
31612
  exports.lib = backtest;
33471
31613
  exports.listExchangeSchema = listExchangeSchema;
33472
31614
  exports.listFrameSchema = listFrameSchema;
33473
- exports.listOptimizerSchema = listOptimizerSchema;
33474
31615
  exports.listRiskSchema = listRiskSchema;
33475
31616
  exports.listSizingSchema = listSizingSchema;
33476
31617
  exports.listStrategySchema = listStrategySchema;
@@ -33488,7 +31629,6 @@ exports.listenDoneWalker = listenDoneWalker;
33488
31629
  exports.listenDoneWalkerOnce = listenDoneWalkerOnce;
33489
31630
  exports.listenError = listenError;
33490
31631
  exports.listenExit = listenExit;
33491
- exports.listenOptimizerProgress = listenOptimizerProgress;
33492
31632
  exports.listenPartialLossAvailable = listenPartialLossAvailable;
33493
31633
  exports.listenPartialLossAvailableOnce = listenPartialLossAvailableOnce;
33494
31634
  exports.listenPartialProfitAvailable = listenPartialProfitAvailable;
@@ -33512,7 +31652,6 @@ exports.listenWalkerProgress = listenWalkerProgress;
33512
31652
  exports.overrideActionSchema = overrideActionSchema;
33513
31653
  exports.overrideExchangeSchema = overrideExchangeSchema;
33514
31654
  exports.overrideFrameSchema = overrideFrameSchema;
33515
- exports.overrideOptimizerSchema = overrideOptimizerSchema;
33516
31655
  exports.overrideRiskSchema = overrideRiskSchema;
33517
31656
  exports.overrideSizingSchema = overrideSizingSchema;
33518
31657
  exports.overrideStrategySchema = overrideStrategySchema;