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.mjs CHANGED
@@ -1,13 +1,12 @@
1
1
  import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, trycatch, retry, errorData, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, iterateDocuments, distinctDocuments, singlerun } from 'functools-kit';
3
+ import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, trycatch, retry, errorData, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
4
4
  import * as fs from 'fs/promises';
5
- import fs__default, { mkdir, writeFile } from 'fs/promises';
5
+ import fs__default from 'fs/promises';
6
6
  import path, { join, dirname } from 'path';
7
7
  import crypto from 'crypto';
8
8
  import os from 'os';
9
9
  import { createWriteStream } from 'fs';
10
- import { createRequire } from 'module';
11
10
  import { parseArgs as parseArgs$1 } from 'util';
12
11
 
13
12
  const { init, inject, provide } = createActivator("backtest");
@@ -52,7 +51,6 @@ const connectionServices$1 = {
52
51
  sizingConnectionService: Symbol('sizingConnectionService'),
53
52
  riskConnectionService: Symbol('riskConnectionService'),
54
53
  actionConnectionService: Symbol('actionConnectionService'),
55
- optimizerConnectionService: Symbol('optimizerConnectionService'),
56
54
  partialConnectionService: Symbol('partialConnectionService'),
57
55
  breakevenConnectionService: Symbol('breakevenConnectionService'),
58
56
  };
@@ -64,7 +62,6 @@ const schemaServices$1 = {
64
62
  sizingSchemaService: Symbol('sizingSchemaService'),
65
63
  riskSchemaService: Symbol('riskSchemaService'),
66
64
  actionSchemaService: Symbol('actionSchemaService'),
67
- optimizerSchemaService: Symbol('optimizerSchemaService'),
68
65
  };
69
66
  const coreServices$1 = {
70
67
  exchangeCoreService: Symbol('exchangeCoreService'),
@@ -75,7 +72,6 @@ const coreServices$1 = {
75
72
  const globalServices$1 = {
76
73
  sizingGlobalService: Symbol('sizingGlobalService'),
77
74
  riskGlobalService: Symbol('riskGlobalService'),
78
- optimizerGlobalService: Symbol('optimizerGlobalService'),
79
75
  partialGlobalService: Symbol('partialGlobalService'),
80
76
  breakevenGlobalService: Symbol('breakevenGlobalService'),
81
77
  };
@@ -125,16 +121,9 @@ const validationServices$1 = {
125
121
  sizingValidationService: Symbol('sizingValidationService'),
126
122
  riskValidationService: Symbol('riskValidationService'),
127
123
  actionValidationService: Symbol('actionValidationService'),
128
- optimizerValidationService: Symbol('optimizerValidationService'),
129
124
  configValidationService: Symbol('configValidationService'),
130
125
  columnValidationService: Symbol('columnValidationService'),
131
126
  };
132
- const templateServices$1 = {
133
- optimizerTemplateService: Symbol('optimizerTemplateService'),
134
- };
135
- const promptServices$1 = {
136
- signalPromptService: Symbol('signalPromptService'),
137
- };
138
127
  const TYPES = {
139
128
  ...baseServices$1,
140
129
  ...contextServices$1,
@@ -148,8 +137,6 @@ const TYPES = {
148
137
  ...markdownServices$1,
149
138
  ...reportServices$1,
150
139
  ...validationServices$1,
151
- ...templateServices$1,
152
- ...promptServices$1,
153
140
  };
154
141
 
155
142
  /**
@@ -473,11 +460,6 @@ const progressBacktestEmitter = new Subject();
473
460
  * Emits progress updates during walker execution.
474
461
  */
475
462
  const progressWalkerEmitter = new Subject();
476
- /**
477
- * Progress emitter for optimizer execution progress.
478
- * Emits progress updates during optimizer execution.
479
- */
480
- const progressOptimizerEmitter = new Subject();
481
463
  /**
482
464
  * Performance emitter for execution metrics.
483
465
  * Emits performance metrics for profiling and bottleneck detection.
@@ -552,7 +534,6 @@ var emitters = /*#__PURE__*/Object.freeze({
552
534
  partialProfitSubject: partialProfitSubject,
553
535
  performanceEmitter: performanceEmitter,
554
536
  progressBacktestEmitter: progressBacktestEmitter,
555
- progressOptimizerEmitter: progressOptimizerEmitter,
556
537
  progressWalkerEmitter: progressWalkerEmitter,
557
538
  riskSubject: riskSubject,
558
539
  schedulePingSubject: schedulePingSubject,
@@ -1646,6 +1627,7 @@ class PersistCandleUtils {
1646
1627
  */
1647
1628
  const PersistCandleAdapter = new PersistCandleUtils();
1648
1629
 
1630
+ const MS_PER_MINUTE$1 = 60000;
1649
1631
  const INTERVAL_MINUTES$4 = {
1650
1632
  "1m": 1,
1651
1633
  "3m": 3,
@@ -1791,7 +1773,7 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
1791
1773
  const GET_CANDLES_FN = async (dto, since, self) => {
1792
1774
  const step = INTERVAL_MINUTES$4[dto.interval];
1793
1775
  const sinceTimestamp = since.getTime();
1794
- const untilTimestamp = sinceTimestamp + dto.limit * step * 60 * 1000;
1776
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
1795
1777
  // Try to read from cache first
1796
1778
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
1797
1779
  if (cachedCandles !== null) {
@@ -1897,7 +1879,7 @@ class ClientExchange {
1897
1879
  if (!adjust) {
1898
1880
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
1899
1881
  }
1900
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
1882
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * MS_PER_MINUTE$1);
1901
1883
  let allData = [];
1902
1884
  // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
1903
1885
  if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
@@ -1910,7 +1892,7 @@ class ClientExchange {
1910
1892
  remaining -= chunkLimit;
1911
1893
  if (remaining > 0) {
1912
1894
  // Move currentSince forward by the number of candles fetched
1913
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1895
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1914
1896
  }
1915
1897
  }
1916
1898
  }
@@ -1957,7 +1939,7 @@ class ClientExchange {
1957
1939
  const now = Date.now();
1958
1940
  // Вычисляем конечное время запроса
1959
1941
  const step = INTERVAL_MINUTES$4[interval];
1960
- const endTime = since.getTime() + limit * step * 60 * 1000;
1942
+ const endTime = since.getTime() + limit * step * MS_PER_MINUTE$1;
1961
1943
  // Проверяем что запрошенный период не заходит за Date.now()
1962
1944
  if (endTime > now) {
1963
1945
  return [];
@@ -1974,7 +1956,7 @@ class ClientExchange {
1974
1956
  remaining -= chunkLimit;
1975
1957
  if (remaining > 0) {
1976
1958
  // Move currentSince forward by the number of candles fetched
1977
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1959
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1978
1960
  }
1979
1961
  }
1980
1962
  }
@@ -2069,22 +2051,19 @@ class ClientExchange {
2069
2051
  /**
2070
2052
  * Fetches raw candles with flexible date/limit parameters.
2071
2053
  *
2072
- * Compatibility layer that:
2073
- * - RAW MODE (sDate + eDate + limit): fetches exactly as specified, NO look-ahead bias protection
2074
- * - Other modes: respects execution context and prevents look-ahead bias
2054
+ * All modes respect execution context and prevent look-ahead bias.
2075
2055
  *
2076
2056
  * Parameter combinations:
2077
- * 1. sDate + eDate + limit: RAW MODE - fetches exactly as specified, no validation against when
2078
- * 2. sDate + eDate: calculates limit from date range, validates endTimestamp <= when
2079
- * 3. eDate + limit: calculates sDate backward, validates endTimestamp <= when
2080
- * 4. sDate + limit: fetches forward, validates endTimestamp <= when
2057
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
2058
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
2059
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
2060
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
2081
2061
  * 5. Only limit: uses execution.context.when as reference (backward)
2082
2062
  *
2083
2063
  * Edge cases:
2084
2064
  * - If calculated limit is 0 or negative: throws error
2085
2065
  * - If sDate >= eDate: throws error
2086
- * - If startTimestamp >= endTimestamp: throws error
2087
- * - If endTimestamp > when (non-RAW modes only): throws error to prevent look-ahead bias
2066
+ * - If eDate > when: throws error to prevent look-ahead bias
2088
2067
  *
2089
2068
  * @param symbol - Trading pair symbol
2090
2069
  * @param interval - Candle interval
@@ -2103,73 +2082,75 @@ class ClientExchange {
2103
2082
  eDate,
2104
2083
  });
2105
2084
  const step = INTERVAL_MINUTES$4[interval];
2106
- const stepMs = step * 60 * 1000;
2107
- const when = this.params.execution.context.when.getTime();
2108
- let startTimestamp;
2109
- let endTimestamp;
2110
- let candleLimit;
2111
- let isRawMode = false;
2112
- // Case 1: sDate + eDate + limit - RAW MODE (no look-ahead bias protection)
2113
- if (sDate !== undefined &&
2114
- eDate !== undefined &&
2115
- limit !== undefined) {
2116
- isRawMode = true;
2085
+ if (!step) {
2086
+ throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2087
+ }
2088
+ const whenTimestamp = this.params.execution.context.when.getTime();
2089
+ let sinceTimestamp;
2090
+ let untilTimestamp;
2091
+ let calculatedLimit;
2092
+ // Case 1: all three parameters provided
2093
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
2117
2094
  if (sDate >= eDate) {
2118
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2095
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2119
2096
  }
2120
- startTimestamp = sDate;
2121
- endTimestamp = eDate;
2122
- candleLimit = limit;
2097
+ if (eDate > whenTimestamp) {
2098
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2099
+ }
2100
+ sinceTimestamp = sDate;
2101
+ untilTimestamp = eDate;
2102
+ calculatedLimit = limit;
2123
2103
  }
2124
- // Case 2: sDate + eDate - calculate limit, respect backtest context
2104
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
2125
2105
  else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
2126
2106
  if (sDate >= eDate) {
2127
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2128
- }
2129
- startTimestamp = sDate;
2130
- endTimestamp = eDate;
2131
- const rangeDuration = endTimestamp - startTimestamp;
2132
- candleLimit = Math.floor(rangeDuration / stepMs);
2133
- if (candleLimit <= 0) {
2134
- throw new Error(`ClientExchange getRawCandles: calculated limit is ${candleLimit} for range [${sDate}, ${eDate}]`);
2135
- }
2136
- }
2137
- // Case 3: eDate + limit - calculate sDate backward, respect backtest context
2138
- else if (eDate !== undefined && limit !== undefined && sDate === undefined) {
2139
- endTimestamp = eDate;
2140
- startTimestamp = eDate - limit * stepMs;
2141
- candleLimit = limit;
2142
- }
2143
- // Case 4: sDate + limit - fetch forward, respect backtest context
2144
- else if (sDate !== undefined && limit !== undefined && eDate === undefined) {
2145
- startTimestamp = sDate;
2146
- endTimestamp = sDate + limit * stepMs;
2147
- candleLimit = limit;
2148
- }
2149
- // Case 5: Only limit - use execution context (backward from when)
2150
- else if (limit !== undefined && sDate === undefined && eDate === undefined) {
2151
- endTimestamp = when;
2152
- startTimestamp = when - limit * stepMs;
2153
- candleLimit = limit;
2154
- }
2155
- // Invalid combination
2156
- else {
2157
- throw new Error(`ClientExchange getRawCandles: invalid parameter combination. Must provide either (limit), (eDate+limit), (sDate+limit), (sDate+eDate), or (sDate+eDate+limit)`);
2107
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2108
+ }
2109
+ if (eDate > whenTimestamp) {
2110
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2111
+ }
2112
+ sinceTimestamp = sDate;
2113
+ untilTimestamp = eDate;
2114
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE$1));
2115
+ if (calculatedLimit <= 0) {
2116
+ throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
2117
+ }
2118
+ }
2119
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
2120
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
2121
+ if (eDate > whenTimestamp) {
2122
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2123
+ }
2124
+ untilTimestamp = eDate;
2125
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE$1;
2126
+ calculatedLimit = limit;
2127
+ }
2128
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
2129
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
2130
+ sinceTimestamp = sDate;
2131
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE$1;
2132
+ if (untilTimestamp > whenTimestamp) {
2133
+ throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2134
+ }
2135
+ calculatedLimit = limit;
2158
2136
  }
2159
- // Validate timestamps
2160
- if (startTimestamp >= endTimestamp) {
2161
- throw new Error(`ClientExchange getRawCandles: startTimestamp (${startTimestamp}) >= endTimestamp (${endTimestamp})`);
2137
+ // Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
2138
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
2139
+ untilTimestamp = whenTimestamp;
2140
+ sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
2141
+ calculatedLimit = limit;
2162
2142
  }
2163
- // Check if trying to fetch future data (prevent look-ahead bias)
2164
- // ONLY for non-RAW modes - RAW MODE bypasses this check
2165
- if (!isRawMode && endTimestamp > when) {
2166
- throw new Error(`ClientExchange getRawCandles: endTimestamp (${endTimestamp}) is beyond execution context when (${when}) - look-ahead bias prevented`);
2143
+ // Invalid: no parameters or only sDate or only eDate
2144
+ else {
2145
+ throw new Error(`ClientExchange getRawCandles: invalid parameter combination. ` +
2146
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
2147
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
2167
2148
  }
2168
- const since = new Date(startTimestamp);
2149
+ // Fetch candles using existing logic
2150
+ const since = new Date(sinceTimestamp);
2169
2151
  let allData = [];
2170
- // Fetch data in chunks if limit exceeds max per request
2171
- if (candleLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2172
- let remaining = candleLimit;
2152
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2153
+ let remaining = calculatedLimit;
2173
2154
  let currentSince = new Date(since.getTime());
2174
2155
  while (remaining > 0) {
2175
2156
  const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
@@ -2177,16 +2158,16 @@ class ClientExchange {
2177
2158
  allData.push(...chunkData);
2178
2159
  remaining -= chunkLimit;
2179
2160
  if (remaining > 0) {
2180
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
2161
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
2181
2162
  }
2182
2163
  }
2183
2164
  }
2184
2165
  else {
2185
- allData = await GET_CANDLES_FN({ symbol, interval, limit: candleLimit }, since, this);
2166
+ allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2186
2167
  }
2187
2168
  // Filter candles to strictly match the requested range
2188
- const filteredData = allData.filter((candle) => candle.timestamp >= startTimestamp &&
2189
- candle.timestamp < endTimestamp);
2169
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2170
+ candle.timestamp < untilTimestamp);
2190
2171
  // Apply distinct by timestamp to remove duplicates
2191
2172
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2192
2173
  if (filteredData.length !== uniqueData.length) {
@@ -2194,12 +2175,12 @@ class ClientExchange {
2194
2175
  this.params.logger.warn(msg);
2195
2176
  console.warn(msg);
2196
2177
  }
2197
- if (uniqueData.length < candleLimit) {
2198
- const msg = `ClientExchange getRawCandles: Expected ${candleLimit} candles, got ${uniqueData.length}`;
2178
+ if (uniqueData.length < calculatedLimit) {
2179
+ const msg = `ClientExchange getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`;
2199
2180
  this.params.logger.warn(msg);
2200
2181
  console.warn(msg);
2201
2182
  }
2202
- await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, candleLimit, uniqueData);
2183
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
2203
2184
  return uniqueData;
2204
2185
  }
2205
2186
  /**
@@ -2221,7 +2202,7 @@ class ClientExchange {
2221
2202
  });
2222
2203
  const to = new Date(this.params.execution.context.when.getTime());
2223
2204
  const from = new Date(to.getTime() -
2224
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
2205
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2225
2206
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2226
2207
  }
2227
2208
  }
@@ -2412,6 +2393,28 @@ class ExchangeConnectionService {
2412
2393
  });
2413
2394
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2414
2395
  };
2396
+ /**
2397
+ * Fetches raw candles with flexible date/limit parameters.
2398
+ *
2399
+ * Routes to exchange determined by methodContextService.context.exchangeName.
2400
+ *
2401
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2402
+ * @param interval - Candle interval (e.g., "1h", "1d")
2403
+ * @param limit - Optional number of candles to fetch
2404
+ * @param sDate - Optional start date in milliseconds
2405
+ * @param eDate - Optional end date in milliseconds
2406
+ * @returns Promise resolving to array of candle data
2407
+ */
2408
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
2409
+ this.loggerService.log("exchangeConnectionService getRawCandles", {
2410
+ symbol,
2411
+ interval,
2412
+ limit,
2413
+ sDate,
2414
+ eDate,
2415
+ });
2416
+ return await this.getExchange(this.methodContextService.context.exchangeName).getRawCandles(symbol, interval, limit, sDate, eDate);
2417
+ };
2415
2418
  }
2416
2419
  }
2417
2420
 
@@ -9955,6 +9958,40 @@ class ExchangeCoreService {
9955
9958
  backtest,
9956
9959
  });
9957
9960
  };
9961
+ /**
9962
+ * Fetches raw candles with flexible date/limit parameters and execution context.
9963
+ *
9964
+ * @param symbol - Trading pair symbol
9965
+ * @param interval - Candle interval (e.g., "1m", "1h")
9966
+ * @param when - Timestamp for context (used in backtest mode)
9967
+ * @param backtest - Whether running in backtest mode
9968
+ * @param limit - Optional number of candles to fetch
9969
+ * @param sDate - Optional start date in milliseconds
9970
+ * @param eDate - Optional end date in milliseconds
9971
+ * @returns Promise resolving to array of candles
9972
+ */
9973
+ this.getRawCandles = async (symbol, interval, when, backtest, limit, sDate, eDate) => {
9974
+ this.loggerService.log("exchangeCoreService getRawCandles", {
9975
+ symbol,
9976
+ interval,
9977
+ when,
9978
+ backtest,
9979
+ limit,
9980
+ sDate,
9981
+ eDate,
9982
+ });
9983
+ if (!MethodContextService.hasContext()) {
9984
+ throw new Error("exchangeCoreService getRawCandles requires a method context");
9985
+ }
9986
+ await this.validate(this.methodContextService.context.exchangeName);
9987
+ return await ExecutionContextService.runInContext(async () => {
9988
+ return await this.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
9989
+ }, {
9990
+ symbol,
9991
+ when,
9992
+ backtest,
9993
+ });
9994
+ };
9958
9995
  }
9959
9996
  }
9960
9997
 
@@ -18344,1469 +18381,120 @@ class ActionValidationService {
18344
18381
  }
18345
18382
 
18346
18383
  /**
18347
- * Default template service for generating optimizer code snippets.
18348
- * Implements all IOptimizerTemplate methods with Ollama LLM integration.
18384
+ * Symbol marker indicating that partial state needs initialization.
18385
+ * Used as sentinel value for _states before waitForInit() is called.
18386
+ */
18387
+ const NEED_FETCH$1 = Symbol("need_fetch");
18388
+ /**
18389
+ * Array of profit level milestones to track (10%, 20%, ..., 100%).
18390
+ * Each level is checked during profit() method to emit events for newly reached levels.
18391
+ */
18392
+ const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
18393
+ /**
18394
+ * Array of loss level milestones to track (-10%, -20%, ..., -100%).
18395
+ * Each level is checked during loss() method to emit events for newly reached levels.
18396
+ */
18397
+ const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
18398
+ /**
18399
+ * Internal profit handler function for ClientPartial.
18349
18400
  *
18350
- * Features:
18351
- * - Multi-timeframe analysis (1m, 5m, 15m, 1h)
18352
- * - JSON structured output for signals
18353
- * - Debug logging to ./dump/strategy
18354
- * - CCXT exchange integration
18355
- * - Walker-based strategy comparison
18401
+ * Checks which profit levels have been reached and emits events for new levels only.
18402
+ * Uses Set-based deduplication to prevent duplicate events.
18356
18403
  *
18357
- * Can be partially overridden in optimizer schema configuration.
18404
+ * @param symbol - Trading pair symbol
18405
+ * @param data - Signal row data
18406
+ * @param currentPrice - Current market price
18407
+ * @param revenuePercent - Current profit percentage (positive value)
18408
+ * @param backtest - True if backtest mode
18409
+ * @param when - Event timestamp
18410
+ * @param self - ClientPartial instance reference
18358
18411
  */
18359
- class OptimizerTemplateService {
18360
- constructor() {
18361
- this.loggerService = inject(TYPES.loggerService);
18362
- /**
18363
- * Generates the top banner with imports and constants.
18364
- *
18365
- * @param symbol - Trading pair symbol
18366
- * @returns Shebang, imports, and WARN_KB constant
18367
- */
18368
- this.getTopBanner = async (symbol) => {
18369
- this.loggerService.log("optimizerTemplateService getTopBanner", {
18370
- symbol,
18371
- });
18372
- return [
18373
- "#!/usr/bin/env node",
18374
- "",
18375
- `import { Ollama } from "ollama";`,
18376
- `import ccxt from "ccxt";`,
18377
- `import {`,
18378
- ` addExchangeSchema,`,
18379
- ` addStrategySchema,`,
18380
- ` addFrameSchema,`,
18381
- ` addWalkerSchema,`,
18382
- ` Walker,`,
18383
- ` Backtest,`,
18384
- ` getCandles,`,
18385
- ` listenSignalBacktest,`,
18386
- ` listenWalkerComplete,`,
18387
- ` listenDoneBacktest,`,
18388
- ` listenBacktestProgress,`,
18389
- ` listenWalkerProgress,`,
18390
- ` listenError,`,
18391
- ` Markdown,`,
18392
- `} from "backtest-kit";`,
18393
- `import { promises as fs } from "fs";`,
18394
- `import { v4 as uuid } from "uuid";`,
18395
- `import path from "path";`,
18396
- ``,
18397
- `const WARN_KB = 100;`,
18398
- ``,
18399
- `Markdown.enable()`,
18400
- ].join("\n");
18401
- };
18402
- /**
18403
- * Generates default user message for LLM conversation.
18404
- * Simple prompt to read and acknowledge data.
18405
- *
18406
- * @param symbol - Trading pair symbol
18407
- * @param data - Fetched data array
18408
- * @param name - Source name
18409
- * @returns User message with JSON data
18410
- */
18411
- this.getUserMessage = async (symbol, data, name) => {
18412
- this.loggerService.log("optimizerTemplateService getUserMessage", {
18413
- symbol,
18414
- data,
18415
- name,
18416
- });
18417
- return ["Прочитай данные и скажи ОК", "", JSON.stringify(data)].join("\n");
18418
- };
18419
- /**
18420
- * Generates default assistant message for LLM conversation.
18421
- * Simple acknowledgment response.
18422
- *
18423
- * @param symbol - Trading pair symbol
18424
- * @param data - Fetched data array
18425
- * @param name - Source name
18426
- * @returns Assistant acknowledgment message
18427
- */
18428
- this.getAssistantMessage = async (symbol, data, name) => {
18429
- this.loggerService.log("optimizerTemplateService getAssistantMessage", {
18430
- symbol,
18431
- data,
18432
- name,
18433
- });
18434
- return "ОК";
18435
- };
18436
- /**
18437
- * Generates Walker configuration code.
18438
- * Compares multiple strategies on test frame.
18439
- *
18440
- * @param walkerName - Unique walker identifier
18441
- * @param exchangeName - Exchange to use for backtesting
18442
- * @param frameName - Test frame name
18443
- * @param strategies - Array of strategy names to compare
18444
- * @returns Generated addWalker() call
18445
- */
18446
- this.getWalkerTemplate = async (walkerName, exchangeName, frameName, strategies) => {
18447
- this.loggerService.log("optimizerTemplateService getWalkerTemplate", {
18448
- walkerName,
18449
- exchangeName,
18450
- frameName,
18451
- strategies,
18452
- });
18453
- // Escape special characters to prevent code injection
18454
- const escapedWalkerName = String(walkerName)
18455
- .replace(/\\/g, '\\\\')
18456
- .replace(/"/g, '\\"');
18457
- const escapedExchangeName = String(exchangeName)
18458
- .replace(/\\/g, '\\\\')
18459
- .replace(/"/g, '\\"');
18460
- const escapedFrameName = String(frameName)
18461
- .replace(/\\/g, '\\\\')
18462
- .replace(/"/g, '\\"');
18463
- const escapedStrategies = strategies.map((s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'));
18464
- return [
18465
- `addWalkerSchema({`,
18466
- ` walkerName: "${escapedWalkerName}",`,
18467
- ` exchangeName: "${escapedExchangeName}",`,
18468
- ` frameName: "${escapedFrameName}",`,
18469
- ` strategies: [${escapedStrategies.map((s) => `"${s}"`).join(", ")}],`,
18470
- `});`
18471
- ].join("\n");
18472
- };
18473
- /**
18474
- * Generates Strategy configuration with LLM integration.
18475
- * Includes multi-timeframe analysis and signal generation.
18476
- *
18477
- * @param strategyName - Unique strategy identifier
18478
- * @param interval - Signal throttling interval (e.g., "5m")
18479
- * @param prompt - Strategy logic from getPrompt()
18480
- * @returns Generated addStrategy() call with getSignal() function
18481
- */
18482
- this.getStrategyTemplate = async (strategyName, interval, prompt) => {
18483
- this.loggerService.log("optimizerTemplateService getStrategyTemplate", {
18484
- strategyName,
18485
- interval,
18486
- prompt,
18487
- });
18488
- // Convert prompt to plain text first
18489
- const plainPrompt = toPlainString(prompt);
18490
- // Escape special characters to prevent code injection
18491
- const escapedStrategyName = String(strategyName)
18492
- .replace(/\\/g, '\\\\')
18493
- .replace(/"/g, '\\"');
18494
- const escapedInterval = String(interval)
18495
- .replace(/\\/g, '\\\\')
18496
- .replace(/"/g, '\\"');
18497
- const escapedPrompt = String(plainPrompt)
18498
- .replace(/\\/g, '\\\\')
18499
- .replace(/`/g, '\\`')
18500
- .replace(/\$/g, '\\$');
18501
- return [
18502
- `addStrategySchema({`,
18503
- ` strategyName: "${escapedStrategyName}",`,
18504
- ` interval: "${escapedInterval}",`,
18505
- ` getSignal: async (symbol) => {`,
18506
- ` const messages = [];`,
18507
- ``,
18508
- ` // Загружаем данные всех таймфреймов`,
18509
- ` const microTermCandles = await getCandles(symbol, "1m", 30);`,
18510
- ` const mainTermCandles = await getCandles(symbol, "5m", 24);`,
18511
- ` const shortTermCandles = await getCandles(symbol, "15m", 24);`,
18512
- ` const mediumTermCandles = await getCandles(symbol, "1h", 24);`,
18513
- ``,
18514
- ` function formatCandles(candles, timeframe) {`,
18515
- ` return candles.map((c) =>`,
18516
- ` \`\${new Date(c.timestamp).toISOString()}[\${timeframe}]: O:\${c.open} H:\${c.high} L:\${c.low} C:\${c.close} V:\${c.volume}\``,
18517
- ` ).join("\\n");`,
18518
- ` }`,
18519
- ``,
18520
- ` // Сообщение 1: Среднесрочный тренд`,
18521
- ` messages.push(`,
18522
- ` {`,
18523
- ` role: "user",`,
18524
- ` content: [`,
18525
- ` \`\${symbol}\`,`,
18526
- ` "Проанализируй свечи 1h:",`,
18527
- ` "",`,
18528
- ` formatCandles(mediumTermCandles, "1h")`,
18529
- ` ].join("\\n"),`,
18530
- ` },`,
18531
- ` {`,
18532
- ` role: "assistant",`,
18533
- ` content: "Тренд 1h проанализирован",`,
18534
- ` }`,
18535
- ` );`,
18536
- ``,
18537
- ` // Сообщение 2: Краткосрочный тренд`,
18538
- ` messages.push(`,
18539
- ` {`,
18540
- ` role: "user",`,
18541
- ` content: [`,
18542
- ` "Проанализируй свечи 15m:",`,
18543
- ` "",`,
18544
- ` formatCandles(shortTermCandles, "15m")`,
18545
- ` ].join("\\n"),`,
18546
- ` },`,
18547
- ` {`,
18548
- ` role: "assistant",`,
18549
- ` content: "Тренд 15m проанализирован",`,
18550
- ` }`,
18551
- ` );`,
18552
- ``,
18553
- ` // Сообщение 3: Основной таймфрейм`,
18554
- ` messages.push(`,
18555
- ` {`,
18556
- ` role: "user",`,
18557
- ` content: [`,
18558
- ` "Проанализируй свечи 5m:",`,
18559
- ` "",`,
18560
- ` formatCandles(mainTermCandles, "5m")`,
18561
- ` ].join("\\n")`,
18562
- ` },`,
18563
- ` {`,
18564
- ` role: "assistant",`,
18565
- ` content: "Таймфрейм 5m проанализирован",`,
18566
- ` }`,
18567
- ` );`,
18568
- ``,
18569
- ` // Сообщение 4: Микро-структура`,
18570
- ` messages.push(`,
18571
- ` {`,
18572
- ` role: "user",`,
18573
- ` content: [`,
18574
- ` "Проанализируй свечи 1m:",`,
18575
- ` "",`,
18576
- ` formatCandles(microTermCandles, "1m")`,
18577
- ` ].join("\\n")`,
18578
- ` },`,
18579
- ` {`,
18580
- ` role: "assistant",`,
18581
- ` content: "Микроструктура 1m проанализирована",`,
18582
- ` }`,
18583
- ` );`,
18584
- ``,
18585
- ` // Сообщение 5: Запрос сигнала`,
18586
- ` messages.push(`,
18587
- ` {`,
18588
- ` role: "user",`,
18589
- ` content: [`,
18590
- ` "Проанализируй все таймфреймы и сгенерируй торговый сигнал согласно этой стратегии. Открывай позицию ТОЛЬКО при четком сигнале.",`,
18591
- ` "",`,
18592
- ` \`${escapedPrompt}\`,`,
18593
- ` "",`,
18594
- ` "Если сигналы противоречивы или тренд слабый то position: wait"`,
18595
- ` ].join("\\n"),`,
18596
- ` }`,
18597
- ` );`,
18598
- ``,
18599
- ` const resultId = uuid();`,
18600
- ``,
18601
- ` const result = await json(messages);`,
18602
- ``,
18603
- ` await dumpJson(resultId, messages, result);`,
18604
- ``,
18605
- ` result.id = resultId;`,
18606
- ``,
18607
- ` return result;`,
18608
- ` },`,
18609
- `});`
18610
- ].join("\n");
18611
- };
18612
- /**
18613
- * Generates Exchange configuration code.
18614
- * Uses CCXT Binance with standard formatters.
18615
- *
18616
- * @param symbol - Trading pair symbol (unused, for consistency)
18617
- * @param exchangeName - Unique exchange identifier
18618
- * @returns Generated addExchange() call with CCXT integration
18619
- */
18620
- this.getExchangeTemplate = async (symbol, exchangeName) => {
18621
- this.loggerService.log("optimizerTemplateService getExchangeTemplate", {
18622
- exchangeName,
18623
- symbol,
18624
- });
18625
- // Escape special characters to prevent code injection
18626
- const escapedExchangeName = String(exchangeName)
18627
- .replace(/\\/g, '\\\\')
18628
- .replace(/"/g, '\\"');
18629
- return [
18630
- `addExchangeSchema({`,
18631
- ` exchangeName: "${escapedExchangeName}",`,
18632
- ` getCandles: async (symbol, interval, since, limit) => {`,
18633
- ` const exchange = new ccxt.binance();`,
18634
- ` const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);`,
18635
- ` return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({`,
18636
- ` timestamp, open, high, low, close, volume`,
18637
- ` }));`,
18638
- ` },`,
18639
- ` formatPrice: async (symbol, price) => price.toFixed(2),`,
18640
- ` formatQuantity: async (symbol, quantity) => quantity.toFixed(8),`,
18641
- `});`
18642
- ].join("\n");
18643
- };
18644
- /**
18645
- * Generates Frame (timeframe) configuration code.
18646
- *
18647
- * @param symbol - Trading pair symbol (unused, for consistency)
18648
- * @param frameName - Unique frame identifier
18649
- * @param interval - Candle interval (e.g., "1m")
18650
- * @param startDate - Frame start date
18651
- * @param endDate - Frame end date
18652
- * @returns Generated addFrame() call
18653
- */
18654
- this.getFrameTemplate = async (symbol, frameName, interval, startDate, endDate) => {
18655
- this.loggerService.log("optimizerTemplateService getFrameTemplate", {
18656
- symbol,
18657
- frameName,
18658
- interval,
18659
- startDate,
18660
- endDate,
18661
- });
18662
- // Escape special characters to prevent code injection
18663
- const escapedFrameName = String(frameName)
18664
- .replace(/\\/g, '\\\\')
18665
- .replace(/"/g, '\\"');
18666
- const escapedInterval = String(interval)
18667
- .replace(/\\/g, '\\\\')
18668
- .replace(/"/g, '\\"');
18669
- return [
18670
- `addFrameSchema({`,
18671
- ` frameName: "${escapedFrameName}",`,
18672
- ` interval: "${escapedInterval}",`,
18673
- ` startDate: new Date("${startDate.toISOString()}"),`,
18674
- ` endDate: new Date("${endDate.toISOString()}"),`,
18675
- `});`
18676
- ].join("\n");
18677
- };
18678
- /**
18679
- * Generates launcher code to run Walker with event listeners.
18680
- * Includes progress tracking and completion handlers.
18681
- *
18682
- * @param symbol - Trading pair symbol
18683
- * @param walkerName - Walker name to launch
18684
- * @returns Generated Walker.background() call with listeners
18685
- */
18686
- this.getLauncherTemplate = async (symbol, walkerName) => {
18687
- this.loggerService.log("optimizerTemplateService getLauncherTemplate", {
18688
- symbol,
18689
- walkerName,
18690
- });
18691
- // Escape special characters to prevent code injection
18692
- const escapedSymbol = String(symbol)
18693
- .replace(/\\/g, '\\\\')
18694
- .replace(/"/g, '\\"');
18695
- const escapedWalkerName = String(walkerName)
18696
- .replace(/\\/g, '\\\\')
18697
- .replace(/"/g, '\\"');
18698
- return [
18699
- `Walker.background("${escapedSymbol}", {`,
18700
- ` walkerName: "${escapedWalkerName}"`,
18701
- `});`,
18702
- ``,
18703
- `listenSignalBacktest((event) => {`,
18704
- ` console.log(event);`,
18705
- `});`,
18706
- ``,
18707
- `listenBacktestProgress((event) => {`,
18708
- ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
18709
- ` console.log(\`Processed: \${event.processedFrames} / \${event.totalFrames}\`);`,
18710
- `});`,
18711
- ``,
18712
- `listenWalkerProgress((event) => {`,
18713
- ` console.log(\`Progress: \${(event.progress * 100).toFixed(2)}%\`);`,
18714
- ` console.log(\`\${event.processedStrategies} / \${event.totalStrategies} strategies\`);`,
18715
- ` console.log(\`Walker: \${event.walkerName}, Symbol: \${event.symbol}\`);`,
18716
- `});`,
18717
- ``,
18718
- `listenWalkerComplete((results) => {`,
18719
- ` console.log("Walker completed:", results.bestStrategy);`,
18720
- ` Walker.dump(results.symbol, { walkerName: results.walkerName });`,
18721
- `});`,
18722
- ``,
18723
- `listenDoneBacktest((event) => {`,
18724
- ` console.log("Backtest completed:", event.symbol);`,
18725
- ` Backtest.dump(event.symbol, {`,
18726
- ` strategyName: event.strategyName,`,
18727
- ` exchangeName: event.exchangeName,`,
18728
- ` frameName: event.frameName`,
18729
- ` });`,
18730
- `});`,
18731
- ``,
18732
- `listenError((error) => {`,
18733
- ` console.error("Error occurred:", error);`,
18734
- `});`
18735
- ].join("\n");
18736
- };
18737
- /**
18738
- * Generates dumpJson() helper function for debug output.
18739
- * Saves LLM conversations and results to ./dump/strategy/{resultId}/
18740
- *
18741
- * @param symbol - Trading pair symbol (unused, for consistency)
18742
- * @returns Generated async dumpJson() function
18743
- */
18744
- this.getJsonDumpTemplate = async (symbol) => {
18745
- this.loggerService.log("optimizerTemplateService getJsonDumpTemplate", {
18746
- symbol,
18747
- });
18748
- return [
18749
- `async function dumpJson(resultId, history, result, outputDir = "./dump/strategy") {`,
18750
- ` // Extract system messages and system reminders from existing data`,
18751
- ` const systemMessages = history.filter((m) => m.role === "system");`,
18752
- ` const userMessages = history.filter((m) => m.role === "user");`,
18753
- ` const subfolderPath = path.join(outputDir, resultId);`,
18754
- ``,
18755
- ` try {`,
18756
- ` await fs.access(subfolderPath);`,
18757
- ` return;`,
18758
- ` } catch {`,
18759
- ` await fs.mkdir(subfolderPath, { recursive: true });`,
18760
- ` }`,
18761
- ``,
18762
- ` {`,
18763
- ` let summary = "# Outline Result Summary\\n\\n";`,
18764
- ``,
18765
- ` {`,
18766
- ` summary += \`**ResultId**: \${resultId}\\n\\n\`;`,
18767
- ` }`,
18768
- ``,
18769
- ` if (result) {`,
18770
- ` summary += "## Output Data\\n\\n";`,
18771
- ` summary += "\`\`\`json\\n";`,
18772
- ` summary += JSON.stringify(result, null, 2);`,
18773
- ` summary += "\\n\`\`\`\\n\\n";`,
18774
- ` }`,
18775
- ``,
18776
- ` // Add system messages to summary`,
18777
- ` if (systemMessages.length > 0) {`,
18778
- ` summary += "## System Messages\\n\\n";`,
18779
- ` systemMessages.forEach((msg, idx) => {`,
18780
- ` summary += \`### System Message \${idx + 1}\\n\\n\`;`,
18781
- ` summary += msg.content;`,
18782
- ` summary += "\\n\\n";`,
18783
- ` });`,
18784
- ` }`,
18785
- ``,
18786
- ` const summaryFile = path.join(subfolderPath, "00_system_prompt.md");`,
18787
- ` await fs.writeFile(summaryFile, summary, "utf8");`,
18788
- ` }`,
18789
- ``,
18790
- ` {`,
18791
- ` await Promise.all(`,
18792
- ` Array.from(userMessages.entries()).map(async ([idx, message]) => {`,
18793
- ` const messageNum = String(idx + 1).padStart(2, "0");`,
18794
- ` const contentFileName = \`\${messageNum}_user_message.md\`;`,
18795
- ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
18796
- ``,
18797
- ` {`,
18798
- ` const messageSizeBytes = Buffer.byteLength(message.content, "utf8");`,
18799
- ` const messageSizeKb = Math.floor(messageSizeBytes / 1024);`,
18800
- ` if (messageSizeKb > WARN_KB) {`,
18801
- ` console.warn(`,
18802
- ` \`User message \${idx + 1} is \${messageSizeBytes} bytes (\${messageSizeKb}kb), which exceeds warning limit\``,
18803
- ` );`,
18804
- ` }`,
18805
- ` }`,
18806
- ``,
18807
- ` let content = \`# User Input \${idx + 1}\\n\\n\`;`,
18808
- ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
18809
- ` content += message.content;`,
18810
- ` content += "\\n";`,
18811
- ``,
18812
- ` await fs.writeFile(contentFilePath, content, "utf8");`,
18813
- ` })`,
18814
- ` );`,
18815
- ` }`,
18816
- ``,
18817
- ` {`,
18818
- ` const messageNum = String(userMessages.length + 1).padStart(2, "0");`,
18819
- ` const contentFileName = \`\${messageNum}_llm_output.md\`;`,
18820
- ` const contentFilePath = path.join(subfolderPath, contentFileName);`,
18821
- ``,
18822
- ` let content = "# Full Outline Result\\n\\n";`,
18823
- ` content += \`**ResultId**: \${resultId}\\n\\n\`;`,
18824
- ``,
18825
- ` if (result) {`,
18826
- ` content += "## Output Data\\n\\n";`,
18827
- ` content += "\`\`\`json\\n";`,
18828
- ` content += JSON.stringify(result, null, 2);`,
18829
- ` content += "\\n\`\`\`\\n";`,
18830
- ` }`,
18831
- ``,
18832
- ` await fs.writeFile(contentFilePath, content, "utf8");`,
18833
- ` }`,
18834
- `}`
18835
- ].join("\n");
18412
+ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
18413
+ if (self._states === NEED_FETCH$1) {
18414
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
18415
+ }
18416
+ if (data.id !== self.params.signalId) {
18417
+ throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
18418
+ }
18419
+ let state = self._states.get(data.id);
18420
+ if (!state) {
18421
+ state = {
18422
+ profitLevels: new Set(),
18423
+ lossLevels: new Set(),
18836
18424
  };
18837
- /**
18838
- * Generates text() helper for LLM text generation.
18839
- * Uses Ollama deepseek-v3.1:671b model for market analysis.
18840
- *
18841
- * @param symbol - Trading pair symbol (used in prompt)
18842
- * @returns Generated async text() function
18843
- */
18844
- this.getTextTemplate = async (symbol) => {
18845
- this.loggerService.log("optimizerTemplateService getTextTemplate", {
18425
+ self._states.set(data.id, state);
18426
+ }
18427
+ let shouldPersist = false;
18428
+ for (const level of PROFIT_LEVELS) {
18429
+ if (revenuePercent >= level && !state.profitLevels.has(level)) {
18430
+ state.profitLevels.add(level);
18431
+ shouldPersist = true;
18432
+ self.params.logger.debug("ClientPartial profit level reached", {
18846
18433
  symbol,
18434
+ signalId: data.id,
18435
+ level,
18436
+ revenuePercent,
18437
+ backtest,
18847
18438
  });
18848
- // Escape special characters in symbol to prevent code injection
18849
- const escapedSymbol = String(symbol)
18850
- .replace(/\\/g, '\\\\')
18851
- .replace(/`/g, '\\`')
18852
- .replace(/\$/g, '\\$')
18853
- .toUpperCase();
18854
- return [
18855
- `async function text(messages) {`,
18856
- ` const ollama = new Ollama({`,
18857
- ` host: "https://ollama.com",`,
18858
- ` headers: {`,
18859
- ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
18860
- ` },`,
18861
- ` });`,
18862
- ``,
18863
- ` const response = await ollama.chat({`,
18864
- ` model: "deepseek-v3.1:671b",`,
18865
- ` messages: [`,
18866
- ` {`,
18867
- ` role: "system",`,
18868
- ` content: [`,
18869
- ` "В ответ напиши торговую стратегию где нет ничего лишнего,",`,
18870
- ` "только отчёт готовый для копипасты целиком",`,
18871
- ` "",`,
18872
- ` "**ВАЖНО**: Не здоровайся, не говори что делаешь - только отчёт!"`,
18873
- ` ].join("\\n"),`,
18874
- ` },`,
18875
- ` ...messages,`,
18876
- ` {`,
18877
- ` role: "user",`,
18878
- ` content: [`,
18879
- ` "На каких условиях мне купить ${escapedSymbol}?",`,
18880
- ` "Дай анализ рынка на основе поддержки/сопротивления, точек входа в LONG/SHORT позиции.",`,
18881
- ` "Какой RR ставить для позиций?",`,
18882
- ` "Предпочтительны LONG или SHORT позиции?",`,
18883
- ` "",`,
18884
- ` "Сделай не сухой технический, а фундаментальный анализ, содержащий стратигическую рекомендацию, например, покупать на низу боковика"`,
18885
- ` ].join("\\n")`,
18886
- ` }`,
18887
- ` ]`,
18888
- ` });`,
18889
- ``,
18890
- ` const content = response.message.content.trim();`,
18891
- ` return content`,
18892
- ` .replace(/\\\\/g, '\\\\\\\\')`,
18893
- ` .replace(/\`/g, '\\\\\`')`,
18894
- ` .replace(/\\$/g, '\\\\$')`,
18895
- ` .replace(/"/g, '\\\\"')`,
18896
- ` .replace(/'/g, "\\\\'");`,
18897
- `}`
18898
- ].join("\n");
18439
+ await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
18440
+ }
18441
+ }
18442
+ if (shouldPersist) {
18443
+ await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
18444
+ }
18445
+ };
18446
+ /**
18447
+ * Internal loss handler function for ClientPartial.
18448
+ *
18449
+ * Checks which loss levels have been reached and emits events for new levels only.
18450
+ * Uses Set-based deduplication to prevent duplicate events.
18451
+ * Converts negative lossPercent to absolute value for level comparison.
18452
+ *
18453
+ * @param symbol - Trading pair symbol
18454
+ * @param data - Signal row data
18455
+ * @param currentPrice - Current market price
18456
+ * @param lossPercent - Current loss percentage (negative value)
18457
+ * @param backtest - True if backtest mode
18458
+ * @param when - Event timestamp
18459
+ * @param self - ClientPartial instance reference
18460
+ */
18461
+ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
18462
+ if (self._states === NEED_FETCH$1) {
18463
+ throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
18464
+ }
18465
+ if (data.id !== self.params.signalId) {
18466
+ throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
18467
+ }
18468
+ let state = self._states.get(data.id);
18469
+ if (!state) {
18470
+ state = {
18471
+ profitLevels: new Set(),
18472
+ lossLevels: new Set(),
18899
18473
  };
18900
- /**
18901
- * Generates json() helper for structured LLM output.
18902
- * Uses Ollama with JSON schema for trading signals.
18903
- *
18904
- * Signal schema:
18905
- * - position: "wait" | "long" | "short"
18906
- * - note: strategy explanation
18907
- * - priceOpen: entry price
18908
- * - priceTakeProfit: target price
18909
- * - priceStopLoss: stop price
18910
- * - minuteEstimatedTime: expected duration (max 360 min)
18911
- *
18912
- * @param symbol - Trading pair symbol (unused, for consistency)
18913
- * @returns Generated async json() function with signal schema
18914
- */
18915
- this.getJsonTemplate = async (symbol) => {
18916
- this.loggerService.log("optimizerTemplateService getJsonTemplate", {
18474
+ self._states.set(data.id, state);
18475
+ }
18476
+ const absLoss = Math.abs(lossPercent);
18477
+ let shouldPersist = false;
18478
+ for (const level of LOSS_LEVELS) {
18479
+ if (absLoss >= level && !state.lossLevels.has(level)) {
18480
+ state.lossLevels.add(level);
18481
+ shouldPersist = true;
18482
+ self.params.logger.debug("ClientPartial loss level reached", {
18917
18483
  symbol,
18484
+ signalId: data.id,
18485
+ level,
18486
+ lossPercent,
18487
+ backtest,
18918
18488
  });
18919
- return [
18920
- `async function json(messages) {`,
18921
- ` const ollama = new Ollama({`,
18922
- ` host: "https://ollama.com",`,
18923
- ` headers: {`,
18924
- ` Authorization: \`Bearer \${process.env.OLLAMA_API_KEY}\`,`,
18925
- ` },`,
18926
- ` });`,
18927
- ``,
18928
- ` const response = await ollama.chat({`,
18929
- ` model: "deepseek-v3.1:671b",`,
18930
- ` messages: [`,
18931
- ` {`,
18932
- ` role: "system",`,
18933
- ` content: [`,
18934
- ` "Проанализируй торговую стратегию и верни торговый сигнал.",`,
18935
- ` "",`,
18936
- ` "ПРАВИЛА ОТКРЫТИЯ ПОЗИЦИЙ:",`,
18937
- ` "",`,
18938
- ` "1. ТИПЫ ПОЗИЦИЙ:",`,
18939
- ` " - position='wait': нет четкого сигнала, жди лучших условий",`,
18940
- ` " - position='long': бычий сигнал, цена будет расти",`,
18941
- ` " - position='short': медвежий сигнал, цена будет падать",`,
18942
- ` "",`,
18943
- ` "2. ЦЕНА ВХОДА (priceOpen):",`,
18944
- ` " - Может быть текущей рыночной ценой для немедленного входа",`,
18945
- ` " - Может быть отложенной ценой для входа при достижении уровня",`,
18946
- ` " - Укажи оптимальную цену входа согласно технического анализа",`,
18947
- ` "",`,
18948
- ` "3. УРОВНИ ВЫХОДА:",`,
18949
- ` " - LONG: priceTakeProfit > priceOpen > priceStopLoss",`,
18950
- ` " - SHORT: priceStopLoss > priceOpen > priceTakeProfit",`,
18951
- ` " - Уровни должны иметь техническое обоснование (Fibonacci, S/R, Bollinger)",`,
18952
- ` "",`,
18953
- ` "4. ВРЕМЕННЫЕ РАМКИ:",`,
18954
- ` " - minuteEstimatedTime: прогноз времени до TP (макс 360 минут)",`,
18955
- ` " - Расчет на основе ATR, ADX, MACD, Momentum, Slope",`,
18956
- ` " - Если индикаторов, осциллятор или других метрик нет, посчитай их самостоятельно",`,
18957
- ` ].join("\\n"),`,
18958
- ` },`,
18959
- ` ...messages,`,
18960
- ` ],`,
18961
- ` format: {`,
18962
- ` type: "object",`,
18963
- ` properties: {`,
18964
- ` position: {`,
18965
- ` type: "string",`,
18966
- ` enum: ["wait", "long", "short"],`,
18967
- ` description: "Trade decision: wait (no signal), long (buy), or short (sell)",`,
18968
- ` },`,
18969
- ` note: {`,
18970
- ` type: "string",`,
18971
- ` description: "Professional trading recommendation with price levels",`,
18972
- ` },`,
18973
- ` priceOpen: {`,
18974
- ` type: "number",`,
18975
- ` description: "Entry price (current market price or limit order price)",`,
18976
- ` },`,
18977
- ` priceTakeProfit: {`,
18978
- ` type: "number",`,
18979
- ` description: "Take profit target price",`,
18980
- ` },`,
18981
- ` priceStopLoss: {`,
18982
- ` type: "number",`,
18983
- ` description: "Stop loss exit price",`,
18984
- ` },`,
18985
- ` minuteEstimatedTime: {`,
18986
- ` type: "number",`,
18987
- ` description: "Expected time to reach TP in minutes (max 360)",`,
18988
- ` },`,
18989
- ` },`,
18990
- ` required: ["position", "note", "priceOpen", "priceTakeProfit", "priceStopLoss", "minuteEstimatedTime"],`,
18991
- ` },`,
18992
- ` });`,
18993
- ``,
18994
- ` const jsonResponse = JSON.parse(response.message.content.trim());`,
18995
- ` return jsonResponse;`,
18996
- `}`
18997
- ].join("\n");
18998
- };
18489
+ await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
18490
+ }
18999
18491
  }
19000
- }
19001
-
18492
+ if (shouldPersist) {
18493
+ await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
18494
+ }
18495
+ };
19002
18496
  /**
19003
- * Service for managing optimizer schema registration and retrieval.
19004
- * Provides validation and registry management for optimizer configurations.
19005
- *
19006
- * Uses ToolRegistry for immutable schema storage.
19007
- */
19008
- class OptimizerSchemaService {
19009
- constructor() {
19010
- this.loggerService = inject(TYPES.loggerService);
19011
- this._registry = new ToolRegistry("optimizerSchema");
19012
- /**
19013
- * Registers a new optimizer schema.
19014
- * Validates required fields before registration.
19015
- *
19016
- * @param key - Unique optimizer name
19017
- * @param value - Optimizer schema configuration
19018
- * @throws Error if schema validation fails
19019
- */
19020
- this.register = (key, value) => {
19021
- this.loggerService.log(`optimizerSchemaService register`, { key });
19022
- this.validateShallow(value);
19023
- this._registry = this._registry.register(key, value);
19024
- };
19025
- /**
19026
- * Validates optimizer schema structure.
19027
- * Checks required fields: optimizerName, rangeTrain, source, getPrompt.
19028
- *
19029
- * @param optimizerSchema - Schema to validate
19030
- * @throws Error if validation fails
19031
- */
19032
- this.validateShallow = (optimizerSchema) => {
19033
- this.loggerService.log(`optimizerTemplateService validateShallow`, {
19034
- optimizerSchema,
19035
- });
19036
- if (typeof optimizerSchema.optimizerName !== "string") {
19037
- throw new Error(`optimizer template validation failed: missing optimizerName`);
19038
- }
19039
- if (!Array.isArray(optimizerSchema.rangeTrain) || optimizerSchema.rangeTrain.length === 0) {
19040
- throw new Error(`optimizer template validation failed: rangeTrain must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
19041
- }
19042
- if (!Array.isArray(optimizerSchema.source) || optimizerSchema.source.length === 0) {
19043
- throw new Error(`optimizer template validation failed: source must be a non-empty array for optimizerName=${optimizerSchema.optimizerName}`);
19044
- }
19045
- if (typeof optimizerSchema.getPrompt !== "function") {
19046
- throw new Error(`optimizer template validation failed: getPrompt must be a function for optimizerName=${optimizerSchema.optimizerName}`);
19047
- }
19048
- };
19049
- /**
19050
- * Partially overrides an existing optimizer schema.
19051
- * Merges provided values with existing schema.
19052
- *
19053
- * @param key - Optimizer name to override
19054
- * @param value - Partial schema values to merge
19055
- * @returns Updated complete schema
19056
- * @throws Error if optimizer not found
19057
- */
19058
- this.override = (key, value) => {
19059
- this.loggerService.log(`optimizerSchemaService override`, { key });
19060
- this._registry = this._registry.override(key, value);
19061
- return this._registry.get(key);
19062
- };
19063
- /**
19064
- * Retrieves optimizer schema by name.
19065
- *
19066
- * @param key - Optimizer name
19067
- * @returns Complete optimizer schema
19068
- * @throws Error if optimizer not found
19069
- */
19070
- this.get = (key) => {
19071
- this.loggerService.log(`optimizerSchemaService get`, { key });
19072
- return this._registry.get(key);
19073
- };
19074
- }
19075
- }
19076
-
19077
- /**
19078
- * Service for validating optimizer existence and managing optimizer registry.
19079
- * Maintains a Map of registered optimizers for validation purposes.
19080
- *
19081
- * Uses memoization for efficient repeated validation checks.
19082
- */
19083
- class OptimizerValidationService {
19084
- constructor() {
19085
- this.loggerService = inject(TYPES.loggerService);
19086
- this._optimizerMap = new Map();
19087
- /**
19088
- * Adds optimizer to validation registry.
19089
- * Prevents duplicate optimizer names.
19090
- *
19091
- * @param optimizerName - Unique optimizer identifier
19092
- * @param optimizerSchema - Complete optimizer schema
19093
- * @throws Error if optimizer with same name already exists
19094
- */
19095
- this.addOptimizer = (optimizerName, optimizerSchema) => {
19096
- this.loggerService.log("optimizerValidationService addOptimizer", {
19097
- optimizerName,
19098
- optimizerSchema,
19099
- });
19100
- if (this._optimizerMap.has(optimizerName)) {
19101
- throw new Error(`optimizer ${optimizerName} already exist`);
19102
- }
19103
- this._optimizerMap.set(optimizerName, optimizerSchema);
19104
- };
19105
- /**
19106
- * Validates that optimizer exists in registry.
19107
- * Memoized for performance on repeated checks.
19108
- *
19109
- * @param optimizerName - Optimizer name to validate
19110
- * @param source - Source method name for error messages
19111
- * @throws Error if optimizer not found
19112
- */
19113
- this.validate = memoize(([optimizerName]) => optimizerName, (optimizerName, source) => {
19114
- this.loggerService.log("optimizerValidationService validate", {
19115
- optimizerName,
19116
- source,
19117
- });
19118
- const optimizer = this._optimizerMap.get(optimizerName);
19119
- if (!optimizer) {
19120
- throw new Error(`optimizer ${optimizerName} not found source=${source}`);
19121
- }
19122
- return true;
19123
- });
19124
- /**
19125
- * Lists all registered optimizer schemas.
19126
- *
19127
- * @returns Array of all optimizer schemas
19128
- */
19129
- this.list = async () => {
19130
- this.loggerService.log("optimizerValidationService list");
19131
- return Array.from(this._optimizerMap.values());
19132
- };
19133
- }
19134
- }
19135
-
19136
- const METHOD_NAME_GET_DATA = "optimizerGlobalService getData";
19137
- const METHOD_NAME_GET_CODE = "optimizerGlobalService getCode";
19138
- const METHOD_NAME_DUMP = "optimizerGlobalService dump";
19139
- /**
19140
- * Global service for optimizer operations with validation.
19141
- * Entry point for public API, performs validation before delegating to ConnectionService.
19142
- *
19143
- * Workflow:
19144
- * 1. Log operation
19145
- * 2. Validate optimizer exists
19146
- * 3. Delegate to OptimizerConnectionService
19147
- */
19148
- class OptimizerGlobalService {
19149
- constructor() {
19150
- this.loggerService = inject(TYPES.loggerService);
19151
- this.optimizerConnectionService = inject(TYPES.optimizerConnectionService);
19152
- this.optimizerValidationService = inject(TYPES.optimizerValidationService);
19153
- /**
19154
- * Fetches data from all sources and generates strategy metadata.
19155
- * Validates optimizer existence before execution.
19156
- *
19157
- * @param symbol - Trading pair symbol
19158
- * @param optimizerName - Optimizer identifier
19159
- * @returns Array of generated strategies with conversation context
19160
- * @throws Error if optimizer not found
19161
- */
19162
- this.getData = async (symbol, optimizerName) => {
19163
- this.loggerService.log(METHOD_NAME_GET_DATA, {
19164
- symbol,
19165
- optimizerName,
19166
- });
19167
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_DATA);
19168
- return await this.optimizerConnectionService.getData(symbol, optimizerName);
19169
- };
19170
- /**
19171
- * Generates complete executable strategy code.
19172
- * Validates optimizer existence before execution.
19173
- *
19174
- * @param symbol - Trading pair symbol
19175
- * @param optimizerName - Optimizer identifier
19176
- * @returns Generated TypeScript/JavaScript code as string
19177
- * @throws Error if optimizer not found
19178
- */
19179
- this.getCode = async (symbol, optimizerName) => {
19180
- this.loggerService.log(METHOD_NAME_GET_CODE, {
19181
- symbol,
19182
- optimizerName,
19183
- });
19184
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_GET_CODE);
19185
- return await this.optimizerConnectionService.getCode(symbol, optimizerName);
19186
- };
19187
- /**
19188
- * Generates and saves strategy code to file.
19189
- * Validates optimizer existence before execution.
19190
- *
19191
- * @param symbol - Trading pair symbol
19192
- * @param optimizerName - Optimizer identifier
19193
- * @param path - Output directory path (optional)
19194
- * @throws Error if optimizer not found
19195
- */
19196
- this.dump = async (symbol, optimizerName, path) => {
19197
- this.loggerService.log(METHOD_NAME_DUMP, {
19198
- symbol,
19199
- optimizerName,
19200
- path,
19201
- });
19202
- this.optimizerValidationService.validate(optimizerName, METHOD_NAME_DUMP);
19203
- return await this.optimizerConnectionService.dump(symbol, optimizerName, path);
19204
- };
19205
- }
19206
- }
19207
-
19208
- const ITERATION_LIMIT = 25;
19209
- const DEFAULT_SOURCE_NAME = "unknown";
19210
- const CREATE_PREFIX_FN = () => (Math.random() + 1).toString(36).substring(7);
19211
- /**
19212
- * Wrapper to call onSourceData callback with error handling.
19213
- * Catches and logs any errors thrown by the user-provided callback.
19214
- */
19215
- const CALL_SOURCE_DATA_CALLBACKS_FN = trycatch(async (self, symbol, name, data, startDate, endDate) => {
19216
- if (self.params.callbacks?.onSourceData) {
19217
- await self.params.callbacks.onSourceData(symbol, name, data, startDate, endDate);
19218
- }
19219
- }, {
19220
- fallback: (error) => {
19221
- const message = "ClientOptimizer CALL_SOURCE_DATA_CALLBACKS_FN thrown";
19222
- const payload = {
19223
- error: errorData(error),
19224
- message: getErrorMessage(error),
19225
- };
19226
- bt.loggerService.warn(message, payload);
19227
- console.warn(message, payload);
19228
- errorEmitter.next(error);
19229
- },
19230
- });
19231
- /**
19232
- * Wrapper to call onData callback with error handling.
19233
- * Catches and logs any errors thrown by the user-provided callback.
19234
- */
19235
- const CALL_DATA_CALLBACKS_FN = trycatch(async (self, symbol, strategyList) => {
19236
- if (self.params.callbacks?.onData) {
19237
- await self.params.callbacks.onData(symbol, strategyList);
19238
- }
19239
- }, {
19240
- fallback: (error) => {
19241
- const message = "ClientOptimizer CALL_DATA_CALLBACKS_FN thrown";
19242
- const payload = {
19243
- error: errorData(error),
19244
- message: getErrorMessage(error),
19245
- };
19246
- bt.loggerService.warn(message, payload);
19247
- console.warn(message, payload);
19248
- errorEmitter.next(error);
19249
- },
19250
- });
19251
- /**
19252
- * Wrapper to call onCode callback with error handling.
19253
- * Catches and logs any errors thrown by the user-provided callback.
19254
- */
19255
- const CALL_CODE_CALLBACKS_FN = trycatch(async (self, symbol, code) => {
19256
- if (self.params.callbacks?.onCode) {
19257
- await self.params.callbacks.onCode(symbol, code);
19258
- }
19259
- }, {
19260
- fallback: (error) => {
19261
- const message = "ClientOptimizer CALL_CODE_CALLBACKS_FN thrown";
19262
- const payload = {
19263
- error: errorData(error),
19264
- message: getErrorMessage(error),
19265
- };
19266
- bt.loggerService.warn(message, payload);
19267
- console.warn(message, payload);
19268
- errorEmitter.next(error);
19269
- },
19270
- });
19271
- /**
19272
- * Wrapper to call onDump callback with error handling.
19273
- * Catches and logs any errors thrown by the user-provided callback.
19274
- */
19275
- const CALL_DUMP_CALLBACKS_FN = trycatch(async (self, symbol, filepath) => {
19276
- if (self.params.callbacks?.onDump) {
19277
- await self.params.callbacks.onDump(symbol, filepath);
19278
- }
19279
- }, {
19280
- fallback: (error) => {
19281
- const message = "ClientOptimizer CALL_DUMP_CALLBACKS_FN thrown";
19282
- const payload = {
19283
- error: errorData(error),
19284
- message: getErrorMessage(error),
19285
- };
19286
- bt.loggerService.warn(message, payload);
19287
- console.warn(message, payload);
19288
- errorEmitter.next(error);
19289
- },
19290
- });
19291
- /**
19292
- * Default user message formatter.
19293
- * Delegates to template's getUserMessage method.
19294
- *
19295
- * @param symbol - Trading pair symbol
19296
- * @param data - Fetched data array
19297
- * @param name - Source name
19298
- * @param self - ClientOptimizer instance
19299
- * @returns Formatted user message content
19300
- */
19301
- const DEFAULT_USER_FN = async (symbol, data, name, self) => {
19302
- return await self.params.template.getUserMessage(symbol, data, name);
19303
- };
19304
- /**
19305
- * Default assistant message formatter.
19306
- * Delegates to template's getAssistantMessage method.
19307
- *
19308
- * @param symbol - Trading pair symbol
19309
- * @param data - Fetched data array
19310
- * @param name - Source name
19311
- * @param self - ClientOptimizer instance
19312
- * @returns Formatted assistant message content
19313
- */
19314
- const DEFAULT_ASSISTANT_FN = async (symbol, data, name, self) => {
19315
- return await self.params.template.getAssistantMessage(symbol, data, name);
19316
- };
19317
- /**
19318
- * Resolves paginated data from source with deduplication.
19319
- * Uses iterateDocuments to handle pagination automatically.
19320
- *
19321
- * @param fetch - Source fetch function
19322
- * @param filterData - Filter arguments (symbol, dates)
19323
- * @returns Deduplicated array of all fetched data
19324
- */
19325
- const RESOLVE_PAGINATION_FN = async (fetch, filterData) => {
19326
- const iterator = iterateDocuments({
19327
- limit: ITERATION_LIMIT,
19328
- async createRequest({ limit, offset }) {
19329
- return await fetch({
19330
- symbol: filterData.symbol,
19331
- startDate: filterData.startDate,
19332
- endDate: filterData.endDate,
19333
- limit,
19334
- offset,
19335
- });
19336
- },
19337
- });
19338
- const distinct = distinctDocuments(iterator, (data) => data.id);
19339
- return await resolveDocuments(distinct);
19340
- };
19341
- /**
19342
- * Collects data from all sources and generates strategy metadata.
19343
- * Iterates through training ranges, fetches data from each source,
19344
- * builds LLM conversation history, and generates strategy prompts.
19345
- *
19346
- * @param symbol - Trading pair symbol
19347
- * @param self - ClientOptimizer instance
19348
- * @returns Array of generated strategies with conversation context
19349
- */
19350
- const GET_STRATEGY_DATA_FN = async (symbol, self) => {
19351
- const strategyList = [];
19352
- const totalSources = self.params.rangeTrain.length * self.params.source.length;
19353
- let processedSources = 0;
19354
- for (const { startDate, endDate } of self.params.rangeTrain) {
19355
- const messageList = [];
19356
- for (const source of self.params.source) {
19357
- // Emit progress event at the start of processing each source
19358
- await self.onProgress({
19359
- optimizerName: self.params.optimizerName,
19360
- symbol,
19361
- totalSources,
19362
- processedSources,
19363
- progress: totalSources > 0 ? processedSources / totalSources : 0,
19364
- });
19365
- if (typeof source === "function") {
19366
- const data = await RESOLVE_PAGINATION_FN(source, {
19367
- symbol,
19368
- startDate,
19369
- endDate,
19370
- });
19371
- await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, DEFAULT_SOURCE_NAME, data, startDate, endDate);
19372
- const [userContent, assistantContent] = await Promise.all([
19373
- DEFAULT_USER_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
19374
- DEFAULT_ASSISTANT_FN(symbol, data, DEFAULT_SOURCE_NAME, self),
19375
- ]);
19376
- messageList.push({
19377
- role: "user",
19378
- content: userContent,
19379
- }, {
19380
- role: "assistant",
19381
- content: assistantContent,
19382
- });
19383
- processedSources++;
19384
- }
19385
- else {
19386
- const { fetch, name = DEFAULT_SOURCE_NAME, assistant = DEFAULT_ASSISTANT_FN, user = DEFAULT_USER_FN, } = source;
19387
- const data = await RESOLVE_PAGINATION_FN(fetch, {
19388
- symbol,
19389
- startDate,
19390
- endDate,
19391
- });
19392
- await CALL_SOURCE_DATA_CALLBACKS_FN(self, symbol, name, data, startDate, endDate);
19393
- const [userContent, assistantContent] = await Promise.all([
19394
- user(symbol, data, name, self),
19395
- assistant(symbol, data, name, self),
19396
- ]);
19397
- messageList.push({
19398
- role: "user",
19399
- content: userContent,
19400
- }, {
19401
- role: "assistant",
19402
- content: assistantContent,
19403
- });
19404
- processedSources++;
19405
- }
19406
- const name = "name" in source
19407
- ? source.name || DEFAULT_SOURCE_NAME
19408
- : DEFAULT_SOURCE_NAME;
19409
- strategyList.push({
19410
- symbol,
19411
- name,
19412
- messages: messageList,
19413
- strategy: await self.params.getPrompt(symbol, messageList),
19414
- });
19415
- }
19416
- }
19417
- // Emit final progress event (100%)
19418
- await self.onProgress({
19419
- optimizerName: self.params.optimizerName,
19420
- symbol,
19421
- totalSources,
19422
- processedSources: totalSources,
19423
- progress: 1.0,
19424
- });
19425
- await CALL_DATA_CALLBACKS_FN(self, symbol, strategyList);
19426
- return strategyList;
19427
- };
19428
- /**
19429
- * Generates complete executable strategy code.
19430
- * Assembles all components: imports, helpers, exchange, frames, strategies, walker, launcher.
19431
- *
19432
- * @param symbol - Trading pair symbol
19433
- * @param self - ClientOptimizer instance
19434
- * @returns Generated TypeScript/JavaScript code as string
19435
- */
19436
- const GET_STRATEGY_CODE_FN = async (symbol, self) => {
19437
- const strategyData = await self.getData(symbol);
19438
- const prefix = CREATE_PREFIX_FN();
19439
- const sections = [];
19440
- const exchangeName = `${prefix}_exchange`;
19441
- // 1. Top banner with imports
19442
- {
19443
- sections.push(await self.params.template.getTopBanner(symbol));
19444
- sections.push("");
19445
- }
19446
- // 2. JSON dump helper function
19447
- {
19448
- sections.push(await self.params.template.getJsonDumpTemplate(symbol));
19449
- sections.push("");
19450
- }
19451
- // 3. Helper functions (text and json)
19452
- {
19453
- sections.push(await self.params.template.getTextTemplate(symbol));
19454
- sections.push("");
19455
- }
19456
- {
19457
- sections.push(await self.params.template.getJsonTemplate(symbol));
19458
- sections.push("");
19459
- }
19460
- // 4. Exchange template (assuming first strategy has exchange info)
19461
- {
19462
- sections.push(await self.params.template.getExchangeTemplate(symbol, exchangeName));
19463
- sections.push("");
19464
- }
19465
- // 5. Train frame templates
19466
- {
19467
- for (let i = 0; i < self.params.rangeTrain.length; i++) {
19468
- const range = self.params.rangeTrain[i];
19469
- const frameName = `${prefix}_train_frame-${i + 1}`;
19470
- sections.push(await self.params.template.getFrameTemplate(symbol, frameName, "1m", // default interval
19471
- range.startDate, range.endDate));
19472
- sections.push("");
19473
- }
19474
- }
19475
- // 6. Test frame template
19476
- {
19477
- const testFrameName = `${prefix}_test_frame`;
19478
- sections.push(await self.params.template.getFrameTemplate(symbol, testFrameName, "1m", // default interval
19479
- self.params.rangeTest.startDate, self.params.rangeTest.endDate));
19480
- sections.push("");
19481
- }
19482
- // 7. Strategy templates for each generated strategy
19483
- {
19484
- for (let i = 0; i < strategyData.length; i++) {
19485
- const strategy = strategyData[i];
19486
- const strategyName = `${prefix}_strategy-${i + 1}`;
19487
- const interval = "5m"; // default interval
19488
- sections.push(await self.params.template.getStrategyTemplate(strategyName, interval, strategy.strategy));
19489
- sections.push("");
19490
- }
19491
- }
19492
- // 8. Walker template (uses test frame for validation)
19493
- {
19494
- const walkerName = `${prefix}_walker`;
19495
- const testFrameName = `${prefix}_test_frame`;
19496
- const strategies = strategyData.map((_, i) => `${prefix}_strategy-${i + 1}`);
19497
- sections.push(await self.params.template.getWalkerTemplate(walkerName, `${exchangeName}`, testFrameName, strategies));
19498
- sections.push("");
19499
- }
19500
- // 9. Launcher template
19501
- {
19502
- const walkerName = `${prefix}_walker`;
19503
- sections.push(await self.params.template.getLauncherTemplate(symbol, walkerName));
19504
- sections.push("");
19505
- }
19506
- const code = sections.join("\n");
19507
- await CALL_CODE_CALLBACKS_FN(self, symbol, code);
19508
- return code;
19509
- };
19510
- /**
19511
- * Saves generated strategy code to file.
19512
- * Creates directory if needed, writes .mjs file with generated code.
19513
- *
19514
- * @param symbol - Trading pair symbol
19515
- * @param path - Output directory path
19516
- * @param self - ClientOptimizer instance
19517
- */
19518
- const GET_STRATEGY_DUMP_FN = async (symbol, path, self) => {
19519
- const report = await self.getCode(symbol);
19520
- try {
19521
- const dir = join(process.cwd(), path);
19522
- await mkdir(dir, { recursive: true });
19523
- const filename = `${self.params.optimizerName}_${symbol}.mjs`;
19524
- const filepath = join(dir, filename);
19525
- await writeFile(filepath, report, "utf-8");
19526
- self.params.logger.info(`Optimizer report saved: ${filepath}`);
19527
- await CALL_DUMP_CALLBACKS_FN(self, symbol, filepath);
19528
- }
19529
- catch (error) {
19530
- self.params.logger.warn(`Failed to save optimizer report:`, error);
19531
- throw error;
19532
- }
19533
- };
19534
- /**
19535
- * Client implementation for optimizer operations.
19536
- *
19537
- * Features:
19538
- * - Data collection from multiple sources with pagination
19539
- * - LLM conversation history building
19540
- * - Strategy code generation with templates
19541
- * - File export with callbacks
19542
- *
19543
- * Used by OptimizerConnectionService to create optimizer instances.
19544
- */
19545
- class ClientOptimizer {
19546
- constructor(params, onProgress) {
19547
- this.params = params;
19548
- this.onProgress = onProgress;
19549
- /**
19550
- * Fetches data from all sources and generates strategy metadata.
19551
- * Processes each training range and builds LLM conversation history.
19552
- *
19553
- * @param symbol - Trading pair symbol
19554
- * @returns Array of generated strategies with conversation context
19555
- */
19556
- this.getData = async (symbol) => {
19557
- this.params.logger.debug("ClientOptimizer getData", {
19558
- symbol,
19559
- });
19560
- return await GET_STRATEGY_DATA_FN(symbol, this);
19561
- };
19562
- /**
19563
- * Generates complete executable strategy code.
19564
- * Includes imports, helpers, strategies, walker, and launcher.
19565
- *
19566
- * @param symbol - Trading pair symbol
19567
- * @returns Generated TypeScript/JavaScript code as string
19568
- */
19569
- this.getCode = async (symbol) => {
19570
- this.params.logger.debug("ClientOptimizer getCode", {
19571
- symbol,
19572
- });
19573
- return await GET_STRATEGY_CODE_FN(symbol, this);
19574
- };
19575
- /**
19576
- * Generates and saves strategy code to file.
19577
- * Creates directory if needed, writes .mjs file.
19578
- *
19579
- * @param symbol - Trading pair symbol
19580
- * @param path - Output directory path (default: "./")
19581
- */
19582
- this.dump = async (symbol, path = "./") => {
19583
- this.params.logger.debug("ClientOptimizer dump", {
19584
- symbol,
19585
- path,
19586
- });
19587
- return await GET_STRATEGY_DUMP_FN(symbol, path, this);
19588
- };
19589
- }
19590
- }
19591
-
19592
- /**
19593
- * Callback function for emitting progress events to progressOptimizerEmitter.
19594
- */
19595
- const COMMIT_PROGRESS_FN = async (progress) => progressOptimizerEmitter.next(progress);
19596
- /**
19597
- * Service for creating and caching optimizer client instances.
19598
- * Handles dependency injection and template merging.
19599
- *
19600
- * Features:
19601
- * - Memoized optimizer instances (one per optimizerName)
19602
- * - Template merging (custom + defaults)
19603
- * - Logger injection
19604
- * - Delegates to ClientOptimizer for actual operations
19605
- */
19606
- class OptimizerConnectionService {
19607
- constructor() {
19608
- this.loggerService = inject(TYPES.loggerService);
19609
- this.optimizerSchemaService = inject(TYPES.optimizerSchemaService);
19610
- this.optimizerTemplateService = inject(TYPES.optimizerTemplateService);
19611
- /**
19612
- * Creates or retrieves cached optimizer instance.
19613
- * Memoized by optimizerName for performance.
19614
- *
19615
- * Merges custom templates from schema with defaults from OptimizerTemplateService.
19616
- *
19617
- * @param optimizerName - Unique optimizer identifier
19618
- * @returns ClientOptimizer instance with resolved dependencies
19619
- */
19620
- this.getOptimizer = memoize(([optimizerName]) => `${optimizerName}`, (optimizerName) => {
19621
- const { getPrompt, rangeTest, rangeTrain, source, template: rawTemplate = {}, callbacks, } = this.optimizerSchemaService.get(optimizerName);
19622
- 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;
19623
- const template = {
19624
- getAssistantMessage,
19625
- getExchangeTemplate,
19626
- getFrameTemplate,
19627
- getJsonDumpTemplate,
19628
- getJsonTemplate,
19629
- getLauncherTemplate,
19630
- getStrategyTemplate,
19631
- getTextTemplate,
19632
- getWalkerTemplate,
19633
- getTopBanner,
19634
- getUserMessage,
19635
- };
19636
- return new ClientOptimizer({
19637
- optimizerName,
19638
- logger: this.loggerService,
19639
- getPrompt,
19640
- rangeTest,
19641
- rangeTrain,
19642
- source,
19643
- template,
19644
- callbacks,
19645
- }, COMMIT_PROGRESS_FN);
19646
- });
19647
- /**
19648
- * Fetches data from all sources and generates strategy metadata.
19649
- *
19650
- * @param symbol - Trading pair symbol
19651
- * @param optimizerName - Optimizer identifier
19652
- * @returns Array of generated strategies with conversation context
19653
- */
19654
- this.getData = async (symbol, optimizerName) => {
19655
- this.loggerService.log("optimizerConnectionService getData", {
19656
- symbol,
19657
- optimizerName,
19658
- });
19659
- const optimizer = this.getOptimizer(optimizerName);
19660
- return await optimizer.getData(symbol);
19661
- };
19662
- /**
19663
- * Generates complete executable strategy code.
19664
- *
19665
- * @param symbol - Trading pair symbol
19666
- * @param optimizerName - Optimizer identifier
19667
- * @returns Generated TypeScript/JavaScript code as string
19668
- */
19669
- this.getCode = async (symbol, optimizerName) => {
19670
- this.loggerService.log("optimizerConnectionService getCode", {
19671
- symbol,
19672
- optimizerName,
19673
- });
19674
- const optimizer = this.getOptimizer(optimizerName);
19675
- return await optimizer.getCode(symbol);
19676
- };
19677
- /**
19678
- * Generates and saves strategy code to file.
19679
- *
19680
- * @param symbol - Trading pair symbol
19681
- * @param optimizerName - Optimizer identifier
19682
- * @param path - Output directory path (optional)
19683
- */
19684
- this.dump = async (symbol, optimizerName, path) => {
19685
- this.loggerService.log("optimizerConnectionService getCode", {
19686
- symbol,
19687
- optimizerName,
19688
- });
19689
- const optimizer = this.getOptimizer(optimizerName);
19690
- return await optimizer.dump(symbol, path);
19691
- };
19692
- }
19693
- }
19694
-
19695
- /**
19696
- * Symbol marker indicating that partial state needs initialization.
19697
- * Used as sentinel value for _states before waitForInit() is called.
19698
- */
19699
- const NEED_FETCH$1 = Symbol("need_fetch");
19700
- /**
19701
- * Array of profit level milestones to track (10%, 20%, ..., 100%).
19702
- * Each level is checked during profit() method to emit events for newly reached levels.
19703
- */
19704
- const PROFIT_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
19705
- /**
19706
- * Array of loss level milestones to track (-10%, -20%, ..., -100%).
19707
- * Each level is checked during loss() method to emit events for newly reached levels.
19708
- */
19709
- const LOSS_LEVELS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
19710
- /**
19711
- * Internal profit handler function for ClientPartial.
19712
- *
19713
- * Checks which profit levels have been reached and emits events for new levels only.
19714
- * Uses Set-based deduplication to prevent duplicate events.
19715
- *
19716
- * @param symbol - Trading pair symbol
19717
- * @param data - Signal row data
19718
- * @param currentPrice - Current market price
19719
- * @param revenuePercent - Current profit percentage (positive value)
19720
- * @param backtest - True if backtest mode
19721
- * @param when - Event timestamp
19722
- * @param self - ClientPartial instance reference
19723
- */
19724
- const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, backtest, when, self) => {
19725
- if (self._states === NEED_FETCH$1) {
19726
- throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
19727
- }
19728
- if (data.id !== self.params.signalId) {
19729
- throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
19730
- }
19731
- let state = self._states.get(data.id);
19732
- if (!state) {
19733
- state = {
19734
- profitLevels: new Set(),
19735
- lossLevels: new Set(),
19736
- };
19737
- self._states.set(data.id, state);
19738
- }
19739
- let shouldPersist = false;
19740
- for (const level of PROFIT_LEVELS) {
19741
- if (revenuePercent >= level && !state.profitLevels.has(level)) {
19742
- state.profitLevels.add(level);
19743
- shouldPersist = true;
19744
- self.params.logger.debug("ClientPartial profit level reached", {
19745
- symbol,
19746
- signalId: data.id,
19747
- level,
19748
- revenuePercent,
19749
- backtest,
19750
- });
19751
- await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
19752
- }
19753
- }
19754
- if (shouldPersist) {
19755
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
19756
- }
19757
- };
19758
- /**
19759
- * Internal loss handler function for ClientPartial.
19760
- *
19761
- * Checks which loss levels have been reached and emits events for new levels only.
19762
- * Uses Set-based deduplication to prevent duplicate events.
19763
- * Converts negative lossPercent to absolute value for level comparison.
19764
- *
19765
- * @param symbol - Trading pair symbol
19766
- * @param data - Signal row data
19767
- * @param currentPrice - Current market price
19768
- * @param lossPercent - Current loss percentage (negative value)
19769
- * @param backtest - True if backtest mode
19770
- * @param when - Event timestamp
19771
- * @param self - ClientPartial instance reference
19772
- */
19773
- const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest, when, self) => {
19774
- if (self._states === NEED_FETCH$1) {
19775
- throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
19776
- }
19777
- if (data.id !== self.params.signalId) {
19778
- throw new Error(`Signal ID mismatch: expected ${self.params.signalId}, got ${data.id}`);
19779
- }
19780
- let state = self._states.get(data.id);
19781
- if (!state) {
19782
- state = {
19783
- profitLevels: new Set(),
19784
- lossLevels: new Set(),
19785
- };
19786
- self._states.set(data.id, state);
19787
- }
19788
- const absLoss = Math.abs(lossPercent);
19789
- let shouldPersist = false;
19790
- for (const level of LOSS_LEVELS) {
19791
- if (absLoss >= level && !state.lossLevels.has(level)) {
19792
- state.lossLevels.add(level);
19793
- shouldPersist = true;
19794
- self.params.logger.debug("ClientPartial loss level reached", {
19795
- symbol,
19796
- signalId: data.id,
19797
- level,
19798
- lossPercent,
19799
- backtest,
19800
- });
19801
- await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, level, backtest, when.getTime());
19802
- }
19803
- }
19804
- if (shouldPersist) {
19805
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
19806
- }
19807
- };
19808
- /**
19809
- * Internal initialization function for ClientPartial.
18497
+ * Internal initialization function for ClientPartial.
19810
18498
  *
19811
18499
  * Loads persisted partial state from disk and restores in-memory Maps.
19812
18500
  * Converts serialized arrays back to Sets for O(1) lookups.
@@ -21989,162 +20677,6 @@ class BreakevenGlobalService {
21989
20677
  }
21990
20678
  }
21991
20679
 
21992
- /**
21993
- * Warning threshold for message size in kilobytes.
21994
- * Messages exceeding this size trigger console warnings.
21995
- */
21996
- const WARN_KB = 30;
21997
- /**
21998
- * Internal function for dumping signal data to markdown files.
21999
- * Creates a directory structure with system prompts, user messages, and LLM output.
22000
- *
22001
- * @param signalId - Unique identifier for the result
22002
- * @param history - Array of message models from LLM conversation
22003
- * @param signal - Signal DTO with trade parameters
22004
- * @param outputDir - Output directory path (default: "./dump/strategy")
22005
- * @returns Promise that resolves when all files are written
22006
- */
22007
- const DUMP_SIGNAL_FN = async (signalId, history, signal, outputDir = "./dump/outline") => {
22008
- // Extract system messages and system reminders from existing data
22009
- const systemMessages = history.filter((m) => m.role === "system");
22010
- const userMessages = history.filter((m) => m.role === "user");
22011
- const subfolderPath = path.join(outputDir, String(signalId));
22012
- // Generate system prompt markdown
22013
- {
22014
- let summary = "# Outline Result Summary\n";
22015
- {
22016
- summary += "\n";
22017
- summary += `**ResultId**: ${String(signalId)}\n`;
22018
- summary += "\n";
22019
- }
22020
- if (signal) {
22021
- summary += "## Output Data\n\n";
22022
- summary += "```json\n";
22023
- summary += JSON.stringify(signal, null, 2);
22024
- summary += "\n```\n\n";
22025
- }
22026
- // Add system messages to summary
22027
- if (systemMessages.length > 0) {
22028
- summary += "## System Messages\n\n";
22029
- systemMessages.forEach((msg, idx) => {
22030
- summary += `### System Message ${idx + 1}\n\n`;
22031
- summary += msg.content;
22032
- summary += "\n";
22033
- });
22034
- }
22035
- await Markdown.writeData("outline", summary, {
22036
- path: subfolderPath,
22037
- file: "00_system_prompt.md",
22038
- symbol: "",
22039
- signalId: String(signalId),
22040
- strategyName: "",
22041
- exchangeName: "",
22042
- frameName: ""
22043
- });
22044
- }
22045
- // Generate user messages
22046
- {
22047
- await Promise.all(Array.from(userMessages.entries()).map(async ([idx, message]) => {
22048
- const messageNum = String(idx + 1).padStart(2, "0");
22049
- const contentFileName = `${messageNum}_user_message.md`;
22050
- {
22051
- const messageSizeBytes = Buffer.byteLength(message.content, "utf8");
22052
- const messageSizeKb = Math.floor(messageSizeBytes / 1024);
22053
- if (messageSizeKb > WARN_KB) {
22054
- console.warn(`User message ${idx + 1} is ${messageSizeBytes} bytes (${messageSizeKb}kb), which exceeds warning limit`);
22055
- }
22056
- }
22057
- let content = `# User Input ${idx + 1}\n\n`;
22058
- content += `**ResultId**: ${String(signalId)}\n\n`;
22059
- content += message.content;
22060
- content += "\n";
22061
- await Markdown.writeData("outline", content, {
22062
- path: subfolderPath,
22063
- file: contentFileName,
22064
- signalId: String(signalId),
22065
- symbol: "",
22066
- strategyName: "",
22067
- exchangeName: "",
22068
- frameName: ""
22069
- });
22070
- }));
22071
- }
22072
- // Generate LLM output
22073
- {
22074
- const messageNum = String(userMessages.length + 1).padStart(2, "0");
22075
- const contentFileName = `${messageNum}_llm_output.md`;
22076
- let content = "# Full Outline Result\n\n";
22077
- content += `**ResultId**: ${String(signalId)}\n\n`;
22078
- if (signal) {
22079
- content += "## Output Data\n\n";
22080
- content += "```json\n";
22081
- content += JSON.stringify(signal, null, 2);
22082
- content += "\n```\n";
22083
- }
22084
- await Markdown.writeData("outline", content, {
22085
- path: subfolderPath,
22086
- file: contentFileName,
22087
- symbol: "",
22088
- signalId: String(signalId),
22089
- strategyName: "",
22090
- exchangeName: "",
22091
- frameName: ""
22092
- });
22093
- }
22094
- };
22095
- /**
22096
- * Service for generating markdown documentation from LLM outline results.
22097
- * Used by AI Strategy Optimizer to save debug logs and conversation history.
22098
- *
22099
- * Creates directory structure:
22100
- * - ./dump/strategy/{signalId}/00_system_prompt.md - System messages and output data
22101
- * - ./dump/strategy/{signalId}/01_user_message.md - First user input
22102
- * - ./dump/strategy/{signalId}/02_user_message.md - Second user input
22103
- * - ./dump/strategy/{signalId}/XX_llm_output.md - Final LLM output
22104
- */
22105
- class OutlineMarkdownService {
22106
- constructor() {
22107
- /** Logger service injected via DI */
22108
- this.loggerService = inject(TYPES.loggerService);
22109
- /**
22110
- * Dumps signal data and conversation history to markdown files.
22111
- * Skips if directory already exists to avoid overwriting previous results.
22112
- *
22113
- * Generated files:
22114
- * - 00_system_prompt.md - System messages and output summary
22115
- * - XX_user_message.md - Each user message in separate file (numbered)
22116
- * - XX_llm_output.md - Final LLM output with signal data
22117
- *
22118
- * @param signalId - Unique identifier for the result (used as directory name)
22119
- * @param history - Array of message models from LLM conversation
22120
- * @param signal - Signal DTO with trade parameters (priceOpen, TP, SL, etc.)
22121
- * @param outputDir - Output directory path (default: "./dump/strategy")
22122
- * @returns Promise that resolves when all files are written
22123
- *
22124
- * @example
22125
- * ```typescript
22126
- * await outlineService.dumpSignal(
22127
- * "strategy-1",
22128
- * conversationHistory,
22129
- * { position: "long", priceTakeProfit: 51000, priceStopLoss: 49000, minuteEstimatedTime: 60 }
22130
- * );
22131
- * // Creates: ./dump/strategy/strategy-1/00_system_prompt.md
22132
- * // ./dump/strategy/strategy-1/01_user_message.md
22133
- * // ./dump/strategy/strategy-1/02_llm_output.md
22134
- * ```
22135
- */
22136
- this.dumpSignal = async (signalId, history, signal, outputDir = "./dump/strategy") => {
22137
- this.loggerService.log("outlineMarkdownService dumpSignal", {
22138
- signalId,
22139
- history,
22140
- signal,
22141
- outputDir,
22142
- });
22143
- return await DUMP_SIGNAL_FN(signalId, history, signal, outputDir);
22144
- };
22145
- }
22146
- }
22147
-
22148
20680
  /**
22149
20681
  * Service for validating GLOBAL_CONFIG parameters to ensure mathematical correctness
22150
20682
  * and prevent unprofitable trading configurations.
@@ -24530,111 +23062,6 @@ class RiskReportService {
24530
23062
  }
24531
23063
  }
24532
23064
 
24533
- const require = createRequire(import.meta.url);
24534
- /**
24535
- * Default fallback prompt configuration.
24536
- * Used when signal.prompt.cjs file is not found.
24537
- */
24538
- const DEFAULT_PROMPT = {
24539
- user: "",
24540
- system: [],
24541
- };
24542
- /**
24543
- * Lazy-loads and caches signal prompt configuration.
24544
- * Attempts to load from config/prompt/signal.prompt.cjs, falls back to DEFAULT_PROMPT if not found.
24545
- * Uses singleshot pattern to ensure configuration is loaded only once.
24546
- * @returns Prompt configuration with system and user prompts
24547
- */
24548
- const GET_PROMPT_FN = singleshot(() => {
24549
- try {
24550
- const modulePath = require.resolve(path.join(process.cwd(), `./config/prompt/signal.prompt.cjs`));
24551
- console.log(`Using ${modulePath} implementation as signal.prompt.cjs`);
24552
- return require(modulePath);
24553
- }
24554
- catch (error) {
24555
- console.log(`Using empty fallback for signal.prompt.cjs`, error);
24556
- return DEFAULT_PROMPT;
24557
- }
24558
- });
24559
- /**
24560
- * Service for managing signal prompts for AI/LLM integrations.
24561
- *
24562
- * Provides access to system and user prompts configured in signal.prompt.cjs.
24563
- * Supports both static prompt arrays and dynamic prompt functions.
24564
- *
24565
- * Key responsibilities:
24566
- * - Lazy-loads prompt configuration from config/prompt/signal.prompt.cjs
24567
- * - Resolves system prompts (static arrays or async functions)
24568
- * - Provides user prompt strings
24569
- * - Falls back to empty prompts if configuration is missing
24570
- *
24571
- * Used for AI-powered signal analysis and strategy recommendations.
24572
- */
24573
- class SignalPromptService {
24574
- constructor() {
24575
- this.loggerService = inject(TYPES.loggerService);
24576
- /**
24577
- * Retrieves system prompts for AI context.
24578
- *
24579
- * System prompts can be:
24580
- * - Static array of strings (returned directly)
24581
- * - Async/sync function returning string array (executed and awaited)
24582
- * - Undefined (returns empty array)
24583
- *
24584
- * @param symbol - Trading symbol (e.g., "BTCUSDT")
24585
- * @param strategyName - Strategy identifier
24586
- * @param exchangeName - Exchange identifier
24587
- * @param frameName - Timeframe identifier
24588
- * @param backtest - Whether running in backtest mode
24589
- * @returns Promise resolving to array of system prompt strings
24590
- */
24591
- this.getSystemPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
24592
- this.loggerService.log("signalPromptService getSystemPrompt", {
24593
- symbol,
24594
- strategyName,
24595
- exchangeName,
24596
- frameName,
24597
- backtest,
24598
- });
24599
- const { system } = GET_PROMPT_FN();
24600
- if (Array.isArray(system)) {
24601
- return system;
24602
- }
24603
- if (typeof system === "function") {
24604
- return await system(symbol, strategyName, exchangeName, frameName, backtest);
24605
- }
24606
- return [];
24607
- };
24608
- /**
24609
- * Retrieves user prompt string for AI input.
24610
- *
24611
- * @param symbol - Trading symbol (e.g., "BTCUSDT")
24612
- * @param strategyName - Strategy identifier
24613
- * @param exchangeName - Exchange identifier
24614
- * @param frameName - Timeframe identifier
24615
- * @param backtest - Whether running in backtest mode
24616
- * @returns Promise resolving to user prompt string
24617
- */
24618
- this.getUserPrompt = async (symbol, strategyName, exchangeName, frameName, backtest) => {
24619
- this.loggerService.log("signalPromptService getUserPrompt", {
24620
- symbol,
24621
- strategyName,
24622
- exchangeName,
24623
- frameName,
24624
- backtest,
24625
- });
24626
- const { user } = GET_PROMPT_FN();
24627
- if (typeof user === "string") {
24628
- return user;
24629
- }
24630
- if (typeof user === "function") {
24631
- return await user(symbol, strategyName, exchangeName, frameName, backtest);
24632
- }
24633
- return "";
24634
- };
24635
- }
24636
- }
24637
-
24638
23065
  {
24639
23066
  provide(TYPES.loggerService, () => new LoggerService());
24640
23067
  }
@@ -24649,7 +23076,6 @@ class SignalPromptService {
24649
23076
  provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
24650
23077
  provide(TYPES.riskConnectionService, () => new RiskConnectionService());
24651
23078
  provide(TYPES.actionConnectionService, () => new ActionConnectionService());
24652
- provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
24653
23079
  provide(TYPES.partialConnectionService, () => new PartialConnectionService());
24654
23080
  provide(TYPES.breakevenConnectionService, () => new BreakevenConnectionService());
24655
23081
  }
@@ -24661,7 +23087,6 @@ class SignalPromptService {
24661
23087
  provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
24662
23088
  provide(TYPES.riskSchemaService, () => new RiskSchemaService());
24663
23089
  provide(TYPES.actionSchemaService, () => new ActionSchemaService());
24664
- provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
24665
23090
  }
24666
23091
  {
24667
23092
  provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
@@ -24672,7 +23097,6 @@ class SignalPromptService {
24672
23097
  {
24673
23098
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
24674
23099
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
24675
- provide(TYPES.optimizerGlobalService, () => new OptimizerGlobalService());
24676
23100
  provide(TYPES.partialGlobalService, () => new PartialGlobalService());
24677
23101
  provide(TYPES.breakevenGlobalService, () => new BreakevenGlobalService());
24678
23102
  }
@@ -24700,7 +23124,6 @@ class SignalPromptService {
24700
23124
  provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
24701
23125
  provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
24702
23126
  provide(TYPES.breakevenMarkdownService, () => new BreakevenMarkdownService());
24703
- provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
24704
23127
  provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
24705
23128
  }
24706
23129
  {
@@ -24722,16 +23145,9 @@ class SignalPromptService {
24722
23145
  provide(TYPES.sizingValidationService, () => new SizingValidationService());
24723
23146
  provide(TYPES.riskValidationService, () => new RiskValidationService());
24724
23147
  provide(TYPES.actionValidationService, () => new ActionValidationService());
24725
- provide(TYPES.optimizerValidationService, () => new OptimizerValidationService());
24726
23148
  provide(TYPES.configValidationService, () => new ConfigValidationService());
24727
23149
  provide(TYPES.columnValidationService, () => new ColumnValidationService());
24728
23150
  }
24729
- {
24730
- provide(TYPES.optimizerTemplateService, () => new OptimizerTemplateService());
24731
- }
24732
- {
24733
- provide(TYPES.signalPromptService, () => new SignalPromptService());
24734
- }
24735
23151
 
24736
23152
  const baseServices = {
24737
23153
  loggerService: inject(TYPES.loggerService),
@@ -24747,7 +23163,6 @@ const connectionServices = {
24747
23163
  sizingConnectionService: inject(TYPES.sizingConnectionService),
24748
23164
  riskConnectionService: inject(TYPES.riskConnectionService),
24749
23165
  actionConnectionService: inject(TYPES.actionConnectionService),
24750
- optimizerConnectionService: inject(TYPES.optimizerConnectionService),
24751
23166
  partialConnectionService: inject(TYPES.partialConnectionService),
24752
23167
  breakevenConnectionService: inject(TYPES.breakevenConnectionService),
24753
23168
  };
@@ -24759,7 +23174,6 @@ const schemaServices = {
24759
23174
  sizingSchemaService: inject(TYPES.sizingSchemaService),
24760
23175
  riskSchemaService: inject(TYPES.riskSchemaService),
24761
23176
  actionSchemaService: inject(TYPES.actionSchemaService),
24762
- optimizerSchemaService: inject(TYPES.optimizerSchemaService),
24763
23177
  };
24764
23178
  const coreServices = {
24765
23179
  exchangeCoreService: inject(TYPES.exchangeCoreService),
@@ -24770,7 +23184,6 @@ const coreServices = {
24770
23184
  const globalServices = {
24771
23185
  sizingGlobalService: inject(TYPES.sizingGlobalService),
24772
23186
  riskGlobalService: inject(TYPES.riskGlobalService),
24773
- optimizerGlobalService: inject(TYPES.optimizerGlobalService),
24774
23187
  partialGlobalService: inject(TYPES.partialGlobalService),
24775
23188
  breakevenGlobalService: inject(TYPES.breakevenGlobalService),
24776
23189
  };
@@ -24798,7 +23211,6 @@ const markdownServices = {
24798
23211
  heatMarkdownService: inject(TYPES.heatMarkdownService),
24799
23212
  partialMarkdownService: inject(TYPES.partialMarkdownService),
24800
23213
  breakevenMarkdownService: inject(TYPES.breakevenMarkdownService),
24801
- outlineMarkdownService: inject(TYPES.outlineMarkdownService),
24802
23214
  riskMarkdownService: inject(TYPES.riskMarkdownService),
24803
23215
  };
24804
23216
  const reportServices = {
@@ -24820,16 +23232,9 @@ const validationServices = {
24820
23232
  sizingValidationService: inject(TYPES.sizingValidationService),
24821
23233
  riskValidationService: inject(TYPES.riskValidationService),
24822
23234
  actionValidationService: inject(TYPES.actionValidationService),
24823
- optimizerValidationService: inject(TYPES.optimizerValidationService),
24824
23235
  configValidationService: inject(TYPES.configValidationService),
24825
23236
  columnValidationService: inject(TYPES.columnValidationService),
24826
23237
  };
24827
- const templateServices = {
24828
- optimizerTemplateService: inject(TYPES.optimizerTemplateService),
24829
- };
24830
- const promptServices = {
24831
- signalPromptService: inject(TYPES.signalPromptService),
24832
- };
24833
23238
  const backtest = {
24834
23239
  ...baseServices,
24835
23240
  ...contextServices,
@@ -24843,8 +23248,6 @@ const backtest = {
24843
23248
  ...markdownServices,
24844
23249
  ...reportServices,
24845
23250
  ...validationServices,
24846
- ...templateServices,
24847
- ...promptServices,
24848
23251
  };
24849
23252
  init();
24850
23253
  var bt = backtest;
@@ -24943,18 +23346,6 @@ const getSizingMap = async () => {
24943
23346
  }
24944
23347
  return sizingMap;
24945
23348
  };
24946
- /**
24947
- * Retrieves all registered optimizers as a map
24948
- * @private
24949
- * @returns Map of optimizer names
24950
- */
24951
- const getOptimizerMap = async () => {
24952
- const optimizerMap = {};
24953
- for (const { optimizerName } of await bt.optimizerValidationService.list()) {
24954
- Object.assign(optimizerMap, { [optimizerName]: optimizerName });
24955
- }
24956
- return optimizerMap;
24957
- };
24958
23349
  /**
24959
23350
  * Retrieves all registered walkers as a map
24960
23351
  * @private
@@ -24981,7 +23372,7 @@ const getWalkerMap = async () => {
24981
23372
  * @throws {Error} If any entity name is not found in its registry
24982
23373
  */
24983
23374
  const validateInternal = async (args) => {
24984
- 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;
23375
+ const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
24985
23376
  for (const exchangeName of Object.values(ExchangeName)) {
24986
23377
  bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
24987
23378
  }
@@ -25000,9 +23391,6 @@ const validateInternal = async (args) => {
25000
23391
  for (const sizingName of Object.values(SizingName)) {
25001
23392
  bt.sizingValidationService.validate(sizingName, METHOD_NAME);
25002
23393
  }
25003
- for (const optimizerName of Object.values(OptimizerName)) {
25004
- bt.optimizerValidationService.validate(optimizerName, METHOD_NAME);
25005
- }
25006
23394
  for (const walkerName of Object.values(WalkerName)) {
25007
23395
  bt.walkerValidationService.validate(walkerName, METHOD_NAME);
25008
23396
  }
@@ -25011,7 +23399,7 @@ const validateInternal = async (args) => {
25011
23399
  * Validates the existence of all provided entity names across validation services.
25012
23400
  *
25013
23401
  * This function accepts enum objects for various entity types (exchanges, frames,
25014
- * strategies, risks, sizings, optimizers, walkers) and validates that each entity
23402
+ * strategies, risks, sizings, walkers) and validates that each entity
25015
23403
  * name exists in its respective registry. Validation results are memoized for performance.
25016
23404
  *
25017
23405
  * If no arguments are provided (or specific entity types are omitted), the function
@@ -25071,7 +23459,6 @@ const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
25071
23459
  const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
25072
23460
  const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
25073
23461
  const GET_RISK_METHOD_NAME = "get.getRiskSchema";
25074
- const GET_OPTIMIZER_METHOD_NAME = "get.getOptimizerSchema";
25075
23462
  const GET_ACTION_METHOD_NAME = "get.getActionSchema";
25076
23463
  /**
25077
23464
  * Retrieves a registered strategy schema by name.
@@ -25203,29 +23590,6 @@ function getRiskSchema(riskName) {
25203
23590
  bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
25204
23591
  return bt.riskSchemaService.get(riskName);
25205
23592
  }
25206
- /**
25207
- * Retrieves a registered optimizer schema by name.
25208
- *
25209
- * @param optimizerName - Unique optimizer identifier
25210
- * @returns The optimizer schema configuration object
25211
- * @throws Error if optimizer is not registered
25212
- *
25213
- * @example
25214
- * ```typescript
25215
- * const optimizer = getOptimizer("llm-strategy-generator");
25216
- * console.log(optimizer.rangeTrain); // Array of training ranges
25217
- * console.log(optimizer.rangeTest); // Testing range
25218
- * console.log(optimizer.source); // Array of data sources
25219
- * console.log(optimizer.getPrompt); // async function
25220
- * ```
25221
- */
25222
- function getOptimizerSchema(optimizerName) {
25223
- bt.loggerService.log(GET_OPTIMIZER_METHOD_NAME, {
25224
- optimizerName,
25225
- });
25226
- bt.optimizerValidationService.validate(optimizerName, GET_OPTIMIZER_METHOD_NAME);
25227
- return bt.optimizerSchemaService.get(optimizerName);
25228
- }
25229
23593
  /**
25230
23594
  * Retrieves a registered action schema by name.
25231
23595
  *
@@ -25258,6 +23622,7 @@ const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
25258
23622
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25259
23623
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25260
23624
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
23625
+ const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25261
23626
  /**
25262
23627
  * Checks if trade context is active (execution and method contexts).
25263
23628
  *
@@ -25498,22 +23863,69 @@ async function getContext() {
25498
23863
  * console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
25499
23864
  * console.log(orderBook.asks); // [{ price: "50001.00", quantity: "0.3" }, ...]
25500
23865
  *
25501
- * // Fetch deeper order book
25502
- * const deepBook = await getOrderBook("BTCUSDT", 100);
23866
+ * // Fetch deeper order book
23867
+ * const deepBook = await getOrderBook("BTCUSDT", 100);
23868
+ * ```
23869
+ */
23870
+ async function getOrderBook(symbol, depth) {
23871
+ bt.loggerService.info(GET_ORDER_BOOK_METHOD_NAME, {
23872
+ symbol,
23873
+ depth,
23874
+ });
23875
+ if (!ExecutionContextService.hasContext()) {
23876
+ throw new Error("getOrderBook requires an execution context");
23877
+ }
23878
+ if (!MethodContextService.hasContext()) {
23879
+ throw new Error("getOrderBook requires a method context");
23880
+ }
23881
+ return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
23882
+ }
23883
+ /**
23884
+ * Fetches raw candles with flexible date/limit parameters.
23885
+ *
23886
+ * All modes respect execution context and prevent look-ahead bias.
23887
+ *
23888
+ * Parameter combinations:
23889
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
23890
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
23891
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
23892
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
23893
+ * 5. Only limit: uses execution.context.when as reference (backward)
23894
+ *
23895
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
23896
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
23897
+ * @param limit - Optional number of candles to fetch
23898
+ * @param sDate - Optional start date in milliseconds
23899
+ * @param eDate - Optional end date in milliseconds
23900
+ * @returns Promise resolving to array of candle data
23901
+ *
23902
+ * @example
23903
+ * ```typescript
23904
+ * // Fetch 100 candles backward from current context time
23905
+ * const candles = await getRawCandles("BTCUSDT", "1m", 100);
23906
+ *
23907
+ * // Fetch candles for specific date range
23908
+ * const rangeCandles = await getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
23909
+ *
23910
+ * // Fetch with all parameters specified
23911
+ * const exactCandles = await getRawCandles("BTCUSDT", "1m", 100, startMs, endMs);
25503
23912
  * ```
25504
23913
  */
25505
- async function getOrderBook(symbol, depth) {
25506
- bt.loggerService.info(GET_ORDER_BOOK_METHOD_NAME, {
23914
+ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
23915
+ bt.loggerService.info(GET_RAW_CANDLES_METHOD_NAME, {
25507
23916
  symbol,
25508
- depth,
23917
+ interval,
23918
+ limit,
23919
+ sDate,
23920
+ eDate,
25509
23921
  });
25510
23922
  if (!ExecutionContextService.hasContext()) {
25511
- throw new Error("getOrderBook requires an execution context");
23923
+ throw new Error("getRawCandles requires an execution context");
25512
23924
  }
25513
23925
  if (!MethodContextService.hasContext()) {
25514
- throw new Error("getOrderBook requires a method context");
23926
+ throw new Error("getRawCandles requires a method context");
25515
23927
  }
25516
- return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
23928
+ return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25517
23929
  }
25518
23930
 
25519
23931
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
@@ -26038,7 +24450,6 @@ const ADD_FRAME_METHOD_NAME = "add.addFrameSchema";
26038
24450
  const ADD_WALKER_METHOD_NAME = "add.addWalkerSchema";
26039
24451
  const ADD_SIZING_METHOD_NAME = "add.addSizingSchema";
26040
24452
  const ADD_RISK_METHOD_NAME = "add.addRiskSchema";
26041
- const ADD_OPTIMIZER_METHOD_NAME = "add.addOptimizerSchema";
26042
24453
  const ADD_ACTION_METHOD_NAME = "add.addActionSchema";
26043
24454
  /**
26044
24455
  * Registers a trading strategy in the framework.
@@ -26331,100 +24742,6 @@ function addRiskSchema(riskSchema) {
26331
24742
  bt.riskValidationService.addRisk(riskSchema.riskName, riskSchema);
26332
24743
  bt.riskSchemaService.register(riskSchema.riskName, riskSchema);
26333
24744
  }
26334
- /**
26335
- * Registers an optimizer configuration in the framework.
26336
- *
26337
- * The optimizer generates trading strategies by:
26338
- * - Collecting data from multiple sources across training periods
26339
- * - Building LLM conversation history with fetched data
26340
- * - Generating strategy prompts using getPrompt()
26341
- * - Creating executable backtest code with templates
26342
- *
26343
- * The optimizer produces a complete .mjs file containing:
26344
- * - Exchange, Frame, Strategy, and Walker configurations
26345
- * - Multi-timeframe analysis logic
26346
- * - LLM integration for signal generation
26347
- * - Event listeners for progress tracking
26348
- *
26349
- * @param optimizerSchema - Optimizer configuration object
26350
- * @param optimizerSchema.optimizerName - Unique optimizer identifier
26351
- * @param optimizerSchema.rangeTrain - Array of training time ranges (each generates a strategy variant)
26352
- * @param optimizerSchema.rangeTest - Testing time range for strategy validation
26353
- * @param optimizerSchema.source - Array of data sources (functions or source objects with custom formatters)
26354
- * @param optimizerSchema.getPrompt - Function to generate strategy prompt from conversation history
26355
- * @param optimizerSchema.template - Optional custom template overrides (top banner, helpers, strategy logic, etc.)
26356
- * @param optimizerSchema.callbacks - Optional lifecycle callbacks (onData, onCode, onDump, onSourceData)
26357
- *
26358
- * @example
26359
- * ```typescript
26360
- * // Basic optimizer with single data source
26361
- * addOptimizer({
26362
- * optimizerName: "llm-strategy-generator",
26363
- * rangeTrain: [
26364
- * {
26365
- * note: "Bull market period",
26366
- * startDate: new Date("2024-01-01"),
26367
- * endDate: new Date("2024-01-31"),
26368
- * },
26369
- * {
26370
- * note: "Bear market period",
26371
- * startDate: new Date("2024-02-01"),
26372
- * endDate: new Date("2024-02-28"),
26373
- * },
26374
- * ],
26375
- * rangeTest: {
26376
- * note: "Validation period",
26377
- * startDate: new Date("2024-03-01"),
26378
- * endDate: new Date("2024-03-31"),
26379
- * },
26380
- * source: [
26381
- * {
26382
- * name: "historical-backtests",
26383
- * fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
26384
- * // Fetch historical backtest results from database
26385
- * return await db.backtests.find({
26386
- * symbol,
26387
- * date: { $gte: startDate, $lte: endDate },
26388
- * })
26389
- * .skip(offset)
26390
- * .limit(limit);
26391
- * },
26392
- * user: async (symbol, data, name) => {
26393
- * return `Analyze these ${data.length} backtest results for ${symbol}:\n${JSON.stringify(data)}`;
26394
- * },
26395
- * assistant: async (symbol, data, name) => {
26396
- * return "Historical data analyzed successfully";
26397
- * },
26398
- * },
26399
- * ],
26400
- * getPrompt: async (symbol, messages) => {
26401
- * // Generate strategy prompt from conversation
26402
- * return `"Analyze ${symbol} using RSI and MACD. Enter LONG when RSI < 30 and MACD crosses above signal."`;
26403
- * },
26404
- * callbacks: {
26405
- * onData: (symbol, strategyData) => {
26406
- * console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
26407
- * },
26408
- * onCode: (symbol, code) => {
26409
- * console.log(`Generated ${code.length} characters of code for ${symbol}`);
26410
- * },
26411
- * onDump: (symbol, filepath) => {
26412
- * console.log(`Saved strategy to ${filepath}`);
26413
- * },
26414
- * onSourceData: (symbol, sourceName, data, startDate, endDate) => {
26415
- * console.log(`Fetched ${data.length} rows from ${sourceName} for ${symbol}`);
26416
- * },
26417
- * },
26418
- * });
26419
- * ```
26420
- */
26421
- function addOptimizerSchema(optimizerSchema) {
26422
- bt.loggerService.info(ADD_OPTIMIZER_METHOD_NAME, {
26423
- optimizerSchema,
26424
- });
26425
- bt.optimizerValidationService.addOptimizer(optimizerSchema.optimizerName, optimizerSchema);
26426
- bt.optimizerSchemaService.register(optimizerSchema.optimizerName, optimizerSchema);
26427
- }
26428
24745
  /**
26429
24746
  * Registers an action handler in the framework.
26430
24747
  *
@@ -26507,7 +24824,6 @@ const METHOD_NAME_OVERRIDE_FRAME = "function.override.overrideFrameSchema";
26507
24824
  const METHOD_NAME_OVERRIDE_WALKER = "function.override.overrideWalkerSchema";
26508
24825
  const METHOD_NAME_OVERRIDE_SIZING = "function.override.overrideSizingSchema";
26509
24826
  const METHOD_NAME_OVERRIDE_RISK = "function.override.overrideRiskSchema";
26510
- const METHOD_NAME_OVERRIDE_OPTIMIZER = "function.override.overrideOptimizerSchema";
26511
24827
  const METHOD_NAME_OVERRIDE_ACTION = "function.override.overrideActionSchema";
26512
24828
  /**
26513
24829
  * Overrides an existing trading strategy in the framework.
@@ -26680,40 +24996,6 @@ async function overrideRiskSchema(riskSchema) {
26680
24996
  await bt.riskValidationService.validate(riskSchema.riskName, METHOD_NAME_OVERRIDE_RISK);
26681
24997
  return bt.riskSchemaService.override(riskSchema.riskName, riskSchema);
26682
24998
  }
26683
- /**
26684
- * Overrides an existing optimizer configuration in the framework.
26685
- *
26686
- * This function partially updates a previously registered optimizer with new configuration.
26687
- * Only the provided fields will be updated, other fields remain unchanged.
26688
- *
26689
- * @param optimizerSchema - Partial optimizer configuration object
26690
- * @param optimizerSchema.optimizerName - Unique optimizer identifier (must exist)
26691
- * @param optimizerSchema.rangeTrain - Optional: Array of training time ranges
26692
- * @param optimizerSchema.rangeTest - Optional: Testing time range
26693
- * @param optimizerSchema.source - Optional: Array of data sources
26694
- * @param optimizerSchema.getPrompt - Optional: Function to generate strategy prompt
26695
- * @param optimizerSchema.template - Optional: Custom template overrides
26696
- * @param optimizerSchema.callbacks - Optional: Lifecycle callbacks
26697
- *
26698
- * @example
26699
- * ```typescript
26700
- * overrideOptimizer({
26701
- * optimizerName: "llm-strategy-generator",
26702
- * rangeTest: {
26703
- * note: "Updated validation period",
26704
- * startDate: new Date("2024-04-01"),
26705
- * endDate: new Date("2024-04-30"),
26706
- * },
26707
- * });
26708
- * ```
26709
- */
26710
- async function overrideOptimizerSchema(optimizerSchema) {
26711
- bt.loggerService.log(METHOD_NAME_OVERRIDE_OPTIMIZER, {
26712
- optimizerSchema,
26713
- });
26714
- await bt.optimizerValidationService.validate(optimizerSchema.optimizerName, METHOD_NAME_OVERRIDE_OPTIMIZER);
26715
- return bt.optimizerSchemaService.override(optimizerSchema.optimizerName, optimizerSchema);
26716
- }
26717
24999
  /**
26718
25000
  * Overrides an existing action handler configuration in the framework.
26719
25001
  *
@@ -26788,7 +25070,6 @@ const LIST_FRAMES_METHOD_NAME = "list.listFrameSchema";
26788
25070
  const LIST_WALKERS_METHOD_NAME = "list.listWalkerSchema";
26789
25071
  const LIST_SIZINGS_METHOD_NAME = "list.listSizingSchema";
26790
25072
  const LIST_RISKS_METHOD_NAME = "list.listRiskSchema";
26791
- const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizerSchema";
26792
25073
  /**
26793
25074
  * Returns a list of all registered exchange schemas.
26794
25075
  *
@@ -26986,46 +25267,6 @@ async function listRiskSchema() {
26986
25267
  bt.loggerService.log(LIST_RISKS_METHOD_NAME);
26987
25268
  return await bt.riskValidationService.list();
26988
25269
  }
26989
- /**
26990
- * Returns a list of all registered optimizer schemas.
26991
- *
26992
- * Retrieves all optimizers that have been registered via addOptimizer().
26993
- * Useful for debugging, documentation, or building dynamic UIs.
26994
- *
26995
- * @returns Array of optimizer schemas with their configurations
26996
- *
26997
- * @example
26998
- * ```typescript
26999
- * import { listOptimizers, addOptimizer } from "backtest-kit";
27000
- *
27001
- * addOptimizer({
27002
- * optimizerName: "llm-strategy-generator",
27003
- * note: "Generates trading strategies using LLM",
27004
- * rangeTrain: [
27005
- * {
27006
- * note: "Training period 1",
27007
- * startDate: new Date("2024-01-01"),
27008
- * endDate: new Date("2024-01-31"),
27009
- * },
27010
- * ],
27011
- * rangeTest: {
27012
- * note: "Testing period",
27013
- * startDate: new Date("2024-02-01"),
27014
- * endDate: new Date("2024-02-28"),
27015
- * },
27016
- * source: [],
27017
- * getPrompt: async (symbol, messages) => "Generate strategy",
27018
- * });
27019
- *
27020
- * const optimizers = listOptimizers();
27021
- * console.log(optimizers);
27022
- * // [{ optimizerName: "llm-strategy-generator", note: "Generates...", ... }]
27023
- * ```
27024
- */
27025
- async function listOptimizerSchema() {
27026
- bt.loggerService.log(LIST_OPTIMIZERS_METHOD_NAME);
27027
- return await bt.optimizerValidationService.list();
27028
- }
27029
25270
 
27030
25271
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
27031
25272
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
@@ -27043,7 +25284,6 @@ const LISTEN_DONE_WALKER_METHOD_NAME = "event.listenDoneWalker";
27043
25284
  const LISTEN_DONE_WALKER_ONCE_METHOD_NAME = "event.listenDoneWalkerOnce";
27044
25285
  const LISTEN_PROGRESS_METHOD_NAME = "event.listenBacktestProgress";
27045
25286
  const LISTEN_PROGRESS_WALKER_METHOD_NAME = "event.listenWalkerProgress";
27046
- const LISTEN_PROGRESS_OPTIMIZER_METHOD_NAME = "event.listenOptimizerProgress";
27047
25287
  const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
27048
25288
  const LISTEN_WALKER_METHOD_NAME = "event.listenWalker";
27049
25289
  const LISTEN_WALKER_ONCE_METHOD_NAME = "event.listenWalkerOnce";
@@ -27531,34 +25771,6 @@ function listenWalkerProgress(fn) {
27531
25771
  bt.loggerService.log(LISTEN_PROGRESS_WALKER_METHOD_NAME);
27532
25772
  return progressWalkerEmitter.subscribe(queued(async (event) => fn(event)));
27533
25773
  }
27534
- /**
27535
- * Subscribes to optimizer progress events with queued async processing.
27536
- *
27537
- * Emits during optimizer execution to track data source processing progress.
27538
- * Events are processed sequentially in order received, even if callback is async.
27539
- * Uses queued wrapper to prevent concurrent execution of the callback.
27540
- *
27541
- * @param fn - Callback function to handle optimizer progress events
27542
- * @returns Unsubscribe function to stop listening to events
27543
- *
27544
- * @example
27545
- * ```typescript
27546
- * import { listenOptimizerProgress } from "backtest-kit";
27547
- *
27548
- * const unsubscribe = listenOptimizerProgress((event) => {
27549
- * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
27550
- * console.log(`${event.processedSources} / ${event.totalSources} sources`);
27551
- * console.log(`Optimizer: ${event.optimizerName}, Symbol: ${event.symbol}`);
27552
- * });
27553
- *
27554
- * // Later: stop listening
27555
- * unsubscribe();
27556
- * ```
27557
- */
27558
- function listenOptimizerProgress(fn) {
27559
- bt.loggerService.log(LISTEN_PROGRESS_OPTIMIZER_METHOD_NAME);
27560
- return progressOptimizerEmitter.subscribe(queued(async (event) => fn(event)));
27561
- }
27562
25774
  /**
27563
25775
  * Subscribes to performance metric events with queued async processing.
27564
25776
  *
@@ -28119,138 +26331,6 @@ function listenActivePingOnce(filterFn, fn) {
28119
26331
  return activePingSubject.filter(filterFn).once(fn);
28120
26332
  }
28121
26333
 
28122
- const METHOD_NAME_SIGNAL = "history.commitSignalPromptHistory";
28123
- /**
28124
- * Commits signal prompt history to the message array.
28125
- *
28126
- * Extracts trading context from ExecutionContext and MethodContext,
28127
- * then adds signal-specific system prompts at the beginning and user prompt
28128
- * at the end of the history array if they are not empty.
28129
- *
28130
- * Context extraction:
28131
- * - symbol: Provided as parameter for debugging convenience
28132
- * - backtest mode: From ExecutionContext
28133
- * - strategyName, exchangeName, frameName: From MethodContext
28134
- *
28135
- * @param symbol - Trading symbol (e.g., "BTCUSDT") for debugging convenience
28136
- * @param history - Message array to append prompts to
28137
- * @returns Promise that resolves when prompts are added
28138
- * @throws Error if ExecutionContext or MethodContext is not active
28139
- *
28140
- * @example
28141
- * ```typescript
28142
- * const messages: MessageModel[] = [];
28143
- * await commitSignalPromptHistory("BTCUSDT", messages);
28144
- * // messages now contains system prompts at start and user prompt at end
28145
- * ```
28146
- */
28147
- async function commitSignalPromptHistory(symbol, history) {
28148
- bt.loggerService.log(METHOD_NAME_SIGNAL, {
28149
- symbol,
28150
- });
28151
- if (!ExecutionContextService.hasContext()) {
28152
- throw new Error("commitSignalPromptHistory requires an execution context");
28153
- }
28154
- if (!MethodContextService.hasContext()) {
28155
- throw new Error("commitSignalPromptHistory requires a method context");
28156
- }
28157
- const { backtest: isBacktest } = bt.executionContextService.context;
28158
- const { strategyName, exchangeName, frameName } = bt.methodContextService.context;
28159
- const systemPrompts = await bt.signalPromptService.getSystemPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
28160
- const userPrompt = await bt.signalPromptService.getUserPrompt(symbol, strategyName, exchangeName, frameName, isBacktest);
28161
- if (systemPrompts.length > 0) {
28162
- for (const content of systemPrompts) {
28163
- history.unshift({
28164
- role: "system",
28165
- content,
28166
- });
28167
- }
28168
- }
28169
- if (userPrompt && userPrompt.trim() !== "") {
28170
- history.push({
28171
- role: "user",
28172
- content: userPrompt,
28173
- });
28174
- }
28175
- }
28176
-
28177
- const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
28178
- /**
28179
- * Dumps signal data and LLM conversation history to markdown files.
28180
- * Used by AI-powered strategies to save debug logs for analysis.
28181
- *
28182
- * Creates a directory structure with:
28183
- * - 00_system_prompt.md - System messages and output summary
28184
- * - XX_user_message.md - Each user message in separate file (numbered)
28185
- * - XX_llm_output.md - Final LLM output with signal data
28186
- *
28187
- * Skips if directory already exists to avoid overwriting previous results.
28188
- *
28189
- * @param signalId - Unique identifier for the result (used as directory name, e.g., UUID)
28190
- * @param history - Array of message models from LLM conversation
28191
- * @param signal - Signal DTO returned by LLM (position, priceOpen, TP, SL, etc.)
28192
- * @param outputDir - Output directory path (default: "./dump/strategy")
28193
- * @returns Promise that resolves when all files are written
28194
- *
28195
- * @example
28196
- * ```typescript
28197
- * import { dumpSignal, getCandles } from "backtest-kit";
28198
- * import { v4 as uuid } from "uuid";
28199
- *
28200
- * addStrategy({
28201
- * strategyName: "llm-strategy",
28202
- * interval: "5m",
28203
- * getSignal: async (symbol) => {
28204
- * const messages = [];
28205
- *
28206
- * // Build multi-timeframe analysis conversation
28207
- * const candles1h = await getCandles(symbol, "1h", 24);
28208
- * messages.push(
28209
- * { role: "user", content: `Analyze 1h trend:\n${formatCandles(candles1h)}` },
28210
- * { role: "assistant", content: "Trend analyzed" }
28211
- * );
28212
- *
28213
- * const candles5m = await getCandles(symbol, "5m", 24);
28214
- * messages.push(
28215
- * { role: "user", content: `Analyze 5m structure:\n${formatCandles(candles5m)}` },
28216
- * { role: "assistant", content: "Structure analyzed" }
28217
- * );
28218
- *
28219
- * // Request signal
28220
- * messages.push({
28221
- * role: "user",
28222
- * content: "Generate trading signal. Use position: 'wait' if uncertain."
28223
- * });
28224
- *
28225
- * const resultId = uuid();
28226
- * const signal = await llmRequest(messages);
28227
- *
28228
- * // Save conversation and result for debugging
28229
- * await dumpSignal(resultId, messages, signal);
28230
- *
28231
- * return signal;
28232
- * }
28233
- * });
28234
- *
28235
- * // Creates: ./dump/strategy/{uuid}/00_system_prompt.md
28236
- * // ./dump/strategy/{uuid}/01_user_message.md (1h analysis)
28237
- * // ./dump/strategy/{uuid}/02_assistant_message.md
28238
- * // ./dump/strategy/{uuid}/03_user_message.md (5m analysis)
28239
- * // ./dump/strategy/{uuid}/04_assistant_message.md
28240
- * // ./dump/strategy/{uuid}/05_user_message.md (signal request)
28241
- * // ./dump/strategy/{uuid}/06_llm_output.md (final signal)
28242
- * ```
28243
- */
28244
- async function dumpSignalData(signalId, history, signal, outputDir = "./dump/strategy") {
28245
- bt.loggerService.info(DUMP_SIGNAL_METHOD_NAME, {
28246
- signalId,
28247
- history,
28248
- signal,
28249
- outputDir,
28250
- });
28251
- return await bt.outlineMarkdownService.dumpSignal(signalId, history, signal, outputDir);
28252
- }
28253
-
28254
26334
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
28255
26335
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
28256
26336
  const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
@@ -31410,104 +29490,6 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
31410
29490
  };
31411
29491
  const PositionSize = PositionSizeUtils;
31412
29492
 
31413
- const OPTIMIZER_METHOD_NAME_GET_DATA = "OptimizerUtils.getData";
31414
- const OPTIMIZER_METHOD_NAME_GET_CODE = "OptimizerUtils.getCode";
31415
- const OPTIMIZER_METHOD_NAME_DUMP = "OptimizerUtils.dump";
31416
- /**
31417
- * Public API utilities for optimizer operations.
31418
- * Provides high-level methods for strategy generation and code export.
31419
- *
31420
- * Usage:
31421
- * ```typescript
31422
- * import { Optimizer } from "backtest-kit";
31423
- *
31424
- * // Get strategy data
31425
- * const strategies = await Optimizer.getData("BTCUSDT", {
31426
- * optimizerName: "my-optimizer"
31427
- * });
31428
- *
31429
- * // Generate code
31430
- * const code = await Optimizer.getCode("BTCUSDT", {
31431
- * optimizerName: "my-optimizer"
31432
- * });
31433
- *
31434
- * // Save to file
31435
- * await Optimizer.dump("BTCUSDT", {
31436
- * optimizerName: "my-optimizer"
31437
- * }, "./output");
31438
- * ```
31439
- */
31440
- class OptimizerUtils {
31441
- constructor() {
31442
- /**
31443
- * Fetches data from all sources and generates strategy metadata.
31444
- * Processes each training range and builds LLM conversation history.
31445
- *
31446
- * @param symbol - Trading pair symbol
31447
- * @param context - Context with optimizerName
31448
- * @returns Array of generated strategies with conversation context
31449
- * @throws Error if optimizer not found
31450
- */
31451
- this.getData = async (symbol, context) => {
31452
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_GET_DATA, {
31453
- symbol,
31454
- context,
31455
- });
31456
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_DATA);
31457
- return await bt.optimizerGlobalService.getData(symbol, context.optimizerName);
31458
- };
31459
- /**
31460
- * Generates complete executable strategy code.
31461
- * Includes imports, helpers, strategies, walker, and launcher.
31462
- *
31463
- * @param symbol - Trading pair symbol
31464
- * @param context - Context with optimizerName
31465
- * @returns Generated TypeScript/JavaScript code as string
31466
- * @throws Error if optimizer not found
31467
- */
31468
- this.getCode = async (symbol, context) => {
31469
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_GET_CODE, {
31470
- symbol,
31471
- context,
31472
- });
31473
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_GET_CODE);
31474
- return await bt.optimizerGlobalService.getCode(symbol, context.optimizerName);
31475
- };
31476
- /**
31477
- * Generates and saves strategy code to file.
31478
- * Creates directory if needed, writes .mjs file.
31479
- *
31480
- * Format: `{optimizerName}_{symbol}.mjs`
31481
- *
31482
- * @param symbol - Trading pair symbol
31483
- * @param context - Context with optimizerName
31484
- * @param path - Output directory path (default: "./")
31485
- * @throws Error if optimizer not found or file write fails
31486
- */
31487
- this.dump = async (symbol, context, path) => {
31488
- bt.loggerService.info(OPTIMIZER_METHOD_NAME_DUMP, {
31489
- symbol,
31490
- context,
31491
- path,
31492
- });
31493
- bt.optimizerValidationService.validate(context.optimizerName, OPTIMIZER_METHOD_NAME_DUMP);
31494
- await bt.optimizerGlobalService.dump(symbol, context.optimizerName, path);
31495
- };
31496
- }
31497
- }
31498
- /**
31499
- * Singleton instance of OptimizerUtils.
31500
- * Public API for optimizer operations.
31501
- *
31502
- * @example
31503
- * ```typescript
31504
- * import { Optimizer } from "backtest-kit";
31505
- *
31506
- * await Optimizer.dump("BTCUSDT", { optimizerName: "my-optimizer" });
31507
- * ```
31508
- */
31509
- const Optimizer = new OptimizerUtils();
31510
-
31511
29493
  const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
31512
29494
  const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
31513
29495
  const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
@@ -31786,6 +29768,8 @@ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
31786
29768
  const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
31787
29769
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31788
29770
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
29771
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
29772
+ const MS_PER_MINUTE = 60000;
31789
29773
  /**
31790
29774
  * Gets backtest mode flag from execution context if available.
31791
29775
  * Returns false if no execution context exists (live mode).
@@ -32139,6 +30123,151 @@ class ExchangeInstance {
32139
30123
  const isBacktest = await GET_BACKTEST_FN();
32140
30124
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32141
30125
  };
30126
+ /**
30127
+ * Fetches raw candles with flexible date/limit parameters.
30128
+ *
30129
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
30130
+ *
30131
+ * Parameter combinations:
30132
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
30133
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
30134
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= now
30135
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
30136
+ * 5. Only limit: uses Date.now() as reference (backward)
30137
+ *
30138
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
30139
+ * @param interval - Candle interval (e.g., "1m", "1h")
30140
+ * @param limit - Optional number of candles to fetch
30141
+ * @param sDate - Optional start date in milliseconds
30142
+ * @param eDate - Optional end date in milliseconds
30143
+ * @returns Promise resolving to array of candle data
30144
+ *
30145
+ * @example
30146
+ * ```typescript
30147
+ * const instance = new ExchangeInstance("binance");
30148
+ *
30149
+ * // Fetch 100 candles backward from now
30150
+ * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
30151
+ *
30152
+ * // Fetch candles for specific date range
30153
+ * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
30154
+ * ```
30155
+ */
30156
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
30157
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
30158
+ exchangeName: this.exchangeName,
30159
+ symbol,
30160
+ interval,
30161
+ limit,
30162
+ sDate,
30163
+ eDate,
30164
+ });
30165
+ const step = INTERVAL_MINUTES$1[interval];
30166
+ if (!step) {
30167
+ throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
30168
+ }
30169
+ const nowTimestamp = Date.now();
30170
+ let sinceTimestamp;
30171
+ let untilTimestamp;
30172
+ let calculatedLimit;
30173
+ // Case 1: all three parameters provided
30174
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
30175
+ if (sDate >= eDate) {
30176
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
30177
+ }
30178
+ if (eDate > nowTimestamp) {
30179
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30180
+ }
30181
+ sinceTimestamp = sDate;
30182
+ untilTimestamp = eDate;
30183
+ calculatedLimit = limit;
30184
+ }
30185
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
30186
+ else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
30187
+ if (sDate >= eDate) {
30188
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
30189
+ }
30190
+ if (eDate > nowTimestamp) {
30191
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30192
+ }
30193
+ sinceTimestamp = sDate;
30194
+ untilTimestamp = eDate;
30195
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE));
30196
+ if (calculatedLimit <= 0) {
30197
+ throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
30198
+ }
30199
+ }
30200
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
30201
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
30202
+ if (eDate > nowTimestamp) {
30203
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30204
+ }
30205
+ untilTimestamp = eDate;
30206
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE;
30207
+ calculatedLimit = limit;
30208
+ }
30209
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
30210
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
30211
+ sinceTimestamp = sDate;
30212
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE;
30213
+ if (untilTimestamp > nowTimestamp) {
30214
+ throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
30215
+ }
30216
+ calculatedLimit = limit;
30217
+ }
30218
+ // Case 5: Only limit - use Date.now() as reference (backward)
30219
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
30220
+ untilTimestamp = nowTimestamp;
30221
+ sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
30222
+ calculatedLimit = limit;
30223
+ }
30224
+ // Invalid: no parameters or only sDate or only eDate
30225
+ else {
30226
+ throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
30227
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
30228
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
30229
+ }
30230
+ // Try to read from cache first
30231
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
30232
+ if (cachedCandles !== null) {
30233
+ return cachedCandles;
30234
+ }
30235
+ // Fetch candles
30236
+ const since = new Date(sinceTimestamp);
30237
+ let allData = [];
30238
+ const isBacktest = await GET_BACKTEST_FN();
30239
+ const getCandles = this._methods.getCandles;
30240
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
30241
+ let remaining = calculatedLimit;
30242
+ let currentSince = new Date(since.getTime());
30243
+ while (remaining > 0) {
30244
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
30245
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
30246
+ allData.push(...chunkData);
30247
+ remaining -= chunkLimit;
30248
+ if (remaining > 0) {
30249
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
30250
+ }
30251
+ }
30252
+ }
30253
+ else {
30254
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
30255
+ }
30256
+ // Filter candles to strictly match the requested range
30257
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
30258
+ candle.timestamp < untilTimestamp);
30259
+ // Apply distinct by timestamp to remove duplicates
30260
+ const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
30261
+ if (filteredData.length !== uniqueData.length) {
30262
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
30263
+ }
30264
+ if (uniqueData.length < calculatedLimit) {
30265
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`);
30266
+ }
30267
+ // Write to cache after successful fetch
30268
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
30269
+ return uniqueData;
30270
+ };
32142
30271
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
32143
30272
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
32144
30273
  }
@@ -32243,6 +30372,24 @@ class ExchangeUtils {
32243
30372
  const instance = this._getInstance(context.exchangeName);
32244
30373
  return await instance.getOrderBook(symbol, depth);
32245
30374
  };
30375
+ /**
30376
+ * Fetches raw candles with flexible date/limit parameters.
30377
+ *
30378
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
30379
+ *
30380
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
30381
+ * @param interval - Candle interval (e.g., "1m", "1h")
30382
+ * @param context - Execution context with exchange name
30383
+ * @param limit - Optional number of candles to fetch
30384
+ * @param sDate - Optional start date in milliseconds
30385
+ * @param eDate - Optional end date in milliseconds
30386
+ * @returns Promise resolving to array of candle data
30387
+ */
30388
+ this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
30389
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
30390
+ const instance = this._getInstance(context.exchangeName);
30391
+ return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
30392
+ };
32246
30393
  }
32247
30394
  }
32248
30395
  /**
@@ -33374,4 +31521,4 @@ const set = (object, path, value) => {
33374
31521
  }
33375
31522
  };
33376
31523
 
33377
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Optimizer, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addOptimizerSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitSignalPromptHistory, commitTrailingStop, commitTrailingTake, dumpSignalData, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getOptimizerSchema, getOrderBook, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listOptimizerSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideOptimizerSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
31524
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };