backtest-kit 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +707 -153
  2. package/build/index.cjs +4132 -407
  3. package/build/index.mjs +4116 -401
  4. package/package.json +2 -9
  5. package/types.d.ts +2432 -69
package/build/index.mjs CHANGED
@@ -1,63 +1,215 @@
1
1
  import { createActivator } from 'di-kit';
2
- import { memoize, singlerun, sleep } from 'functools-kit';
3
2
  import { scoped } from 'di-scoped';
4
- import Table from 'cli-table3';
5
-
6
- const NOOP_LOGGER = {
7
- log() {
8
- },
9
- debug() {
10
- },
11
- info() {
12
- },
13
- };
14
- class LoggerService {
15
- constructor() {
16
- this._commonLogger = NOOP_LOGGER;
17
- this.log = async (topic, ...args) => {
18
- await this._commonLogger.log(topic, ...args);
19
- };
20
- this.debug = async (topic, ...args) => {
21
- await this._commonLogger.debug(topic, ...args);
22
- };
23
- this.info = async (topic, ...args) => {
24
- await this._commonLogger.info(topic, ...args);
25
- };
26
- this.setLogger = (logger) => {
27
- this._commonLogger = logger;
28
- };
29
- }
30
- }
3
+ import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, randomString, Subject, ToolRegistry, sleep, str, queued } from 'functools-kit';
4
+ import fs, { mkdir, writeFile } from 'fs/promises';
5
+ import path, { join } from 'path';
6
+ import crypto from 'crypto';
7
+ import os from 'os';
31
8
 
32
9
  const { init, inject, provide } = createActivator("backtest");
33
10
 
11
+ /**
12
+ * Scoped service for method context propagation.
13
+ *
14
+ * Uses di-scoped for implicit context passing without explicit parameters.
15
+ * Context includes strategyName, exchangeName, and frameName.
16
+ *
17
+ * Used by PublicServices to inject schema names into ConnectionServices.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * MethodContextService.runAsyncIterator(
22
+ * backtestGenerator,
23
+ * {
24
+ * strategyName: "my-strategy",
25
+ * exchangeName: "my-exchange",
26
+ * frameName: "1d-backtest"
27
+ * }
28
+ * );
29
+ * ```
30
+ */
31
+ const MethodContextService = scoped(class {
32
+ constructor(context) {
33
+ this.context = context;
34
+ }
35
+ });
36
+
34
37
  const baseServices$1 = {
35
38
  loggerService: Symbol('loggerService'),
36
39
  };
37
40
  const contextServices$1 = {
38
41
  executionContextService: Symbol('executionContextService'),
42
+ methodContextService: Symbol('methodContextService'),
39
43
  };
40
44
  const connectionServices$1 = {
41
45
  exchangeConnectionService: Symbol('exchangeConnectionService'),
42
46
  strategyConnectionService: Symbol('strategyConnectionService'),
47
+ frameConnectionService: Symbol('frameConnectionService'),
43
48
  };
44
49
  const schemaServices$1 = {
45
50
  exchangeSchemaService: Symbol('exchangeSchemaService'),
46
51
  strategySchemaService: Symbol('strategySchemaService'),
52
+ frameSchemaService: Symbol('frameSchemaService'),
53
+ };
54
+ const globalServices$1 = {
55
+ exchangeGlobalService: Symbol('exchangeGlobalService'),
56
+ strategyGlobalService: Symbol('strategyGlobalService'),
57
+ frameGlobalService: Symbol('frameGlobalService'),
58
+ liveGlobalService: Symbol('liveGlobalService'),
59
+ backtestGlobalService: Symbol('backtestGlobalService'),
60
+ };
61
+ const logicPrivateServices$1 = {
62
+ backtestLogicPrivateService: Symbol('backtestLogicPrivateService'),
63
+ liveLogicPrivateService: Symbol('liveLogicPrivateService'),
64
+ };
65
+ const logicPublicServices$1 = {
66
+ backtestLogicPublicService: Symbol('backtestLogicPublicService'),
67
+ liveLogicPublicService: Symbol('liveLogicPublicService'),
68
+ };
69
+ const markdownServices$1 = {
70
+ backtestMarkdownService: Symbol('backtestMarkdownService'),
71
+ liveMarkdownService: Symbol('liveMarkdownService'),
47
72
  };
48
- const publicServices$1 = {
49
- exchangePublicService: Symbol('exchangePublicService'),
50
- strategyPublicService: Symbol('strategyPublicService'),
73
+ const validationServices$1 = {
74
+ exchangeValidationService: Symbol('exchangeValidationService'),
75
+ strategyValidationService: Symbol('strategyValidationService'),
76
+ frameValidationService: Symbol('frameValidationService'),
51
77
  };
52
78
  const TYPES = {
53
79
  ...baseServices$1,
54
80
  ...contextServices$1,
55
81
  ...connectionServices$1,
56
82
  ...schemaServices$1,
57
- ...publicServices$1,
83
+ ...globalServices$1,
84
+ ...logicPrivateServices$1,
85
+ ...logicPublicServices$1,
86
+ ...markdownServices$1,
87
+ ...validationServices$1,
58
88
  };
59
89
 
60
- const INTERVAL_MINUTES = {
90
+ /**
91
+ * Scoped service for execution context propagation.
92
+ *
93
+ * Uses di-scoped for implicit context passing without explicit parameters.
94
+ * Context includes symbol, when (timestamp), and backtest flag.
95
+ *
96
+ * Used by GlobalServices to inject context into operations.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * ExecutionContextService.runInContext(
101
+ * async () => {
102
+ * // Inside this callback, context is automatically available
103
+ * return await someOperation();
104
+ * },
105
+ * { symbol: "BTCUSDT", when: new Date(), backtest: true }
106
+ * );
107
+ * ```
108
+ */
109
+ const ExecutionContextService = scoped(class {
110
+ constructor(context) {
111
+ this.context = context;
112
+ }
113
+ });
114
+
115
+ /**
116
+ * No-op logger implementation used as default.
117
+ * Silently discards all log messages.
118
+ */
119
+ const NOOP_LOGGER = {
120
+ log() {
121
+ },
122
+ debug() {
123
+ },
124
+ info() {
125
+ },
126
+ warn() {
127
+ },
128
+ };
129
+ /**
130
+ * Logger service with automatic context injection.
131
+ *
132
+ * Features:
133
+ * - Delegates to user-provided logger via setLogger()
134
+ * - Automatically appends method context (strategyName, exchangeName, frameName)
135
+ * - Automatically appends execution context (symbol, when, backtest)
136
+ * - Defaults to NOOP_LOGGER if no logger configured
137
+ *
138
+ * Used throughout the framework for consistent logging with context.
139
+ */
140
+ class LoggerService {
141
+ constructor() {
142
+ this.methodContextService = inject(TYPES.methodContextService);
143
+ this.executionContextService = inject(TYPES.executionContextService);
144
+ this._commonLogger = NOOP_LOGGER;
145
+ /**
146
+ * Logs general-purpose message with automatic context injection.
147
+ *
148
+ * @param topic - Log topic/category
149
+ * @param args - Additional log arguments
150
+ */
151
+ this.log = async (topic, ...args) => {
152
+ await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
153
+ };
154
+ /**
155
+ * Logs debug-level message with automatic context injection.
156
+ *
157
+ * @param topic - Log topic/category
158
+ * @param args - Additional log arguments
159
+ */
160
+ this.debug = async (topic, ...args) => {
161
+ await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
162
+ };
163
+ /**
164
+ * Logs info-level message with automatic context injection.
165
+ *
166
+ * @param topic - Log topic/category
167
+ * @param args - Additional log arguments
168
+ */
169
+ this.info = async (topic, ...args) => {
170
+ await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
171
+ };
172
+ /**
173
+ * Logs warning-level message with automatic context injection.
174
+ *
175
+ * @param topic - Log topic/category
176
+ * @param args - Additional log arguments
177
+ */
178
+ this.warn = async (topic, ...args) => {
179
+ await this._commonLogger.warn(topic, ...args, this.methodContext, this.executionContext);
180
+ };
181
+ /**
182
+ * Sets custom logger implementation.
183
+ *
184
+ * @param logger - Custom logger implementing ILogger interface
185
+ */
186
+ this.setLogger = (logger) => {
187
+ this._commonLogger = logger;
188
+ };
189
+ }
190
+ /**
191
+ * Gets current method context if available.
192
+ * Contains strategyName, exchangeName, frameName from MethodContextService.
193
+ */
194
+ get methodContext() {
195
+ if (MethodContextService.hasContext()) {
196
+ return this.methodContextService.context;
197
+ }
198
+ return {};
199
+ }
200
+ /**
201
+ * Gets current execution context if available.
202
+ * Contains symbol, when, backtest from ExecutionContextService.
203
+ */
204
+ get executionContext() {
205
+ if (ExecutionContextService.hasContext()) {
206
+ return this.executionContextService.context;
207
+ }
208
+ return {};
209
+ }
210
+ }
211
+
212
+ const INTERVAL_MINUTES$2 = {
61
213
  "1m": 1,
62
214
  "3m": 3,
63
215
  "5m": 5,
@@ -69,117 +221,334 @@ const INTERVAL_MINUTES = {
69
221
  "6h": 360,
70
222
  "8h": 480,
71
223
  };
224
+ /**
225
+ * Client implementation for exchange data access.
226
+ *
227
+ * Features:
228
+ * - Historical candle fetching (backwards from execution context)
229
+ * - Future candle fetching (forwards for backtest)
230
+ * - VWAP calculation from last 5 1m candles
231
+ * - Price/quantity formatting for exchange
232
+ *
233
+ * All methods use prototype functions for memory efficiency.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const exchange = new ClientExchange({
238
+ * exchangeName: "binance",
239
+ * getCandles: async (symbol, interval, since, limit) => [...],
240
+ * formatPrice: async (symbol, price) => price.toFixed(2),
241
+ * formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
242
+ * execution: executionService,
243
+ * logger: loggerService,
244
+ * });
245
+ *
246
+ * const candles = await exchange.getCandles("BTCUSDT", "1m", 100);
247
+ * const vwap = await exchange.getAveragePrice("BTCUSDT");
248
+ * ```
249
+ */
72
250
  class ClientExchange {
73
251
  constructor(params) {
74
252
  this.params = params;
75
- this.getCandles = async (symbol, interval, limit) => {
76
- this.params.logger.debug(`ClientExchange getCandles`, {
77
- symbol,
78
- interval,
79
- limit,
80
- });
81
- const step = INTERVAL_MINUTES[interval];
82
- const adjust = step * limit;
83
- if (!adjust) {
84
- throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
85
- }
86
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
87
- const data = await this.params.getCandles(symbol, interval, since, limit);
88
- if (this.params.callbacks?.onCandleData) {
89
- this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
90
- }
91
- return data;
92
- };
93
- this.getAveragePrice = async (symbol) => {
94
- this.params.logger.debug(`ClientExchange getAveragePrice`, {
95
- symbol,
96
- });
97
- const candles = await this.getCandles(symbol, "1m", 5);
98
- if (candles.length === 0) {
99
- throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
100
- }
101
- // VWAP (Volume Weighted Average Price)
102
- // Используем типичную цену (typical price) = (high + low + close) / 3
103
- const sumPriceVolume = candles.reduce((acc, candle) => {
104
- const typicalPrice = (candle.high + candle.low + candle.close) / 3;
105
- return acc + typicalPrice * candle.volume;
106
- }, 0);
107
- const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
108
- if (totalVolume === 0) {
109
- // Если объем нулевой, возвращаем простое среднее close цен
110
- const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
111
- return sum / candles.length;
112
- }
113
- const vwap = sumPriceVolume / totalVolume;
114
- return vwap;
115
- };
116
- this.formatQuantity = async (symbol, quantity) => {
117
- this.params.logger.debug("binanceService formatQuantity", {
118
- symbol,
119
- quantity,
120
- });
121
- return await this.params.formatQuantity(symbol, quantity);
122
- };
123
- this.formatPrice = async (symbol, price) => {
124
- this.params.logger.debug("binanceService formatPrice", {
125
- symbol,
126
- price,
127
- });
128
- return await this.params.formatPrice(symbol, price);
129
- };
253
+ }
254
+ /**
255
+ * Fetches historical candles backwards from execution context time.
256
+ *
257
+ * @param symbol - Trading pair symbol
258
+ * @param interval - Candle interval
259
+ * @param limit - Number of candles to fetch
260
+ * @returns Promise resolving to array of candles
261
+ */
262
+ async getCandles(symbol, interval, limit) {
263
+ this.params.logger.debug(`ClientExchange getCandles`, {
264
+ symbol,
265
+ interval,
266
+ limit,
267
+ });
268
+ const step = INTERVAL_MINUTES$2[interval];
269
+ const adjust = step * limit - step;
270
+ if (!adjust) {
271
+ throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
272
+ }
273
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
274
+ const data = await this.params.getCandles(symbol, interval, since, limit);
275
+ // Filter candles to strictly match the requested range
276
+ const whenTimestamp = this.params.execution.context.when.getTime();
277
+ const sinceTimestamp = since.getTime();
278
+ const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= whenTimestamp);
279
+ if (filteredData.length < limit) {
280
+ this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${filteredData.length}`);
281
+ }
282
+ if (this.params.callbacks?.onCandleData) {
283
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
284
+ }
285
+ return filteredData;
286
+ }
287
+ /**
288
+ * Fetches future candles forwards from execution context time.
289
+ * Used in backtest mode to get candles for signal duration.
290
+ *
291
+ * @param symbol - Trading pair symbol
292
+ * @param interval - Candle interval
293
+ * @param limit - Number of candles to fetch
294
+ * @returns Promise resolving to array of candles
295
+ * @throws Error if trying to fetch future candles in live mode
296
+ */
297
+ async getNextCandles(symbol, interval, limit) {
298
+ this.params.logger.debug(`ClientExchange getNextCandles`, {
299
+ symbol,
300
+ interval,
301
+ limit,
302
+ });
303
+ const since = new Date(this.params.execution.context.when.getTime());
304
+ const now = Date.now();
305
+ // Вычисляем конечное время запроса
306
+ const step = INTERVAL_MINUTES$2[interval];
307
+ const endTime = since.getTime() + limit * step * 60 * 1000;
308
+ // Проверяем что запрошенный период не заходит за Date.now()
309
+ if (endTime > now) {
310
+ return [];
311
+ }
312
+ const data = await this.params.getCandles(symbol, interval, since, limit);
313
+ // Filter candles to strictly match the requested range
314
+ const sinceTimestamp = since.getTime();
315
+ const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
316
+ if (filteredData.length < limit) {
317
+ this.params.logger.warn(`ClientExchange getNextCandles: Expected ${limit} candles, got ${filteredData.length}`);
318
+ }
319
+ if (this.params.callbacks?.onCandleData) {
320
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
321
+ }
322
+ return filteredData;
323
+ }
324
+ /**
325
+ * Calculates VWAP (Volume Weighted Average Price) from last 5 1m candles.
326
+ *
327
+ * Formula:
328
+ * - Typical Price = (high + low + close) / 3
329
+ * - VWAP = sum(typical_price * volume) / sum(volume)
330
+ *
331
+ * If volume is zero, returns simple average of close prices.
332
+ *
333
+ * @param symbol - Trading pair symbol
334
+ * @returns Promise resolving to VWAP price
335
+ * @throws Error if no candles available
336
+ */
337
+ async getAveragePrice(symbol) {
338
+ this.params.logger.debug(`ClientExchange getAveragePrice`, {
339
+ symbol,
340
+ });
341
+ const candles = await this.getCandles(symbol, "1m", 5);
342
+ if (candles.length === 0) {
343
+ throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
344
+ }
345
+ // VWAP (Volume Weighted Average Price)
346
+ // Используем типичную цену (typical price) = (high + low + close) / 3
347
+ const sumPriceVolume = candles.reduce((acc, candle) => {
348
+ const typicalPrice = (candle.high + candle.low + candle.close) / 3;
349
+ return acc + typicalPrice * candle.volume;
350
+ }, 0);
351
+ const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
352
+ if (totalVolume === 0) {
353
+ // Если объем нулевой, возвращаем простое среднее close цен
354
+ const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
355
+ return sum / candles.length;
356
+ }
357
+ const vwap = sumPriceVolume / totalVolume;
358
+ return vwap;
359
+ }
360
+ async formatQuantity(symbol, quantity) {
361
+ this.params.logger.debug("binanceService formatQuantity", {
362
+ symbol,
363
+ quantity,
364
+ });
365
+ return await this.params.formatQuantity(symbol, quantity);
366
+ }
367
+ async formatPrice(symbol, price) {
368
+ this.params.logger.debug("binanceService formatPrice", {
369
+ symbol,
370
+ price,
371
+ });
372
+ return await this.params.formatPrice(symbol, price);
130
373
  }
131
374
  }
132
375
 
376
+ /**
377
+ * Connection service routing exchange operations to correct ClientExchange instance.
378
+ *
379
+ * Routes all IExchange method calls to the appropriate exchange implementation
380
+ * based on methodContextService.context.exchangeName. Uses memoization to cache
381
+ * ClientExchange instances for performance.
382
+ *
383
+ * Key features:
384
+ * - Automatic exchange routing via method context
385
+ * - Memoized ClientExchange instances by exchangeName
386
+ * - Implements full IExchange interface
387
+ * - Logging for all operations
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * // Used internally by framework
392
+ * const candles = await exchangeConnectionService.getCandles(
393
+ * "BTCUSDT", "1h", 100
394
+ * );
395
+ * // Automatically routes to correct exchange based on methodContext
396
+ * ```
397
+ */
133
398
  class ExchangeConnectionService {
134
399
  constructor() {
135
400
  this.loggerService = inject(TYPES.loggerService);
136
401
  this.executionContextService = inject(TYPES.executionContextService);
137
402
  this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
138
- this.getExchange = memoize((symbol) => `${symbol}`, () => {
139
- const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.getSchema();
403
+ this.methodContextService = inject(TYPES.methodContextService);
404
+ /**
405
+ * Retrieves memoized ClientExchange instance for given exchange name.
406
+ *
407
+ * Creates ClientExchange on first call, returns cached instance on subsequent calls.
408
+ * Cache key is exchangeName string.
409
+ *
410
+ * @param exchangeName - Name of registered exchange schema
411
+ * @returns Configured ClientExchange instance
412
+ */
413
+ this.getExchange = memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
414
+ const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
140
415
  return new ClientExchange({
141
416
  execution: this.executionContextService,
142
417
  logger: this.loggerService,
418
+ exchangeName,
143
419
  getCandles,
144
420
  formatPrice,
145
421
  formatQuantity,
146
422
  callbacks,
147
423
  });
148
424
  });
425
+ /**
426
+ * Fetches historical candles for symbol using configured exchange.
427
+ *
428
+ * Routes to exchange determined by methodContextService.context.exchangeName.
429
+ *
430
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
431
+ * @param interval - Candle interval (e.g., "1h", "1d")
432
+ * @param limit - Maximum number of candles to fetch
433
+ * @returns Promise resolving to array of candle data
434
+ */
149
435
  this.getCandles = async (symbol, interval, limit) => {
150
436
  this.loggerService.log("exchangeConnectionService getCandles", {
151
437
  symbol,
152
438
  interval,
153
439
  limit,
154
440
  });
155
- return await this.getExchange(symbol).getCandles(symbol, interval, limit);
441
+ return await this.getExchange(this.methodContextService.context.exchangeName).getCandles(symbol, interval, limit);
442
+ };
443
+ /**
444
+ * Fetches next batch of candles relative to executionContext.when.
445
+ *
446
+ * Returns candles that come after the current execution timestamp.
447
+ * Used for backtest progression and live trading updates.
448
+ *
449
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
450
+ * @param interval - Candle interval (e.g., "1h", "1d")
451
+ * @param limit - Maximum number of candles to fetch
452
+ * @returns Promise resolving to array of candle data
453
+ */
454
+ this.getNextCandles = async (symbol, interval, limit) => {
455
+ this.loggerService.log("exchangeConnectionService getNextCandles", {
456
+ symbol,
457
+ interval,
458
+ limit,
459
+ });
460
+ return await this.getExchange(this.methodContextService.context.exchangeName).getNextCandles(symbol, interval, limit);
156
461
  };
462
+ /**
463
+ * Retrieves current average price for symbol.
464
+ *
465
+ * In live mode: fetches real-time average price from exchange API.
466
+ * In backtest mode: calculates VWAP from candles in current timeframe.
467
+ *
468
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
469
+ * @returns Promise resolving to average price
470
+ */
157
471
  this.getAveragePrice = async (symbol) => {
158
472
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
159
473
  symbol,
160
474
  });
161
- return await this.getExchange(symbol).getAveragePrice(symbol);
475
+ return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
162
476
  };
477
+ /**
478
+ * Formats price according to exchange-specific precision rules.
479
+ *
480
+ * Ensures price meets exchange requirements for decimal places and tick size.
481
+ *
482
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
483
+ * @param price - Raw price value to format
484
+ * @returns Promise resolving to formatted price string
485
+ */
163
486
  this.formatPrice = async (symbol, price) => {
164
487
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
165
488
  symbol,
166
489
  price,
167
490
  });
168
- return await this.getExchange(symbol).formatPrice(symbol, price);
491
+ return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
169
492
  };
493
+ /**
494
+ * Formats quantity according to exchange-specific precision rules.
495
+ *
496
+ * Ensures quantity meets exchange requirements for decimal places and lot size.
497
+ *
498
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
499
+ * @param quantity - Raw quantity value to format
500
+ * @returns Promise resolving to formatted quantity string
501
+ */
170
502
  this.formatQuantity = async (symbol, quantity) => {
171
503
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
172
504
  symbol,
173
505
  quantity,
174
506
  });
175
- return await this.getExchange(symbol).formatQuantity(symbol, quantity);
507
+ return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
176
508
  };
177
509
  }
178
510
  }
179
511
 
512
+ /**
513
+ * Slippage percentage applied to entry and exit prices.
514
+ * Simulates market impact and order book depth.
515
+ */
180
516
  const PERCENT_SLIPPAGE = 0.1;
517
+ /**
518
+ * Fee percentage charged per transaction.
519
+ * Applied twice (entry and exit) for total fee calculation.
520
+ */
181
521
  const PERCENT_FEE = 0.1;
182
- const GET_PNL_FN = (signal, priceClose) => {
522
+ /**
523
+ * Calculates profit/loss for a closed signal with slippage and fees.
524
+ *
525
+ * Formula breakdown:
526
+ * 1. Apply slippage to open/close prices (worse execution)
527
+ * - LONG: buy higher (+slippage), sell lower (-slippage)
528
+ * - SHORT: sell lower (-slippage), buy higher (+slippage)
529
+ * 2. Calculate raw PNL percentage
530
+ * - LONG: ((closePrice - openPrice) / openPrice) * 100
531
+ * - SHORT: ((openPrice - closePrice) / openPrice) * 100
532
+ * 3. Subtract total fees (0.1% * 2 = 0.2%)
533
+ *
534
+ * @param signal - Closed signal with position details
535
+ * @param priceClose - Actual close price at exit
536
+ * @returns PNL data with percentage and prices
537
+ *
538
+ * @example
539
+ * ```typescript
540
+ * const pnl = toProfitLossDto(
541
+ * {
542
+ * position: "long",
543
+ * priceOpen: 50000,
544
+ * // ... other signal fields
545
+ * },
546
+ * 51000 // close price
547
+ * );
548
+ * console.log(pnl.pnlPercentage); // e.g., 1.8% (after slippage and fees)
549
+ * ```
550
+ */
551
+ const toProfitLossDto = (signal, priceClose) => {
183
552
  const priceOpen = signal.priceOpen;
184
553
  let priceOpenWithSlippage;
185
554
  let priceCloseWithSlippage;
@@ -218,247 +587,2889 @@ const GET_PNL_FN = (signal, priceClose) => {
218
587
  priceClose,
219
588
  };
220
589
  };
221
- class ClientStrategy {
222
- constructor(params) {
223
- this.params = params;
224
- this._pendingSignal = null;
225
- this.tick = async (symbol) => {
226
- this.params.logger.debug("ClientStrategy tick", {
227
- symbol,
228
- });
229
- if (!this._pendingSignal) {
230
- this._pendingSignal = await this.params.getSignal(this.params.symbol);
231
- if (this._pendingSignal) {
232
- if (this.params.callbacks?.onOpen) {
233
- this.params.callbacks.onOpen(this.params.execution.context.backtest, symbol, this._pendingSignal);
234
- }
235
- return {
236
- action: "opened",
237
- signal: this._pendingSignal,
238
- };
239
- }
240
- return {
241
- action: "idle",
242
- signal: null,
243
- };
590
+
591
+ const IS_WINDOWS = os.platform() === "win32";
592
+ /**
593
+ * Atomically writes data to a file, ensuring the operation either fully completes or leaves the original file unchanged.
594
+ * Uses a temporary file with a rename strategy on POSIX systems for atomicity, or direct writing with sync on Windows (or when POSIX rename is skipped).
595
+ *
596
+ *
597
+ * @param {string} file - The file parameter.
598
+ * @param {string | Buffer} data - The data to be processed or validated.
599
+ * @param {Options | BufferEncoding} options - The options parameter (optional).
600
+ * @throws {Error} Throws an error if the write, sync, or rename operation fails, after attempting cleanup of temporary files.
601
+ *
602
+ * @example
603
+ * // Basic usage with default options
604
+ * await writeFileAtomic("output.txt", "Hello, world!");
605
+ * // Writes "Hello, world!" to "output.txt" atomically
606
+ *
607
+ * @example
608
+ * // Custom options and Buffer data
609
+ * const buffer = Buffer.from("Binary data");
610
+ * await writeFileAtomic("data.bin", buffer, { encoding: "binary", mode: 0o644, tmpPrefix: "temp-" });
611
+ * // Writes binary data to "data.bin" with custom permissions and temp prefix
612
+ *
613
+ * @example
614
+ * // Using encoding shorthand
615
+ * await writeFileAtomic("log.txt", "Log entry", "utf16le");
616
+ * // Writes "Log entry" to "log.txt" in UTF-16LE encoding
617
+ *
618
+ * @remarks
619
+ * This function ensures atomicity to prevent partial writes:
620
+ * - On POSIX systems (non-Windows, unless `GLOBAL_CONFIG.CC_SKIP_POSIX_RENAME` is true):
621
+ * - Writes data to a temporary file (e.g., `.tmp-<random>-filename`) in the same directory.
622
+ * - Uses `crypto.randomBytes` to generate a unique temporary name, reducing collision risk.
623
+ * - Syncs the data to disk and renames the temporary file to the target file atomically with `fs.rename`.
624
+ * - Cleans up the temporary file on failure, swallowing cleanup errors to prioritize throwing the original error.
625
+ * - On Windows (or when POSIX rename is skipped):
626
+ * - Writes directly to the target file, syncing data to disk to minimize corruption risk (though not fully atomic).
627
+ * - Closes the file handle on failure without additional cleanup.
628
+ * - Accepts `options` as an object or a string (interpreted as `encoding`), defaulting to `{ encoding: "utf8", mode: 0o666, tmpPrefix: ".tmp-" }`.
629
+ * Useful in the agent swarm system for safely writing configuration files, logs, or state data where partial writes could cause corruption.
630
+ *
631
+ * @see {@link https://nodejs.org/api/fs.html#fspromiseswritefilefile-data-options|fs.promises.writeFile} for file writing details.
632
+ * @see {@link https://nodejs.org/api/crypto.html#cryptorandombytessize-callback|crypto.randomBytes} for temporary file naming.
633
+ * @see {@link ../config/params|GLOBAL_CONFIG} for configuration impacting POSIX behavior.
634
+ */
635
+ async function writeFileAtomic(file, data, options = {}) {
636
+ if (typeof options === "string") {
637
+ options = { encoding: options };
638
+ }
639
+ else if (!options) {
640
+ options = {};
641
+ }
642
+ const { encoding = "utf8", mode = 0o666, tmpPrefix = ".tmp-" } = options;
643
+ let fileHandle = null;
644
+ if (IS_WINDOWS) {
645
+ try {
646
+ // Create and write to temporary file
647
+ fileHandle = await fs.open(file, "w", mode);
648
+ // Write data to the temp file
649
+ await fileHandle.writeFile(data, { encoding });
650
+ // Ensure data is flushed to disk
651
+ await fileHandle.sync();
652
+ // Close the file before rename
653
+ await fileHandle.close();
654
+ }
655
+ catch (error) {
656
+ // Clean up if something went wrong
657
+ if (fileHandle) {
658
+ await fileHandle.close().catch(() => { });
244
659
  }
245
- const when = this.params.execution.context.when;
246
- const signal = this._pendingSignal;
247
- // Получаем среднюю цену
248
- const averagePrice = await this.params.exchange.getAveragePrice(symbol);
249
- this.params.logger.debug("ClientStrategy tick check", {
250
- symbol,
251
- averagePrice,
252
- signalId: signal.id,
253
- position: signal.position,
254
- });
255
- let shouldClose = false;
256
- let closeReason;
257
- // Проверяем истечение времени
258
- const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
259
- if (when.getTime() >= signalEndTime) {
260
- shouldClose = true;
261
- closeReason = "time_expired";
660
+ throw error; // Re-throw the original error
661
+ }
662
+ return;
663
+ }
664
+ // Create a temporary filename in the same directory
665
+ const dir = path.dirname(file);
666
+ const filename = path.basename(file);
667
+ const tmpFile = path.join(dir, `${tmpPrefix}${crypto.randomBytes(6).toString("hex")}-${filename}`);
668
+ try {
669
+ // Create and write to temporary file
670
+ fileHandle = await fs.open(tmpFile, "w", mode);
671
+ // Write data to the temp file
672
+ await fileHandle.writeFile(data, { encoding });
673
+ // Ensure data is flushed to disk
674
+ await fileHandle.sync();
675
+ // Close the file before rename
676
+ await fileHandle.close();
677
+ fileHandle = null;
678
+ // Atomically replace the target file with our temp file
679
+ await fs.rename(tmpFile, file);
680
+ }
681
+ catch (error) {
682
+ // Clean up if something went wrong
683
+ if (fileHandle) {
684
+ await fileHandle.close().catch(() => { });
685
+ }
686
+ // Try to remove the temporary file
687
+ try {
688
+ await fs.unlink(tmpFile).catch(() => { });
689
+ }
690
+ catch (_) {
691
+ // Ignore errors during cleanup
692
+ }
693
+ throw error;
694
+ }
695
+ }
696
+
697
+ var _a;
698
+ const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
699
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
700
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
701
+ const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
702
+ const PERSIST_BASE_METHOD_NAME_CTOR = "PersistBase.CTOR";
703
+ const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
704
+ const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
705
+ const PERSIST_BASE_METHOD_NAME_WRITE_VALUE = "PersistBase.writeValue";
706
+ const PERSIST_BASE_METHOD_NAME_HAS_VALUE = "PersistBase.hasValue";
707
+ const PERSIST_BASE_METHOD_NAME_REMOVE_VALUE = "PersistBase.removeValue";
708
+ const PERSIST_BASE_METHOD_NAME_REMOVE_ALL = "PersistBase.removeAll";
709
+ const PERSIST_BASE_METHOD_NAME_VALUES = "PersistBase.values";
710
+ const PERSIST_BASE_METHOD_NAME_KEYS = "PersistBase.keys";
711
+ const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
712
+ const BASE_UNLINK_RETRY_COUNT = 5;
713
+ const BASE_UNLINK_RETRY_DELAY = 1000;
714
+ const BASE_WAIT_FOR_INIT_FN = async (self) => {
715
+ backtest$1.loggerService.debug(BASE_WAIT_FOR_INIT_FN_METHOD_NAME, {
716
+ entityName: self.entityName,
717
+ directory: self._directory,
718
+ });
719
+ await fs.mkdir(self._directory, { recursive: true });
720
+ for await (const key of self.keys()) {
721
+ try {
722
+ await self.readValue(key);
723
+ }
724
+ catch {
725
+ const filePath = self._getFilePath(key);
726
+ console.error(`backtest-kit PersistBase found invalid document for filePath=${filePath} entityName=${self.entityName}`);
727
+ if (await not(BASE_WAIT_FOR_INIT_UNLINK_FN(filePath))) {
728
+ console.error(`backtest-kit PersistBase failed to remove invalid document for filePath=${filePath} entityName=${self.entityName}`);
262
729
  }
263
- // Проверяем достижение TP/SL для long позиции
264
- if (signal.position === "long") {
265
- if (averagePrice >= signal.priceTakeProfit) {
266
- shouldClose = true;
267
- closeReason = "take_profit";
268
- }
269
- else if (averagePrice <= signal.priceStopLoss) {
270
- shouldClose = true;
271
- closeReason = "stop_loss";
272
- }
730
+ }
731
+ }
732
+ };
733
+ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => trycatch(retry(async () => {
734
+ try {
735
+ await fs.unlink(filePath);
736
+ return true;
737
+ }
738
+ catch (error) {
739
+ console.error(`backtest-kit PersistBase unlink failed for filePath=${filePath} error=${getErrorMessage(error)}`);
740
+ throw error;
741
+ }
742
+ }, BASE_UNLINK_RETRY_COUNT, BASE_UNLINK_RETRY_DELAY), {
743
+ defaultValue: false,
744
+ });
745
+ /**
746
+ * Base class for file-based persistence with atomic writes.
747
+ *
748
+ * Features:
749
+ * - Atomic file writes using writeFileAtomic
750
+ * - Auto-validation and cleanup of corrupted files
751
+ * - Async generator support for iteration
752
+ * - Retry logic for file deletion
753
+ *
754
+ * @example
755
+ * ```typescript
756
+ * const persist = new PersistBase("my-entity", "./data");
757
+ * await persist.waitForInit(true);
758
+ * await persist.writeValue("key1", { data: "value" });
759
+ * const value = await persist.readValue("key1");
760
+ * ```
761
+ */
762
+ const PersistBase = makeExtendable(class {
763
+ /**
764
+ * Creates new persistence instance.
765
+ *
766
+ * @param entityName - Unique entity type identifier
767
+ * @param baseDir - Base directory for all entities (default: ./logs/data)
768
+ */
769
+ constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
770
+ this.entityName = entityName;
771
+ this.baseDir = baseDir;
772
+ this[_a] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
773
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
774
+ entityName: this.entityName,
775
+ baseDir,
776
+ });
777
+ this._directory = join(this.baseDir, this.entityName);
778
+ }
779
+ /**
780
+ * Computes file path for entity ID.
781
+ *
782
+ * @param entityId - Entity identifier
783
+ * @returns Full file path to entity JSON file
784
+ */
785
+ _getFilePath(entityId) {
786
+ return join(this.baseDir, this.entityName, `${entityId}.json`);
787
+ }
788
+ async waitForInit(initial) {
789
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
790
+ entityName: this.entityName,
791
+ initial,
792
+ });
793
+ await this[BASE_WAIT_FOR_INIT_SYMBOL]();
794
+ }
795
+ /**
796
+ * Returns count of persisted entities.
797
+ *
798
+ * @returns Promise resolving to number of .json files in directory
799
+ */
800
+ async getCount() {
801
+ const files = await fs.readdir(this._directory);
802
+ const { length } = files.filter((file) => file.endsWith(".json"));
803
+ return length;
804
+ }
805
+ async readValue(entityId) {
806
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
807
+ entityName: this.entityName,
808
+ entityId,
809
+ });
810
+ try {
811
+ const filePath = this._getFilePath(entityId);
812
+ const fileContent = await fs.readFile(filePath, "utf-8");
813
+ return JSON.parse(fileContent);
814
+ }
815
+ catch (error) {
816
+ if (error?.code === "ENOENT") {
817
+ throw new Error(`Entity ${this.entityName}:${entityId} not found`);
273
818
  }
274
- // Проверяем достижение TP/SL для short позиции
275
- if (signal.position === "short") {
276
- if (averagePrice <= signal.priceTakeProfit) {
277
- shouldClose = true;
278
- closeReason = "take_profit";
819
+ throw new Error(`Failed to read entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
820
+ }
821
+ }
822
+ async hasValue(entityId) {
823
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
824
+ entityName: this.entityName,
825
+ entityId,
826
+ });
827
+ try {
828
+ const filePath = this._getFilePath(entityId);
829
+ await fs.access(filePath);
830
+ return true;
831
+ }
832
+ catch (error) {
833
+ if (error?.code === "ENOENT") {
834
+ return false;
835
+ }
836
+ throw new Error(`Failed to check existence of entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
837
+ }
838
+ }
839
+ async writeValue(entityId, entity) {
840
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
841
+ entityName: this.entityName,
842
+ entityId,
843
+ });
844
+ try {
845
+ const filePath = this._getFilePath(entityId);
846
+ const serializedData = JSON.stringify(entity);
847
+ await writeFileAtomic(filePath, serializedData, "utf-8");
848
+ }
849
+ catch (error) {
850
+ throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
851
+ }
852
+ }
853
+ /**
854
+ * Removes entity from storage.
855
+ *
856
+ * @param entityId - Entity identifier to remove
857
+ * @returns Promise that resolves when entity is deleted
858
+ * @throws Error if entity not found or deletion fails
859
+ */
860
+ async removeValue(entityId) {
861
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_VALUE, {
862
+ entityName: this.entityName,
863
+ entityId,
864
+ });
865
+ try {
866
+ const filePath = this._getFilePath(entityId);
867
+ await fs.unlink(filePath);
868
+ }
869
+ catch (error) {
870
+ if (error?.code === "ENOENT") {
871
+ throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
872
+ }
873
+ throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${getErrorMessage(error)}`);
874
+ }
875
+ }
876
+ /**
877
+ * Removes all entities from storage.
878
+ *
879
+ * @returns Promise that resolves when all entities are deleted
880
+ * @throws Error if deletion fails
881
+ */
882
+ async removeAll() {
883
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_ALL, {
884
+ entityName: this.entityName,
885
+ });
886
+ try {
887
+ const files = await fs.readdir(this._directory);
888
+ const entityFiles = files.filter((file) => file.endsWith(".json"));
889
+ for (const file of entityFiles) {
890
+ await fs.unlink(join(this._directory, file));
891
+ }
892
+ }
893
+ catch (error) {
894
+ throw new Error(`Failed to remove values for ${this.entityName}: ${getErrorMessage(error)}`);
895
+ }
896
+ }
897
+ /**
898
+ * Async generator yielding all entity values.
899
+ * Sorted alphanumerically by entity ID.
900
+ *
901
+ * @returns AsyncGenerator yielding entities
902
+ * @throws Error if reading fails
903
+ */
904
+ async *values() {
905
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_VALUES, {
906
+ entityName: this.entityName,
907
+ });
908
+ try {
909
+ const files = await fs.readdir(this._directory);
910
+ const entityIds = files
911
+ .filter((file) => file.endsWith(".json"))
912
+ .map((file) => file.slice(0, -5))
913
+ .sort((a, b) => a.localeCompare(b, undefined, {
914
+ numeric: true,
915
+ sensitivity: "base",
916
+ }));
917
+ for (const entityId of entityIds) {
918
+ const entity = await this.readValue(entityId);
919
+ yield entity;
920
+ }
921
+ }
922
+ catch (error) {
923
+ throw new Error(`Failed to read values for ${this.entityName}: ${getErrorMessage(error)}`);
924
+ }
925
+ }
926
+ /**
927
+ * Async generator yielding all entity IDs.
928
+ * Sorted alphanumerically.
929
+ *
930
+ * @returns AsyncGenerator yielding entity IDs
931
+ * @throws Error if reading fails
932
+ */
933
+ async *keys() {
934
+ backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
935
+ entityName: this.entityName,
936
+ });
937
+ try {
938
+ const files = await fs.readdir(this._directory);
939
+ const entityIds = files
940
+ .filter((file) => file.endsWith(".json"))
941
+ .map((file) => file.slice(0, -5))
942
+ .sort((a, b) => a.localeCompare(b, undefined, {
943
+ numeric: true,
944
+ sensitivity: "base",
945
+ }));
946
+ for (const entityId of entityIds) {
947
+ yield entityId;
948
+ }
949
+ }
950
+ catch (error) {
951
+ throw new Error(`Failed to read keys for ${this.entityName}: ${getErrorMessage(error)}`);
952
+ }
953
+ }
954
+ /**
955
+ * Async iterator implementation.
956
+ * Delegates to values() generator.
957
+ *
958
+ * @returns AsyncIterableIterator yielding entities
959
+ */
960
+ async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
961
+ for await (const entity of this.values()) {
962
+ yield entity;
963
+ }
964
+ }
965
+ /**
966
+ * Filters entities by predicate function.
967
+ *
968
+ * @param predicate - Filter function
969
+ * @returns AsyncGenerator yielding filtered entities
970
+ */
971
+ async *filter(predicate) {
972
+ for await (const entity of this.values()) {
973
+ if (predicate(entity)) {
974
+ yield entity;
975
+ }
976
+ }
977
+ }
978
+ /**
979
+ * Takes first N entities, optionally filtered.
980
+ *
981
+ * @param total - Maximum number of entities to yield
982
+ * @param predicate - Optional filter function
983
+ * @returns AsyncGenerator yielding up to total entities
984
+ */
985
+ async *take(total, predicate) {
986
+ let count = 0;
987
+ if (predicate) {
988
+ for await (const entity of this.values()) {
989
+ if (!predicate(entity)) {
990
+ continue;
279
991
  }
280
- else if (averagePrice >= signal.priceStopLoss) {
281
- shouldClose = true;
282
- closeReason = "stop_loss";
992
+ count += 1;
993
+ yield entity;
994
+ if (count >= total) {
995
+ break;
283
996
  }
284
997
  }
285
- // Закрываем сигнал если выполнены условия
286
- if (shouldClose) {
287
- const pnl = GET_PNL_FN(signal, averagePrice);
288
- this.params.logger.debug("ClientStrategy closing", {
289
- symbol,
290
- signalId: signal.id,
291
- reason: closeReason,
292
- priceClose: averagePrice,
293
- pnlPercentage: pnl.pnlPercentage,
294
- });
295
- if (this.params.callbacks?.onClose) {
296
- this.params.callbacks.onClose(this.params.execution.context.backtest, symbol, averagePrice, signal);
998
+ }
999
+ else {
1000
+ for await (const entity of this.values()) {
1001
+ count += 1;
1002
+ yield entity;
1003
+ if (count >= total) {
1004
+ break;
297
1005
  }
298
- this._pendingSignal = null;
299
- return {
300
- action: "closed",
301
- signal: signal,
302
- currentPrice: averagePrice,
303
- closeReason: closeReason,
304
- pnl: pnl,
305
- };
306
1006
  }
307
- return {
308
- action: "active",
309
- signal: signal,
310
- currentPrice: averagePrice,
311
- };
312
- };
1007
+ }
313
1008
  }
314
- }
315
-
316
- class StrategyConnectionService {
1009
+ });
1010
+ /**
1011
+ * Utility class for managing signal persistence.
1012
+ *
1013
+ * Features:
1014
+ * - Memoized storage instances per strategy
1015
+ * - Custom adapter support
1016
+ * - Atomic read/write operations
1017
+ * - Crash-safe signal state management
1018
+ *
1019
+ * Used by ClientStrategy for live mode persistence.
1020
+ */
1021
+ class PersistSignalUtils {
317
1022
  constructor() {
318
- this.loggerService = inject(TYPES.loggerService);
319
- this.executionContextService = inject(TYPES.executionContextService);
320
- this.strategySchemaService = inject(TYPES.strategySchemaService);
321
- this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
322
- this.getStrategy = memoize((symbol) => `${symbol}`, (symbol) => {
323
- const { getSignal, callbacks } = this.strategySchemaService.getSchema();
324
- return new ClientStrategy({
325
- symbol,
326
- execution: this.executionContextService,
327
- logger: this.loggerService,
328
- exchange: this.exchangeConnectionService,
329
- getSignal,
330
- callbacks,
331
- });
332
- });
333
- this.tick = async (symbol) => {
334
- this.loggerService.log("strategyConnectionService tick", {
335
- symbol,
336
- });
337
- return await this.getStrategy(symbol).tick(symbol);
1023
+ this.PersistSignalFactory = PersistBase;
1024
+ this.getSignalStorage = memoize(([strategyName]) => `${strategyName}`, (strategyName) => Reflect.construct(this.PersistSignalFactory, [
1025
+ strategyName,
1026
+ `./logs/data/signal/`,
1027
+ ]));
1028
+ /**
1029
+ * Reads persisted signal data for a strategy and symbol.
1030
+ *
1031
+ * Called by ClientStrategy.waitForInit() to restore state.
1032
+ * Returns null if no signal exists.
1033
+ *
1034
+ * @param strategyName - Strategy identifier
1035
+ * @param symbol - Trading pair symbol
1036
+ * @returns Promise resolving to signal or null
1037
+ */
1038
+ this.readSignalData = async (strategyName, symbol) => {
1039
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1040
+ const isInitial = !this.getSignalStorage.has(strategyName);
1041
+ const stateStorage = this.getSignalStorage(strategyName);
1042
+ await stateStorage.waitForInit(isInitial);
1043
+ if (await stateStorage.hasValue(symbol)) {
1044
+ const { signalRow } = await stateStorage.readValue(symbol);
1045
+ return signalRow;
1046
+ }
1047
+ return null;
338
1048
  };
1049
+ /**
1050
+ * Writes signal data to disk with atomic file writes.
1051
+ *
1052
+ * Called by ClientStrategy.setPendingSignal() to persist state.
1053
+ * Uses atomic writes to prevent corruption on crashes.
1054
+ *
1055
+ * @param signalRow - Signal data (null to clear)
1056
+ * @param strategyName - Strategy identifier
1057
+ * @param symbol - Trading pair symbol
1058
+ * @returns Promise that resolves when write is complete
1059
+ */
1060
+ this.writeSignalData = async (signalRow, strategyName, symbol) => {
1061
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1062
+ const isInitial = !this.getSignalStorage.has(strategyName);
1063
+ const stateStorage = this.getSignalStorage(strategyName);
1064
+ await stateStorage.waitForInit(isInitial);
1065
+ await stateStorage.writeValue(symbol, { signalRow });
1066
+ };
1067
+ }
1068
+ /**
1069
+ * Registers a custom persistence adapter.
1070
+ *
1071
+ * @param Ctor - Custom PersistBase constructor
1072
+ *
1073
+ * @example
1074
+ * ```typescript
1075
+ * class RedisPersist extends PersistBase {
1076
+ * async readValue(id) { return JSON.parse(await redis.get(id)); }
1077
+ * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1078
+ * }
1079
+ * PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
1080
+ * ```
1081
+ */
1082
+ usePersistSignalAdapter(Ctor) {
1083
+ backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1084
+ this.PersistSignalFactory = Ctor;
339
1085
  }
340
1086
  }
1087
+ /**
1088
+ * Global singleton instance of PersistSignalUtils.
1089
+ * Used by ClientStrategy for signal persistence.
1090
+ *
1091
+ * @example
1092
+ * ```typescript
1093
+ * // Custom adapter
1094
+ * PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
1095
+ *
1096
+ * // Read signal
1097
+ * const signal = await PersistSignalAdaper.readSignalData("my-strategy", "BTCUSDT");
1098
+ *
1099
+ * // Write signal
1100
+ * await PersistSignalAdaper.writeSignalData(signal, "my-strategy", "BTCUSDT");
1101
+ * ```
1102
+ */
1103
+ const PersistSignalAdaper = new PersistSignalUtils();
341
1104
 
342
- const ExecutionContextService = scoped(class {
343
- constructor(context) {
344
- this.context = context;
1105
+ const INTERVAL_MINUTES$1 = {
1106
+ "1m": 1,
1107
+ "3m": 3,
1108
+ "5m": 5,
1109
+ "15m": 15,
1110
+ "30m": 30,
1111
+ "1h": 60,
1112
+ };
1113
+ const VALIDATE_SIGNAL_FN = (signal) => {
1114
+ const errors = [];
1115
+ // Валидация цен
1116
+ if (signal.priceOpen <= 0) {
1117
+ errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
1118
+ }
1119
+ if (signal.priceTakeProfit <= 0) {
1120
+ errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
1121
+ }
1122
+ if (signal.priceStopLoss <= 0) {
1123
+ errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
1124
+ }
1125
+ // Валидация для long позиции
1126
+ if (signal.position === "long") {
1127
+ if (signal.priceTakeProfit <= signal.priceOpen) {
1128
+ errors.push(`Long: priceTakeProfit (${signal.priceTakeProfit}) must be > priceOpen (${signal.priceOpen})`);
1129
+ }
1130
+ if (signal.priceStopLoss >= signal.priceOpen) {
1131
+ errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
1132
+ }
1133
+ }
1134
+ // Валидация для short позиции
1135
+ if (signal.position === "short") {
1136
+ if (signal.priceTakeProfit >= signal.priceOpen) {
1137
+ errors.push(`Short: priceTakeProfit (${signal.priceTakeProfit}) must be < priceOpen (${signal.priceOpen})`);
1138
+ }
1139
+ if (signal.priceStopLoss <= signal.priceOpen) {
1140
+ errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
1141
+ }
1142
+ }
1143
+ // Валидация временных параметров
1144
+ if (signal.minuteEstimatedTime <= 0) {
1145
+ errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
1146
+ }
1147
+ if (signal.timestamp <= 0) {
1148
+ errors.push(`timestamp must be positive, got ${signal.timestamp}`);
1149
+ }
1150
+ // Кидаем ошибку если есть проблемы
1151
+ if (errors.length > 0) {
1152
+ throw new Error(`Invalid signal for ${signal.position} position:\n${errors.join("\n")}`);
1153
+ }
1154
+ };
1155
+ const GET_SIGNAL_FN = trycatch(async (self) => {
1156
+ const currentTime = self.params.execution.context.when.getTime();
1157
+ {
1158
+ const intervalMinutes = INTERVAL_MINUTES$1[self.params.interval];
1159
+ const intervalMs = intervalMinutes * 60 * 1000;
1160
+ // Проверяем что прошел нужный интервал с последнего getSignal
1161
+ if (self._lastSignalTimestamp !== null &&
1162
+ currentTime - self._lastSignalTimestamp < intervalMs) {
1163
+ return null;
1164
+ }
1165
+ self._lastSignalTimestamp = currentTime;
1166
+ }
1167
+ const signal = await self.params.getSignal(self.params.execution.context.symbol);
1168
+ if (!signal) {
1169
+ return null;
1170
+ }
1171
+ const signalRow = {
1172
+ id: randomString(),
1173
+ ...signal,
1174
+ symbol: self.params.execution.context.symbol,
1175
+ exchangeName: self.params.method.context.exchangeName,
1176
+ strategyName: self.params.method.context.strategyName,
1177
+ timestamp: currentTime,
1178
+ };
1179
+ // Валидируем сигнал перед возвратом
1180
+ VALIDATE_SIGNAL_FN(signalRow);
1181
+ return signalRow;
1182
+ }, {
1183
+ defaultValue: null,
1184
+ });
1185
+ const GET_AVG_PRICE_FN = (candles) => {
1186
+ const sumPriceVolume = candles.reduce((acc, c) => {
1187
+ const typicalPrice = (c.high + c.low + c.close) / 3;
1188
+ return acc + typicalPrice * c.volume;
1189
+ }, 0);
1190
+ const totalVolume = candles.reduce((acc, c) => acc + c.volume, 0);
1191
+ return totalVolume === 0
1192
+ ? candles.reduce((acc, c) => acc + c.close, 0) / candles.length
1193
+ : sumPriceVolume / totalVolume;
1194
+ };
1195
+ const WAIT_FOR_INIT_FN = async (self) => {
1196
+ self.params.logger.debug("ClientStrategy waitForInit");
1197
+ if (self.params.execution.context.backtest) {
1198
+ return;
1199
+ }
1200
+ const pendingSignal = await PersistSignalAdaper.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
1201
+ if (!pendingSignal) {
1202
+ return;
1203
+ }
1204
+ if (pendingSignal.exchangeName !== self.params.method.context.exchangeName) {
1205
+ return;
1206
+ }
1207
+ if (pendingSignal.strategyName !== self.params.method.context.strategyName) {
1208
+ return;
1209
+ }
1210
+ self._pendingSignal = pendingSignal;
1211
+ };
1212
+ /**
1213
+ * Client implementation for trading strategy lifecycle management.
1214
+ *
1215
+ * Features:
1216
+ * - Signal generation with interval throttling
1217
+ * - Automatic signal validation (prices, TP/SL logic, timestamps)
1218
+ * - Crash-safe persistence in live mode
1219
+ * - VWAP-based TP/SL monitoring
1220
+ * - Fast backtest with candle array processing
1221
+ *
1222
+ * All methods use prototype functions for memory efficiency.
1223
+ *
1224
+ * @example
1225
+ * ```typescript
1226
+ * const strategy = new ClientStrategy({
1227
+ * strategyName: "my-strategy",
1228
+ * interval: "5m",
1229
+ * getSignal: async (symbol) => ({ ... }),
1230
+ * execution: executionService,
1231
+ * exchange: exchangeService,
1232
+ * logger: loggerService,
1233
+ * });
1234
+ *
1235
+ * await strategy.waitForInit(); // Load persisted state
1236
+ * const result = await strategy.tick(); // Monitor signal
1237
+ * ```
1238
+ */
1239
+ class ClientStrategy {
1240
+ constructor(params) {
1241
+ this.params = params;
1242
+ this._pendingSignal = null;
1243
+ this._lastSignalTimestamp = null;
1244
+ /**
1245
+ * Initializes strategy state by loading persisted signal from disk.
1246
+ *
1247
+ * Uses singleshot pattern to ensure initialization happens exactly once.
1248
+ * In backtest mode: skips persistence, no state to load
1249
+ * In live mode: reads last signal state from disk
1250
+ *
1251
+ * @returns Promise that resolves when initialization is complete
1252
+ */
1253
+ this.waitForInit = singleshot(async () => await WAIT_FOR_INIT_FN(this));
1254
+ }
1255
+ /**
1256
+ * Updates pending signal and persists to disk in live mode.
1257
+ *
1258
+ * Centralized method for all signal state changes.
1259
+ * Uses atomic file writes to prevent corruption.
1260
+ *
1261
+ * @param pendingSignal - New signal state (null to clear)
1262
+ * @returns Promise that resolves when update is complete
1263
+ */
1264
+ async setPendingSignal(pendingSignal) {
1265
+ this.params.logger.debug("ClientStrategy setPendingSignal", {
1266
+ pendingSignal,
1267
+ });
1268
+ this._pendingSignal = pendingSignal;
1269
+ if (this.params.execution.context.backtest) {
1270
+ return;
1271
+ }
1272
+ await PersistSignalAdaper.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
1273
+ }
1274
+ /**
1275
+ * Performs a single tick of strategy execution.
1276
+ *
1277
+ * Flow:
1278
+ * 1. If no pending signal: call getSignal with throttling and validation
1279
+ * 2. If signal opened: trigger onOpen callback, persist state
1280
+ * 3. If pending signal exists: check VWAP against TP/SL
1281
+ * 4. If TP/SL/time reached: close signal, trigger onClose, persist state
1282
+ *
1283
+ * @returns Promise resolving to discriminated union result:
1284
+ * - idle: No signal generated
1285
+ * - opened: New signal just created
1286
+ * - active: Signal monitoring in progress
1287
+ * - closed: Signal completed with PNL
1288
+ *
1289
+ * @example
1290
+ * ```typescript
1291
+ * const result = await strategy.tick();
1292
+ * if (result.action === "closed") {
1293
+ * console.log(`PNL: ${result.pnl.pnlPercentage}%`);
1294
+ * }
1295
+ * ```
1296
+ */
1297
+ async tick() {
1298
+ this.params.logger.debug("ClientStrategy tick");
1299
+ if (!this._pendingSignal) {
1300
+ const pendingSignal = await GET_SIGNAL_FN(this);
1301
+ await this.setPendingSignal(pendingSignal);
1302
+ if (this._pendingSignal) {
1303
+ if (this.params.callbacks?.onOpen) {
1304
+ this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
1305
+ }
1306
+ const result = {
1307
+ action: "opened",
1308
+ signal: this._pendingSignal,
1309
+ strategyName: this.params.method.context.strategyName,
1310
+ exchangeName: this.params.method.context.exchangeName,
1311
+ currentPrice: this._pendingSignal.priceOpen,
1312
+ };
1313
+ if (this.params.callbacks?.onTick) {
1314
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1315
+ }
1316
+ return result;
1317
+ }
1318
+ const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
1319
+ if (this.params.callbacks?.onIdle) {
1320
+ this.params.callbacks.onIdle(this.params.execution.context.symbol, currentPrice, this.params.execution.context.backtest);
1321
+ }
1322
+ const result = {
1323
+ action: "idle",
1324
+ signal: null,
1325
+ strategyName: this.params.method.context.strategyName,
1326
+ exchangeName: this.params.method.context.exchangeName,
1327
+ currentPrice,
1328
+ };
1329
+ if (this.params.callbacks?.onTick) {
1330
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1331
+ }
1332
+ return result;
1333
+ }
1334
+ const when = this.params.execution.context.when;
1335
+ const signal = this._pendingSignal;
1336
+ // Получаем среднюю цену
1337
+ const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
1338
+ this.params.logger.debug("ClientStrategy tick check", {
1339
+ symbol: this.params.execution.context.symbol,
1340
+ averagePrice,
1341
+ signalId: signal.id,
1342
+ position: signal.position,
1343
+ });
1344
+ let shouldClose = false;
1345
+ let closeReason;
1346
+ // Проверяем истечение времени
1347
+ const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
1348
+ if (when.getTime() >= signalEndTime) {
1349
+ shouldClose = true;
1350
+ closeReason = "time_expired";
1351
+ }
1352
+ // Проверяем достижение TP/SL для long позиции
1353
+ if (signal.position === "long") {
1354
+ if (averagePrice >= signal.priceTakeProfit) {
1355
+ shouldClose = true;
1356
+ closeReason = "take_profit";
1357
+ }
1358
+ else if (averagePrice <= signal.priceStopLoss) {
1359
+ shouldClose = true;
1360
+ closeReason = "stop_loss";
1361
+ }
1362
+ }
1363
+ // Проверяем достижение TP/SL для short позиции
1364
+ if (signal.position === "short") {
1365
+ if (averagePrice <= signal.priceTakeProfit) {
1366
+ shouldClose = true;
1367
+ closeReason = "take_profit";
1368
+ }
1369
+ else if (averagePrice >= signal.priceStopLoss) {
1370
+ shouldClose = true;
1371
+ closeReason = "stop_loss";
1372
+ }
1373
+ }
1374
+ // Закрываем сигнал если выполнены условия
1375
+ if (shouldClose) {
1376
+ const pnl = toProfitLossDto(signal, averagePrice);
1377
+ const closeTimestamp = this.params.execution.context.when.getTime();
1378
+ // Предупреждение о закрытии сигнала в убыток
1379
+ if (closeReason === "stop_loss") {
1380
+ this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
1381
+ }
1382
+ // Предупреждение о закрытии сигнала в убыток
1383
+ if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
1384
+ this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
1385
+ }
1386
+ this.params.logger.debug("ClientStrategy closing", {
1387
+ symbol: this.params.execution.context.symbol,
1388
+ signalId: signal.id,
1389
+ reason: closeReason,
1390
+ priceClose: averagePrice,
1391
+ closeTimestamp,
1392
+ pnlPercentage: pnl.pnlPercentage,
1393
+ });
1394
+ if (this.params.callbacks?.onClose) {
1395
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
1396
+ }
1397
+ await this.setPendingSignal(null);
1398
+ const result = {
1399
+ action: "closed",
1400
+ signal: signal,
1401
+ currentPrice: averagePrice,
1402
+ closeReason: closeReason,
1403
+ closeTimestamp: closeTimestamp,
1404
+ pnl: pnl,
1405
+ strategyName: this.params.method.context.strategyName,
1406
+ exchangeName: this.params.method.context.exchangeName,
1407
+ };
1408
+ if (this.params.callbacks?.onTick) {
1409
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1410
+ }
1411
+ return result;
1412
+ }
1413
+ if (this.params.callbacks?.onActive) {
1414
+ this.params.callbacks.onActive(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
1415
+ }
1416
+ const result = {
1417
+ action: "active",
1418
+ signal: signal,
1419
+ currentPrice: averagePrice,
1420
+ strategyName: this.params.method.context.strategyName,
1421
+ exchangeName: this.params.method.context.exchangeName,
1422
+ };
1423
+ if (this.params.callbacks?.onTick) {
1424
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1425
+ }
1426
+ return result;
1427
+ }
1428
+ /**
1429
+ * Fast backtests a pending signal using historical candle data.
1430
+ *
1431
+ * Iterates through candles checking VWAP against TP/SL on each timeframe.
1432
+ * Starts from index 4 (needs 5 candles for VWAP calculation).
1433
+ * Always returns closed result (either TP/SL or time_expired).
1434
+ *
1435
+ * @param candles - Array of candles covering signal's minuteEstimatedTime
1436
+ * @returns Promise resolving to closed signal result with PNL
1437
+ * @throws Error if no pending signal or not in backtest mode
1438
+ *
1439
+ * @example
1440
+ * ```typescript
1441
+ * // After signal opened in backtest
1442
+ * const candles = await exchange.getNextCandles("BTCUSDT", "1m", signal.minuteEstimatedTime);
1443
+ * const result = await strategy.backtest(candles);
1444
+ * console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
1445
+ * ```
1446
+ */
1447
+ async backtest(candles) {
1448
+ this.params.logger.debug("ClientStrategy backtest", {
1449
+ symbol: this.params.execution.context.symbol,
1450
+ candlesCount: candles.length,
1451
+ });
1452
+ const signal = this._pendingSignal;
1453
+ if (!signal) {
1454
+ throw new Error("ClientStrategy backtest: no pending signal");
1455
+ }
1456
+ if (!this.params.execution.context.backtest) {
1457
+ throw new Error("ClientStrategy backtest: running in live context");
1458
+ }
1459
+ // Предупреждение если недостаточно свечей для VWAP
1460
+ if (candles.length < 5) {
1461
+ this.params.logger.warn(`ClientStrategy backtest: Expected at least 5 candles for VWAP, got ${candles.length}`);
1462
+ }
1463
+ // Проверяем каждую свечу на достижение TP/SL
1464
+ // Начинаем с индекса 4 (пятая свеча), чтобы было минимум 5 свечей для VWAP
1465
+ for (let i = 4; i < candles.length; i++) {
1466
+ // Вычисляем VWAP из последних 5 свечей для текущего момента
1467
+ const recentCandles = candles.slice(i - 4, i + 1);
1468
+ const averagePrice = GET_AVG_PRICE_FN(recentCandles);
1469
+ let shouldClose = false;
1470
+ let closeReason;
1471
+ // Проверяем достижение TP/SL для long позиции
1472
+ if (signal.position === "long") {
1473
+ if (averagePrice >= signal.priceTakeProfit) {
1474
+ shouldClose = true;
1475
+ closeReason = "take_profit";
1476
+ }
1477
+ else if (averagePrice <= signal.priceStopLoss) {
1478
+ shouldClose = true;
1479
+ closeReason = "stop_loss";
1480
+ }
1481
+ }
1482
+ // Проверяем достижение TP/SL для short позиции
1483
+ if (signal.position === "short") {
1484
+ if (averagePrice <= signal.priceTakeProfit) {
1485
+ shouldClose = true;
1486
+ closeReason = "take_profit";
1487
+ }
1488
+ else if (averagePrice >= signal.priceStopLoss) {
1489
+ shouldClose = true;
1490
+ closeReason = "stop_loss";
1491
+ }
1492
+ }
1493
+ // Если достигнут TP/SL, закрываем сигнал
1494
+ if (shouldClose) {
1495
+ const pnl = toProfitLossDto(signal, averagePrice);
1496
+ const closeTimestamp = recentCandles[recentCandles.length - 1].timestamp;
1497
+ this.params.logger.debug("ClientStrategy backtest closing", {
1498
+ symbol: this.params.execution.context.symbol,
1499
+ signalId: signal.id,
1500
+ reason: closeReason,
1501
+ priceClose: averagePrice,
1502
+ closeTimestamp,
1503
+ pnlPercentage: pnl.pnlPercentage,
1504
+ });
1505
+ // Предупреждение при убытке от stop_loss
1506
+ if (closeReason === "stop_loss") {
1507
+ this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
1508
+ }
1509
+ if (this.params.callbacks?.onClose) {
1510
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
1511
+ }
1512
+ await this.setPendingSignal(null);
1513
+ const result = {
1514
+ action: "closed",
1515
+ signal: signal,
1516
+ currentPrice: averagePrice,
1517
+ closeReason: closeReason,
1518
+ closeTimestamp: closeTimestamp,
1519
+ pnl: pnl,
1520
+ strategyName: this.params.method.context.strategyName,
1521
+ exchangeName: this.params.method.context.exchangeName,
1522
+ };
1523
+ if (this.params.callbacks?.onTick) {
1524
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1525
+ }
1526
+ return result;
1527
+ }
1528
+ }
1529
+ // Если TP/SL не достигнут за период, вычисляем VWAP из последних 5 свечей
1530
+ const lastFiveCandles = candles.slice(-5);
1531
+ const lastPrice = GET_AVG_PRICE_FN(lastFiveCandles);
1532
+ const closeTimestamp = lastFiveCandles[lastFiveCandles.length - 1].timestamp;
1533
+ const pnl = toProfitLossDto(signal, lastPrice);
1534
+ this.params.logger.debug("ClientStrategy backtest time_expired", {
1535
+ symbol: this.params.execution.context.symbol,
1536
+ signalId: signal.id,
1537
+ priceClose: lastPrice,
1538
+ closeTimestamp,
1539
+ pnlPercentage: pnl.pnlPercentage,
1540
+ });
1541
+ // Предупреждение при убытке от time_expired
1542
+ if (pnl.pnlPercentage < 0) {
1543
+ this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
1544
+ }
1545
+ if (this.params.callbacks?.onClose) {
1546
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, lastPrice, this.params.execution.context.backtest);
1547
+ }
1548
+ await this.setPendingSignal(null);
1549
+ const result = {
1550
+ action: "closed",
1551
+ signal: signal,
1552
+ currentPrice: lastPrice,
1553
+ closeReason: "time_expired",
1554
+ closeTimestamp: closeTimestamp,
1555
+ pnl: pnl,
1556
+ strategyName: this.params.method.context.strategyName,
1557
+ exchangeName: this.params.method.context.exchangeName,
1558
+ };
1559
+ if (this.params.callbacks?.onTick) {
1560
+ this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
1561
+ }
1562
+ return result;
1563
+ }
1564
+ }
1565
+
1566
+ /**
1567
+ * Global signal emitter for all trading events (live + backtest).
1568
+ * Emits all signal events regardless of execution mode.
1569
+ */
1570
+ const signalEmitter = new Subject();
1571
+ /**
1572
+ * Live trading signal emitter.
1573
+ * Emits only signals from live trading execution.
1574
+ */
1575
+ const signalLiveEmitter = new Subject();
1576
+ /**
1577
+ * Backtest signal emitter.
1578
+ * Emits only signals from backtest execution.
1579
+ */
1580
+ const signalBacktestEmitter = new Subject();
1581
+ /**
1582
+ * Error emitter for background execution errors.
1583
+ * Emits errors caught in background tasks (Live.background, Backtest.background).
1584
+ */
1585
+ const errorEmitter = new Subject();
1586
+
1587
+ /**
1588
+ * Connection service routing strategy operations to correct ClientStrategy instance.
1589
+ *
1590
+ * Routes all IStrategy method calls to the appropriate strategy implementation
1591
+ * based on methodContextService.context.strategyName. Uses memoization to cache
1592
+ * ClientStrategy instances for performance.
1593
+ *
1594
+ * Key features:
1595
+ * - Automatic strategy routing via method context
1596
+ * - Memoized ClientStrategy instances by strategyName
1597
+ * - Implements IStrategy interface
1598
+ * - Ensures initialization with waitForInit() before operations
1599
+ * - Handles both tick() (live) and backtest() operations
1600
+ *
1601
+ * @example
1602
+ * ```typescript
1603
+ * // Used internally by framework
1604
+ * const result = await strategyConnectionService.tick();
1605
+ * // Automatically routes to correct strategy based on methodContext
1606
+ * ```
1607
+ */
1608
+ class StrategyConnectionService {
1609
+ constructor() {
1610
+ this.loggerService = inject(TYPES.loggerService);
1611
+ this.executionContextService = inject(TYPES.executionContextService);
1612
+ this.strategySchemaService = inject(TYPES.strategySchemaService);
1613
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
1614
+ this.methodContextService = inject(TYPES.methodContextService);
1615
+ /**
1616
+ * Retrieves memoized ClientStrategy instance for given strategy name.
1617
+ *
1618
+ * Creates ClientStrategy on first call, returns cached instance on subsequent calls.
1619
+ * Cache key is strategyName string.
1620
+ *
1621
+ * @param strategyName - Name of registered strategy schema
1622
+ * @returns Configured ClientStrategy instance
1623
+ */
1624
+ this.getStrategy = memoize(([strategyName]) => `${strategyName}`, (strategyName) => {
1625
+ const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
1626
+ return new ClientStrategy({
1627
+ interval,
1628
+ execution: this.executionContextService,
1629
+ method: this.methodContextService,
1630
+ logger: this.loggerService,
1631
+ exchange: this.exchangeConnectionService,
1632
+ strategyName,
1633
+ getSignal,
1634
+ callbacks,
1635
+ });
1636
+ });
1637
+ /**
1638
+ * Executes live trading tick for current strategy.
1639
+ *
1640
+ * Waits for strategy initialization before processing tick.
1641
+ * Evaluates current market conditions and returns signal state.
1642
+ *
1643
+ * @returns Promise resolving to tick result (idle, opened, active, closed)
1644
+ */
1645
+ this.tick = async () => {
1646
+ this.loggerService.log("strategyConnectionService tick");
1647
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
1648
+ await strategy.waitForInit();
1649
+ const tick = await strategy.tick();
1650
+ {
1651
+ if (this.executionContextService.context.backtest) {
1652
+ signalBacktestEmitter.next(tick);
1653
+ }
1654
+ if (!this.executionContextService.context.backtest) {
1655
+ signalLiveEmitter.next(tick);
1656
+ }
1657
+ signalEmitter.next(tick);
1658
+ }
1659
+ return tick;
1660
+ };
1661
+ /**
1662
+ * Executes backtest for current strategy with provided candles.
1663
+ *
1664
+ * Waits for strategy initialization before processing candles.
1665
+ * Evaluates strategy signals against historical data.
1666
+ *
1667
+ * @param candles - Array of historical candle data to backtest
1668
+ * @returns Promise resolving to backtest result (signal or idle)
1669
+ */
1670
+ this.backtest = async (candles) => {
1671
+ this.loggerService.log("strategyConnectionService backtest", {
1672
+ candleCount: candles.length,
1673
+ });
1674
+ const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
1675
+ await strategy.waitForInit();
1676
+ const tick = await strategy.backtest(candles);
1677
+ {
1678
+ if (this.executionContextService.context.backtest) {
1679
+ signalBacktestEmitter.next(tick);
1680
+ }
1681
+ signalEmitter.next(tick);
1682
+ }
1683
+ return tick;
1684
+ };
1685
+ }
1686
+ }
1687
+
1688
+ /**
1689
+ * Maps FrameInterval to minutes for timestamp calculation.
1690
+ * Used to generate timeframe arrays with proper spacing.
1691
+ */
1692
+ const INTERVAL_MINUTES = {
1693
+ "1m": 1,
1694
+ "3m": 3,
1695
+ "5m": 5,
1696
+ "15m": 15,
1697
+ "30m": 30,
1698
+ "1h": 60,
1699
+ "2h": 120,
1700
+ "4h": 240,
1701
+ "6h": 360,
1702
+ "8h": 480,
1703
+ "12h": 720,
1704
+ "1d": 1440,
1705
+ "3d": 4320,
1706
+ };
1707
+ /**
1708
+ * Generates timeframe array from startDate to endDate with specified interval.
1709
+ * Uses prototype function pattern for memory efficiency.
1710
+ *
1711
+ * @param symbol - Trading pair symbol (unused, for API consistency)
1712
+ * @param self - ClientFrame instance reference
1713
+ * @returns Array of Date objects representing tick timestamps
1714
+ * @throws Error if interval is unknown
1715
+ */
1716
+ const GET_TIMEFRAME_FN = async (symbol, self) => {
1717
+ self.params.logger.debug("ClientFrame getTimeframe", {
1718
+ symbol,
1719
+ });
1720
+ const { interval, startDate, endDate } = self.params;
1721
+ const intervalMinutes = INTERVAL_MINUTES[interval];
1722
+ if (!intervalMinutes) {
1723
+ throw new Error(`ClientFrame unknown interval: ${interval}`);
1724
+ }
1725
+ const timeframes = [];
1726
+ let currentDate = new Date(startDate);
1727
+ while (currentDate <= endDate) {
1728
+ timeframes.push(new Date(currentDate));
1729
+ currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
1730
+ }
1731
+ if (self.params.callbacks?.onTimeframe) {
1732
+ self.params.callbacks.onTimeframe(timeframes, startDate, endDate, interval);
1733
+ }
1734
+ return timeframes;
1735
+ };
1736
+ /**
1737
+ * Client implementation for backtest timeframe generation.
1738
+ *
1739
+ * Features:
1740
+ * - Generates timestamp arrays for backtest iteration
1741
+ * - Singleshot caching prevents redundant generation
1742
+ * - Configurable interval spacing (1m to 3d)
1743
+ * - Callback support for validation and logging
1744
+ *
1745
+ * Used by BacktestLogicPrivateService to iterate through historical periods.
1746
+ */
1747
+ class ClientFrame {
1748
+ constructor(params) {
1749
+ this.params = params;
1750
+ /**
1751
+ * Generates timeframe array for backtest period.
1752
+ * Results are cached via singleshot pattern.
1753
+ *
1754
+ * @param symbol - Trading pair symbol (unused, for API consistency)
1755
+ * @returns Promise resolving to array of Date objects
1756
+ * @throws Error if interval is invalid
1757
+ */
1758
+ this.getTimeframe = singleshot(async (symbol) => await GET_TIMEFRAME_FN(symbol, this));
1759
+ }
1760
+ }
1761
+
1762
+ /**
1763
+ * Connection service routing frame operations to correct ClientFrame instance.
1764
+ *
1765
+ * Routes all IFrame method calls to the appropriate frame implementation
1766
+ * based on methodContextService.context.frameName. Uses memoization to cache
1767
+ * ClientFrame instances for performance.
1768
+ *
1769
+ * Key features:
1770
+ * - Automatic frame routing via method context
1771
+ * - Memoized ClientFrame instances by frameName
1772
+ * - Implements IFrame interface
1773
+ * - Backtest timeframe management (startDate, endDate, interval)
1774
+ *
1775
+ * Note: frameName is empty string for live mode (no frame constraints).
1776
+ *
1777
+ * @example
1778
+ * ```typescript
1779
+ * // Used internally by framework
1780
+ * const timeframe = await frameConnectionService.getTimeframe("BTCUSDT");
1781
+ * // Automatically routes to correct frame based on methodContext
1782
+ * ```
1783
+ */
1784
+ class FrameConnectionService {
1785
+ constructor() {
1786
+ this.loggerService = inject(TYPES.loggerService);
1787
+ this.frameSchemaService = inject(TYPES.frameSchemaService);
1788
+ this.methodContextService = inject(TYPES.methodContextService);
1789
+ /**
1790
+ * Retrieves memoized ClientFrame instance for given frame name.
1791
+ *
1792
+ * Creates ClientFrame on first call, returns cached instance on subsequent calls.
1793
+ * Cache key is frameName string.
1794
+ *
1795
+ * @param frameName - Name of registered frame schema
1796
+ * @returns Configured ClientFrame instance
1797
+ */
1798
+ this.getFrame = memoize(([frameName]) => `${frameName}`, (frameName) => {
1799
+ const { endDate, interval, startDate, callbacks } = this.frameSchemaService.get(frameName);
1800
+ return new ClientFrame({
1801
+ frameName,
1802
+ logger: this.loggerService,
1803
+ startDate,
1804
+ endDate,
1805
+ interval,
1806
+ callbacks,
1807
+ });
1808
+ });
1809
+ /**
1810
+ * Retrieves backtest timeframe boundaries for symbol.
1811
+ *
1812
+ * Returns startDate and endDate from frame configuration.
1813
+ * Used to limit backtest execution to specific date range.
1814
+ *
1815
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
1816
+ * @returns Promise resolving to { startDate: Date, endDate: Date }
1817
+ */
1818
+ this.getTimeframe = async (symbol) => {
1819
+ this.loggerService.log("frameConnectionService getTimeframe", {
1820
+ symbol,
1821
+ });
1822
+ return await this.getFrame(this.methodContextService.context.frameName).getTimeframe(symbol);
1823
+ };
1824
+ }
1825
+ }
1826
+
1827
+ /**
1828
+ * Global service for exchange operations with execution context injection.
1829
+ *
1830
+ * Wraps ExchangeConnectionService with ExecutionContextService to inject
1831
+ * symbol, when, and backtest parameters into the execution context.
1832
+ *
1833
+ * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
1834
+ */
1835
+ class ExchangeGlobalService {
1836
+ constructor() {
1837
+ this.loggerService = inject(TYPES.loggerService);
1838
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
1839
+ /**
1840
+ * Fetches historical candles with execution context.
1841
+ *
1842
+ * @param symbol - Trading pair symbol
1843
+ * @param interval - Candle interval (e.g., "1m", "1h")
1844
+ * @param limit - Maximum number of candles to fetch
1845
+ * @param when - Timestamp for context (used in backtest mode)
1846
+ * @param backtest - Whether running in backtest mode
1847
+ * @returns Promise resolving to array of candles
1848
+ */
1849
+ this.getCandles = async (symbol, interval, limit, when, backtest) => {
1850
+ this.loggerService.log("exchangeGlobalService getCandles", {
1851
+ symbol,
1852
+ interval,
1853
+ limit,
1854
+ when,
1855
+ backtest,
1856
+ });
1857
+ return await ExecutionContextService.runInContext(async () => {
1858
+ return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
1859
+ }, {
1860
+ symbol,
1861
+ when,
1862
+ backtest,
1863
+ });
1864
+ };
1865
+ /**
1866
+ * Fetches future candles (backtest mode only) with execution context.
1867
+ *
1868
+ * @param symbol - Trading pair symbol
1869
+ * @param interval - Candle interval
1870
+ * @param limit - Maximum number of candles to fetch
1871
+ * @param when - Timestamp for context
1872
+ * @param backtest - Whether running in backtest mode (must be true)
1873
+ * @returns Promise resolving to array of future candles
1874
+ */
1875
+ this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
1876
+ this.loggerService.log("exchangeGlobalService getNextCandles", {
1877
+ symbol,
1878
+ interval,
1879
+ limit,
1880
+ when,
1881
+ backtest,
1882
+ });
1883
+ return await ExecutionContextService.runInContext(async () => {
1884
+ return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
1885
+ }, {
1886
+ symbol,
1887
+ when,
1888
+ backtest,
1889
+ });
1890
+ };
1891
+ /**
1892
+ * Calculates VWAP with execution context.
1893
+ *
1894
+ * @param symbol - Trading pair symbol
1895
+ * @param when - Timestamp for context
1896
+ * @param backtest - Whether running in backtest mode
1897
+ * @returns Promise resolving to VWAP price
1898
+ */
1899
+ this.getAveragePrice = async (symbol, when, backtest) => {
1900
+ this.loggerService.log("exchangeGlobalService getAveragePrice", {
1901
+ symbol,
1902
+ when,
1903
+ backtest,
1904
+ });
1905
+ return await ExecutionContextService.runInContext(async () => {
1906
+ return await this.exchangeConnectionService.getAveragePrice(symbol);
1907
+ }, {
1908
+ symbol,
1909
+ when,
1910
+ backtest,
1911
+ });
1912
+ };
1913
+ /**
1914
+ * Formats price with execution context.
1915
+ *
1916
+ * @param symbol - Trading pair symbol
1917
+ * @param price - Price to format
1918
+ * @param when - Timestamp for context
1919
+ * @param backtest - Whether running in backtest mode
1920
+ * @returns Promise resolving to formatted price string
1921
+ */
1922
+ this.formatPrice = async (symbol, price, when, backtest) => {
1923
+ this.loggerService.log("exchangeGlobalService formatPrice", {
1924
+ symbol,
1925
+ price,
1926
+ when,
1927
+ backtest,
1928
+ });
1929
+ return await ExecutionContextService.runInContext(async () => {
1930
+ return await this.exchangeConnectionService.formatPrice(symbol, price);
1931
+ }, {
1932
+ symbol,
1933
+ when,
1934
+ backtest,
1935
+ });
1936
+ };
1937
+ /**
1938
+ * Formats quantity with execution context.
1939
+ *
1940
+ * @param symbol - Trading pair symbol
1941
+ * @param quantity - Quantity to format
1942
+ * @param when - Timestamp for context
1943
+ * @param backtest - Whether running in backtest mode
1944
+ * @returns Promise resolving to formatted quantity string
1945
+ */
1946
+ this.formatQuantity = async (symbol, quantity, when, backtest) => {
1947
+ this.loggerService.log("exchangeGlobalService formatQuantity", {
1948
+ symbol,
1949
+ quantity,
1950
+ when,
1951
+ backtest,
1952
+ });
1953
+ return await ExecutionContextService.runInContext(async () => {
1954
+ return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
1955
+ }, {
1956
+ symbol,
1957
+ when,
1958
+ backtest,
1959
+ });
1960
+ };
1961
+ }
1962
+ }
1963
+
1964
+ /**
1965
+ * Global service for strategy operations with execution context injection.
1966
+ *
1967
+ * Wraps StrategyConnectionService with ExecutionContextService to inject
1968
+ * symbol, when, and backtest parameters into the execution context.
1969
+ *
1970
+ * Used internally by BacktestLogicPrivateService and LiveLogicPrivateService.
1971
+ */
1972
+ class StrategyGlobalService {
1973
+ constructor() {
1974
+ this.loggerService = inject(TYPES.loggerService);
1975
+ this.strategyConnectionService = inject(TYPES.strategyConnectionService);
1976
+ /**
1977
+ * Checks signal status at a specific timestamp.
1978
+ *
1979
+ * Wraps strategy tick() with execution context containing symbol, timestamp,
1980
+ * and backtest mode flag.
1981
+ *
1982
+ * @param symbol - Trading pair symbol
1983
+ * @param when - Timestamp for tick evaluation
1984
+ * @param backtest - Whether running in backtest mode
1985
+ * @returns Discriminated union of tick result (idle, opened, active, closed)
1986
+ */
1987
+ this.tick = async (symbol, when, backtest) => {
1988
+ this.loggerService.log("strategyGlobalService tick", {
1989
+ symbol,
1990
+ when,
1991
+ backtest,
1992
+ });
1993
+ return await ExecutionContextService.runInContext(async () => {
1994
+ return await this.strategyConnectionService.tick();
1995
+ }, {
1996
+ symbol,
1997
+ when,
1998
+ backtest,
1999
+ });
2000
+ };
2001
+ /**
2002
+ * Runs fast backtest against candle array.
2003
+ *
2004
+ * Wraps strategy backtest() with execution context containing symbol,
2005
+ * timestamp, and backtest mode flag.
2006
+ *
2007
+ * @param symbol - Trading pair symbol
2008
+ * @param candles - Array of historical candles to test against
2009
+ * @param when - Starting timestamp for backtest
2010
+ * @param backtest - Whether running in backtest mode (typically true)
2011
+ * @returns Closed signal result with PNL
2012
+ */
2013
+ this.backtest = async (symbol, candles, when, backtest) => {
2014
+ this.loggerService.log("strategyGlobalService backtest", {
2015
+ symbol,
2016
+ candleCount: candles.length,
2017
+ when,
2018
+ backtest,
2019
+ });
2020
+ return await ExecutionContextService.runInContext(async () => {
2021
+ return await this.strategyConnectionService.backtest(candles);
2022
+ }, {
2023
+ symbol,
2024
+ when,
2025
+ backtest,
2026
+ });
2027
+ };
2028
+ }
2029
+ }
2030
+
2031
+ /**
2032
+ * Global service for frame operations.
2033
+ *
2034
+ * Wraps FrameConnectionService for timeframe generation.
2035
+ * Used internally by BacktestLogicPrivateService.
2036
+ */
2037
+ class FrameGlobalService {
2038
+ constructor() {
2039
+ this.loggerService = inject(TYPES.loggerService);
2040
+ this.frameConnectionService = inject(TYPES.frameConnectionService);
2041
+ /**
2042
+ * Generates timeframe array for backtest iteration.
2043
+ *
2044
+ * @param symbol - Trading pair symbol
2045
+ * @returns Promise resolving to array of Date objects
2046
+ */
2047
+ this.getTimeframe = async (symbol) => {
2048
+ this.loggerService.log("frameGlobalService getTimeframe", {
2049
+ symbol,
2050
+ });
2051
+ return await this.frameConnectionService.getTimeframe(symbol);
2052
+ };
2053
+ }
2054
+ }
2055
+
2056
+ /**
2057
+ * Service for managing exchange schema registry.
2058
+ *
2059
+ * Uses ToolRegistry from functools-kit for type-safe schema storage.
2060
+ * Exchanges are registered via addExchange() and retrieved by name.
2061
+ */
2062
+ class ExchangeSchemaService {
2063
+ constructor() {
2064
+ this.loggerService = inject(TYPES.loggerService);
2065
+ this._registry = new ToolRegistry("exchangeSchema");
2066
+ /**
2067
+ * Registers a new exchange schema.
2068
+ *
2069
+ * @param key - Unique exchange name
2070
+ * @param value - Exchange schema configuration
2071
+ * @throws Error if exchange name already exists
2072
+ */
2073
+ this.register = (key, value) => {
2074
+ this.loggerService.log(`exchangeSchemaService register`, { key });
2075
+ this.validateShallow(value);
2076
+ this._registry = this._registry.register(key, value);
2077
+ };
2078
+ /**
2079
+ * Validates exchange schema structure for required properties.
2080
+ *
2081
+ * Performs shallow validation to ensure all required properties exist
2082
+ * and have correct types before registration in the registry.
2083
+ *
2084
+ * @param exchangeSchema - Exchange schema to validate
2085
+ * @throws Error if exchangeName is missing or not a string
2086
+ * @throws Error if getCandles is missing or not a function
2087
+ * @throws Error if formatPrice is missing or not a function
2088
+ * @throws Error if formatQuantity is missing or not a function
2089
+ */
2090
+ this.validateShallow = (exchangeSchema) => {
2091
+ this.loggerService.log(`exchangeSchemaService validateShallow`, {
2092
+ exchangeSchema,
2093
+ });
2094
+ if (typeof exchangeSchema.exchangeName !== "string") {
2095
+ throw new Error(`exchange schema validation failed: missing exchangeName`);
2096
+ }
2097
+ if (typeof exchangeSchema.getCandles !== "function") {
2098
+ throw new Error(`exchange schema validation failed: missing getCandles for exchangeName=${exchangeSchema.exchangeName}`);
2099
+ }
2100
+ if (typeof exchangeSchema.formatPrice !== "function") {
2101
+ throw new Error(`exchange schema validation failed: missing formatPrice for exchangeName=${exchangeSchema.exchangeName}`);
2102
+ }
2103
+ if (typeof exchangeSchema.formatQuantity !== "function") {
2104
+ throw new Error(`exchange schema validation failed: missing formatQuantity for exchangeName=${exchangeSchema.exchangeName}`);
2105
+ }
2106
+ };
2107
+ /**
2108
+ * Overrides an existing exchange schema with partial updates.
2109
+ *
2110
+ * @param key - Exchange name to override
2111
+ * @param value - Partial schema updates
2112
+ * @returns Updated exchange schema
2113
+ * @throws Error if exchange name doesn't exist
2114
+ */
2115
+ this.override = (key, value) => {
2116
+ this.loggerService.log(`exchangeSchemaService override`, { key });
2117
+ this._registry = this._registry.override(key, value);
2118
+ return this._registry.get(key);
2119
+ };
2120
+ /**
2121
+ * Retrieves an exchange schema by name.
2122
+ *
2123
+ * @param key - Exchange name
2124
+ * @returns Exchange schema configuration
2125
+ * @throws Error if exchange name doesn't exist
2126
+ */
2127
+ this.get = (key) => {
2128
+ this.loggerService.log(`exchangeSchemaService get`, { key });
2129
+ return this._registry.get(key);
2130
+ };
2131
+ }
2132
+ }
2133
+
2134
+ /**
2135
+ * Service for managing strategy schema registry.
2136
+ *
2137
+ * Uses ToolRegistry from functools-kit for type-safe schema storage.
2138
+ * Strategies are registered via addStrategy() and retrieved by name.
2139
+ */
2140
+ class StrategySchemaService {
2141
+ constructor() {
2142
+ this.loggerService = inject(TYPES.loggerService);
2143
+ this._registry = new ToolRegistry("strategySchema");
2144
+ /**
2145
+ * Registers a new strategy schema.
2146
+ *
2147
+ * @param key - Unique strategy name
2148
+ * @param value - Strategy schema configuration
2149
+ * @throws Error if strategy name already exists
2150
+ */
2151
+ this.register = (key, value) => {
2152
+ this.loggerService.log(`strategySchemaService register`, { key });
2153
+ this.validateShallow(value);
2154
+ this._registry = this._registry.register(key, value);
2155
+ };
2156
+ /**
2157
+ * Validates strategy schema structure for required properties.
2158
+ *
2159
+ * Performs shallow validation to ensure all required properties exist
2160
+ * and have correct types before registration in the registry.
2161
+ *
2162
+ * @param strategySchema - Strategy schema to validate
2163
+ * @throws Error if strategyName is missing or not a string
2164
+ * @throws Error if interval is missing or not a valid SignalInterval
2165
+ * @throws Error if getSignal is missing or not a function
2166
+ */
2167
+ this.validateShallow = (strategySchema) => {
2168
+ this.loggerService.log(`strategySchemaService validateShallow`, {
2169
+ strategySchema,
2170
+ });
2171
+ if (typeof strategySchema.strategyName !== "string") {
2172
+ throw new Error(`strategy schema validation failed: missing strategyName`);
2173
+ }
2174
+ if (typeof strategySchema.interval !== "string") {
2175
+ throw new Error(`strategy schema validation failed: missing interval for strategyName=${strategySchema.strategyName}`);
2176
+ }
2177
+ if (typeof strategySchema.getSignal !== "function") {
2178
+ throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
2179
+ }
2180
+ };
2181
+ /**
2182
+ * Overrides an existing strategy schema with partial updates.
2183
+ *
2184
+ * @param key - Strategy name to override
2185
+ * @param value - Partial schema updates
2186
+ * @returns Updated strategy schema
2187
+ * @throws Error if strategy name doesn't exist
2188
+ */
2189
+ this.override = (key, value) => {
2190
+ this.loggerService.log(`strategySchemaService override`, { key });
2191
+ this._registry = this._registry.override(key, value);
2192
+ return this._registry.get(key);
2193
+ };
2194
+ /**
2195
+ * Retrieves a strategy schema by name.
2196
+ *
2197
+ * @param key - Strategy name
2198
+ * @returns Strategy schema configuration
2199
+ * @throws Error if strategy name doesn't exist
2200
+ */
2201
+ this.get = (key) => {
2202
+ this.loggerService.log(`strategySchemaService get`, { key });
2203
+ return this._registry.get(key);
2204
+ };
2205
+ }
2206
+ }
2207
+
2208
+ /**
2209
+ * Service for managing frame schema registry.
2210
+ *
2211
+ * Uses ToolRegistry from functools-kit for type-safe schema storage.
2212
+ * Frames are registered via addFrame() and retrieved by name.
2213
+ */
2214
+ class FrameSchemaService {
2215
+ constructor() {
2216
+ this.loggerService = inject(TYPES.loggerService);
2217
+ this._registry = new ToolRegistry("frameSchema");
2218
+ /**
2219
+ * Validates frame schema structure for required properties.
2220
+ *
2221
+ * Performs shallow validation to ensure all required properties exist
2222
+ * and have correct types before registration in the registry.
2223
+ *
2224
+ * @param frameSchema - Frame schema to validate
2225
+ * @throws Error if frameName is missing or not a string
2226
+ * @throws Error if interval is missing or not a valid FrameInterval
2227
+ * @throws Error if startDate is missing or not a Date
2228
+ * @throws Error if endDate is missing or not a Date
2229
+ */
2230
+ this.validateShallow = (frameSchema) => {
2231
+ this.loggerService.log(`frameSchemaService validateShallow`, {
2232
+ frameSchema,
2233
+ });
2234
+ if (typeof frameSchema.frameName !== "string") {
2235
+ throw new Error(`frame schema validation failed: missing frameName`);
2236
+ }
2237
+ if (typeof frameSchema.interval !== "string") {
2238
+ throw new Error(`frame schema validation failed: missing interval for frameName=${frameSchema.frameName}`);
2239
+ }
2240
+ if (!(frameSchema.startDate instanceof Date)) {
2241
+ throw new Error(`frame schema validation failed: missing startDate for frameName=${frameSchema.frameName}`);
2242
+ }
2243
+ if (!(frameSchema.endDate instanceof Date)) {
2244
+ throw new Error(`frame schema validation failed: missing endDate for frameName=${frameSchema.frameName}`);
2245
+ }
2246
+ };
2247
+ }
2248
+ /**
2249
+ * Registers a new frame schema.
2250
+ *
2251
+ * @param key - Unique frame name
2252
+ * @param value - Frame schema configuration
2253
+ * @throws Error if frame name already exists
2254
+ */
2255
+ register(key, value) {
2256
+ this.loggerService.log(`frameSchemaService register`, { key });
2257
+ this.validateShallow(value);
2258
+ this._registry.register(key, value);
2259
+ }
2260
+ /**
2261
+ * Overrides an existing frame schema with partial updates.
2262
+ *
2263
+ * @param key - Frame name to override
2264
+ * @param value - Partial schema updates
2265
+ * @throws Error if frame name doesn't exist
2266
+ */
2267
+ override(key, value) {
2268
+ this.loggerService.log(`frameSchemaService override`, { key });
2269
+ this._registry.override(key, value);
2270
+ }
2271
+ /**
2272
+ * Retrieves a frame schema by name.
2273
+ *
2274
+ * @param key - Frame name
2275
+ * @returns Frame schema configuration
2276
+ * @throws Error if frame name doesn't exist
2277
+ */
2278
+ get(key) {
2279
+ this.loggerService.log(`frameSchemaService get`, { key });
2280
+ return this._registry.get(key);
2281
+ }
2282
+ }
2283
+
2284
+ /**
2285
+ * Private service for backtest orchestration using async generators.
2286
+ *
2287
+ * Flow:
2288
+ * 1. Get timeframes from frame service
2289
+ * 2. Iterate through timeframes calling tick()
2290
+ * 3. When signal opens: fetch candles and call backtest()
2291
+ * 4. Skip timeframes until signal closes
2292
+ * 5. Yield closed result and continue
2293
+ *
2294
+ * Memory efficient: streams results without array accumulation.
2295
+ * Supports early termination via break in consumer.
2296
+ */
2297
+ class BacktestLogicPrivateService {
2298
+ constructor() {
2299
+ this.loggerService = inject(TYPES.loggerService);
2300
+ this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2301
+ this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
2302
+ this.frameGlobalService = inject(TYPES.frameGlobalService);
2303
+ }
2304
+ /**
2305
+ * Runs backtest for a symbol, streaming closed signals as async generator.
2306
+ *
2307
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2308
+ * @yields Closed signal results with PNL
2309
+ *
2310
+ * @example
2311
+ * ```typescript
2312
+ * for await (const result of backtestLogic.run("BTCUSDT")) {
2313
+ * console.log(result.closeReason, result.pnl.pnlPercentage);
2314
+ * if (result.pnl.pnlPercentage < -10) break; // Early termination
2315
+ * }
2316
+ * ```
2317
+ */
2318
+ async *run(symbol) {
2319
+ this.loggerService.log("backtestLogicPrivateService run", {
2320
+ symbol,
2321
+ });
2322
+ const timeframes = await this.frameGlobalService.getTimeframe(symbol);
2323
+ let i = 0;
2324
+ while (i < timeframes.length) {
2325
+ const when = timeframes[i];
2326
+ const result = await this.strategyGlobalService.tick(symbol, when, true);
2327
+ // Если сигнал открыт, вызываем backtest
2328
+ if (result.action === "opened") {
2329
+ const signal = result.signal;
2330
+ this.loggerService.info("backtestLogicPrivateService signal opened", {
2331
+ symbol,
2332
+ signalId: signal.id,
2333
+ minuteEstimatedTime: signal.minuteEstimatedTime,
2334
+ });
2335
+ // Получаем свечи для бектеста
2336
+ const candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", signal.minuteEstimatedTime, when, true);
2337
+ if (!candles.length) {
2338
+ return;
2339
+ }
2340
+ this.loggerService.info("backtestLogicPrivateService candles fetched", {
2341
+ symbol,
2342
+ signalId: signal.id,
2343
+ candlesCount: candles.length,
2344
+ });
2345
+ // Вызываем backtest - всегда возвращает closed
2346
+ const backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
2347
+ this.loggerService.info("backtestLogicPrivateService signal closed", {
2348
+ symbol,
2349
+ signalId: backtestResult.signal.id,
2350
+ closeTimestamp: backtestResult.closeTimestamp,
2351
+ closeReason: backtestResult.closeReason,
2352
+ });
2353
+ // Пропускаем timeframes до closeTimestamp
2354
+ while (i < timeframes.length &&
2355
+ timeframes[i].getTime() < backtestResult.closeTimestamp) {
2356
+ i++;
2357
+ }
2358
+ yield backtestResult;
2359
+ }
2360
+ i++;
2361
+ }
2362
+ }
2363
+ }
2364
+
2365
+ const TICK_TTL = 1 * 60 * 1000 + 1;
2366
+ /**
2367
+ * Private service for live trading orchestration using async generators.
2368
+ *
2369
+ * Flow:
2370
+ * 1. Infinite while(true) loop for continuous monitoring
2371
+ * 2. Create real-time date with new Date()
2372
+ * 3. Call tick() to check signal status
2373
+ * 4. Yield opened/closed results (skip idle/active)
2374
+ * 5. Sleep for TICK_TTL between iterations
2375
+ *
2376
+ * Features:
2377
+ * - Crash recovery via ClientStrategy.waitForInit()
2378
+ * - Real-time progression with new Date()
2379
+ * - Memory efficient streaming
2380
+ * - Never completes (infinite generator)
2381
+ */
2382
+ class LiveLogicPrivateService {
2383
+ constructor() {
2384
+ this.loggerService = inject(TYPES.loggerService);
2385
+ this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2386
+ }
2387
+ /**
2388
+ * Runs live trading for a symbol, streaming results as async generator.
2389
+ *
2390
+ * Infinite generator that yields opened and closed signals.
2391
+ * Process can crash and restart - state will be recovered from disk.
2392
+ *
2393
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2394
+ * @yields Opened and closed signal results
2395
+ *
2396
+ * @example
2397
+ * ```typescript
2398
+ * for await (const result of liveLogic.run("BTCUSDT")) {
2399
+ * if (result.action === "opened") {
2400
+ * console.log("New signal:", result.signal.id);
2401
+ * }
2402
+ * if (result.action === "closed") {
2403
+ * console.log("PNL:", result.pnl.pnlPercentage);
2404
+ * }
2405
+ * // Infinite loop - will never complete
2406
+ * }
2407
+ * ```
2408
+ */
2409
+ async *run(symbol) {
2410
+ this.loggerService.log("liveLogicPrivateService run", {
2411
+ symbol,
2412
+ });
2413
+ while (true) {
2414
+ const when = new Date();
2415
+ const result = await this.strategyGlobalService.tick(symbol, when, false);
2416
+ this.loggerService.info("liveLogicPrivateService tick result", {
2417
+ symbol,
2418
+ action: result.action,
2419
+ });
2420
+ if (result.action === "active") {
2421
+ await sleep(TICK_TTL);
2422
+ continue;
2423
+ }
2424
+ if (result.action === "idle") {
2425
+ await sleep(TICK_TTL);
2426
+ continue;
2427
+ }
2428
+ yield result;
2429
+ await sleep(TICK_TTL);
2430
+ }
2431
+ }
2432
+ }
2433
+
2434
+ /**
2435
+ * Public service for backtest orchestration with context management.
2436
+ *
2437
+ * Wraps BacktestLogicPrivateService with MethodContextService to provide
2438
+ * implicit context propagation for strategyName, exchangeName, and frameName.
2439
+ *
2440
+ * This allows getCandles(), getSignal(), and other functions to work without
2441
+ * explicit context parameters.
2442
+ *
2443
+ * @example
2444
+ * ```typescript
2445
+ * const backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
2446
+ *
2447
+ * for await (const result of backtestLogicPublicService.run("BTCUSDT", {
2448
+ * strategyName: "my-strategy",
2449
+ * exchangeName: "my-exchange",
2450
+ * frameName: "1d-backtest",
2451
+ * })) {
2452
+ * if (result.action === "closed") {
2453
+ * console.log("PNL:", result.pnl.profit);
2454
+ * }
2455
+ * }
2456
+ * ```
2457
+ */
2458
+ class BacktestLogicPublicService {
2459
+ constructor() {
2460
+ this.loggerService = inject(TYPES.loggerService);
2461
+ this.backtestLogicPrivateService = inject(TYPES.backtestLogicPrivateService);
2462
+ /**
2463
+ * Runs backtest for a symbol with context propagation.
2464
+ *
2465
+ * Streams closed signals as async generator. Context is automatically
2466
+ * injected into all framework functions called during iteration.
2467
+ *
2468
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2469
+ * @param context - Execution context with strategy, exchange, and frame names
2470
+ * @returns Async generator yielding closed signals with PNL
2471
+ */
2472
+ this.run = (symbol, context) => {
2473
+ this.loggerService.log("backtestLogicPublicService run", {
2474
+ symbol,
2475
+ context,
2476
+ });
2477
+ return MethodContextService.runAsyncIterator(this.backtestLogicPrivateService.run(symbol), {
2478
+ exchangeName: context.exchangeName,
2479
+ strategyName: context.strategyName,
2480
+ frameName: context.frameName,
2481
+ });
2482
+ };
345
2483
  }
346
- });
2484
+ }
347
2485
 
348
- class ExchangePublicService {
2486
+ /**
2487
+ * Public service for live trading orchestration with context management.
2488
+ *
2489
+ * Wraps LiveLogicPrivateService with MethodContextService to provide
2490
+ * implicit context propagation for strategyName and exchangeName.
2491
+ *
2492
+ * This allows getCandles(), getSignal(), and other functions to work without
2493
+ * explicit context parameters.
2494
+ *
2495
+ * Features:
2496
+ * - Infinite async generator (never completes)
2497
+ * - Crash recovery via persisted state
2498
+ * - Real-time progression with Date.now()
2499
+ *
2500
+ * @example
2501
+ * ```typescript
2502
+ * const liveLogicPublicService = inject(TYPES.liveLogicPublicService);
2503
+ *
2504
+ * // Infinite loop - use Ctrl+C to stop
2505
+ * for await (const result of liveLogicPublicService.run("BTCUSDT", {
2506
+ * strategyName: "my-strategy",
2507
+ * exchangeName: "my-exchange",
2508
+ * })) {
2509
+ * if (result.action === "opened") {
2510
+ * console.log("Signal opened:", result.signal);
2511
+ * } else if (result.action === "closed") {
2512
+ * console.log("PNL:", result.pnl.profit);
2513
+ * }
2514
+ * }
2515
+ * ```
2516
+ */
2517
+ class LiveLogicPublicService {
349
2518
  constructor() {
350
2519
  this.loggerService = inject(TYPES.loggerService);
351
- this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
352
- this.getCandles = async (symbol, interval, limit, when, backtest) => {
353
- this.loggerService.log("exchangePublicService getCandles", {
2520
+ this.liveLogicPrivateService = inject(TYPES.liveLogicPrivateService);
2521
+ /**
2522
+ * Runs live trading for a symbol with context propagation.
2523
+ *
2524
+ * Streams opened and closed signals as infinite async generator.
2525
+ * Context is automatically injected into all framework functions.
2526
+ * Process can crash and restart - state will be recovered from disk.
2527
+ *
2528
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2529
+ * @param context - Execution context with strategy and exchange names
2530
+ * @returns Infinite async generator yielding opened and closed signals
2531
+ */
2532
+ this.run = (symbol, context) => {
2533
+ this.loggerService.log("liveLogicPublicService run", {
354
2534
  symbol,
355
- interval,
356
- limit,
357
- when,
358
- backtest,
2535
+ context,
359
2536
  });
360
- return await ExecutionContextService.runInContext(async () => {
361
- return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
362
- }, {
363
- when,
364
- backtest,
2537
+ return MethodContextService.runAsyncIterator(this.liveLogicPrivateService.run(symbol), {
2538
+ exchangeName: context.exchangeName,
2539
+ strategyName: context.strategyName,
2540
+ frameName: "",
365
2541
  });
366
2542
  };
367
- this.getAveragePrice = async (symbol, when, backtest) => {
368
- this.loggerService.log("exchangePublicService getAveragePrice", {
2543
+ }
2544
+ }
2545
+
2546
+ const METHOD_NAME_RUN$1 = "liveGlobalService run";
2547
+ /**
2548
+ * Global service providing access to live trading functionality.
2549
+ *
2550
+ * Simple wrapper around LiveLogicPublicService for dependency injection.
2551
+ * Used by public API exports.
2552
+ */
2553
+ class LiveGlobalService {
2554
+ constructor() {
2555
+ this.loggerService = inject(TYPES.loggerService);
2556
+ this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
2557
+ this.strategyValidationService = inject(TYPES.strategyValidationService);
2558
+ this.exchangeValidationService = inject(TYPES.exchangeValidationService);
2559
+ /**
2560
+ * Runs live trading for a symbol with context propagation.
2561
+ *
2562
+ * Infinite async generator with crash recovery support.
2563
+ *
2564
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2565
+ * @param context - Execution context with strategy and exchange names
2566
+ * @returns Infinite async generator yielding opened and closed signals
2567
+ */
2568
+ this.run = (symbol, context) => {
2569
+ this.loggerService.log(METHOD_NAME_RUN$1, {
369
2570
  symbol,
370
- when,
371
- backtest,
372
- });
373
- return await ExecutionContextService.runInContext(async () => {
374
- return await this.exchangeConnectionService.getAveragePrice(symbol);
375
- }, {
376
- when,
377
- backtest,
2571
+ context,
378
2572
  });
2573
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
2574
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
2575
+ return this.liveLogicPublicService.run(symbol, context);
379
2576
  };
380
- this.formatPrice = async (symbol, price, when, backtest) => {
381
- this.loggerService.log("exchangePublicService formatPrice", {
2577
+ }
2578
+ }
2579
+
2580
+ const METHOD_NAME_RUN = "backtestGlobalService run";
2581
+ /**
2582
+ * Global service providing access to backtest functionality.
2583
+ *
2584
+ * Simple wrapper around BacktestLogicPublicService for dependency injection.
2585
+ * Used by public API exports.
2586
+ */
2587
+ class BacktestGlobalService {
2588
+ constructor() {
2589
+ this.loggerService = inject(TYPES.loggerService);
2590
+ this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
2591
+ this.strategyValidationService = inject(TYPES.strategyValidationService);
2592
+ this.exchangeValidationService = inject(TYPES.exchangeValidationService);
2593
+ this.frameValidationService = inject(TYPES.frameValidationService);
2594
+ /**
2595
+ * Runs backtest for a symbol with context propagation.
2596
+ *
2597
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2598
+ * @param context - Execution context with strategy, exchange, and frame names
2599
+ * @returns Async generator yielding closed signals with PNL
2600
+ */
2601
+ this.run = (symbol, context) => {
2602
+ this.loggerService.log(METHOD_NAME_RUN, {
382
2603
  symbol,
383
- price,
384
- when,
385
- backtest,
2604
+ context,
386
2605
  });
387
- return await ExecutionContextService.runInContext(async () => {
388
- return await this.exchangeConnectionService.formatPrice(symbol, price);
389
- }, {
390
- when,
391
- backtest,
2606
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN);
2607
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
2608
+ this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
2609
+ return this.backtestLogicPublicService.run(symbol, context);
2610
+ };
2611
+ }
2612
+ }
2613
+
2614
+ const columns$1 = [
2615
+ {
2616
+ key: "signalId",
2617
+ label: "Signal ID",
2618
+ format: (data) => data.signal.id,
2619
+ },
2620
+ {
2621
+ key: "symbol",
2622
+ label: "Symbol",
2623
+ format: (data) => data.signal.symbol,
2624
+ },
2625
+ {
2626
+ key: "position",
2627
+ label: "Position",
2628
+ format: (data) => data.signal.position.toUpperCase(),
2629
+ },
2630
+ {
2631
+ key: "note",
2632
+ label: "Note",
2633
+ format: (data) => data.signal.note ?? "N/A",
2634
+ },
2635
+ {
2636
+ key: "openPrice",
2637
+ label: "Open Price",
2638
+ format: (data) => `${data.signal.priceOpen.toFixed(8)} USD`,
2639
+ },
2640
+ {
2641
+ key: "closePrice",
2642
+ label: "Close Price",
2643
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
2644
+ },
2645
+ {
2646
+ key: "takeProfit",
2647
+ label: "Take Profit",
2648
+ format: (data) => `${data.signal.priceTakeProfit.toFixed(8)} USD`,
2649
+ },
2650
+ {
2651
+ key: "stopLoss",
2652
+ label: "Stop Loss",
2653
+ format: (data) => `${data.signal.priceStopLoss.toFixed(8)} USD`,
2654
+ },
2655
+ {
2656
+ key: "pnl",
2657
+ label: "PNL (net)",
2658
+ format: (data) => {
2659
+ const pnlPercentage = data.pnl.pnlPercentage;
2660
+ return `${pnlPercentage > 0 ? "+" : ""}${pnlPercentage.toFixed(2)}%`;
2661
+ },
2662
+ },
2663
+ {
2664
+ key: "closeReason",
2665
+ label: "Close Reason",
2666
+ format: (data) => data.closeReason,
2667
+ },
2668
+ {
2669
+ key: "duration",
2670
+ label: "Duration (min)",
2671
+ format: (data) => {
2672
+ const durationMs = data.closeTimestamp - data.signal.timestamp;
2673
+ const durationMin = Math.round(durationMs / 60000);
2674
+ return `${durationMin}`;
2675
+ },
2676
+ },
2677
+ {
2678
+ key: "openTimestamp",
2679
+ label: "Open Time",
2680
+ format: (data) => new Date(data.signal.timestamp).toISOString(),
2681
+ },
2682
+ {
2683
+ key: "closeTimestamp",
2684
+ label: "Close Time",
2685
+ format: (data) => new Date(data.closeTimestamp).toISOString(),
2686
+ },
2687
+ ];
2688
+ /**
2689
+ * Storage class for accumulating closed signals per strategy.
2690
+ * Maintains a list of all closed signals and provides methods to generate reports.
2691
+ */
2692
+ let ReportStorage$1 = class ReportStorage {
2693
+ constructor() {
2694
+ /** Internal list of all closed signals for this strategy */
2695
+ this._signalList = [];
2696
+ }
2697
+ /**
2698
+ * Adds a closed signal to the storage.
2699
+ *
2700
+ * @param data - Closed signal data with PNL and close reason
2701
+ */
2702
+ addSignal(data) {
2703
+ this._signalList.push(data);
2704
+ }
2705
+ /**
2706
+ * Generates markdown report with all closed signals for a strategy.
2707
+ *
2708
+ * @param strategyName - Strategy name
2709
+ * @returns Markdown formatted report with all signals
2710
+ */
2711
+ getReport(strategyName) {
2712
+ if (this._signalList.length === 0) {
2713
+ return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2714
+ }
2715
+ const header = columns$1.map((col) => col.label);
2716
+ const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2717
+ const tableData = [header, ...rows];
2718
+ const table = str.table(tableData);
2719
+ return str.newline(`# Backtest Report: ${strategyName}`, "", `Total signals: ${this._signalList.length}`, "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
2720
+ }
2721
+ /**
2722
+ * Saves strategy report to disk.
2723
+ *
2724
+ * @param strategyName - Strategy name
2725
+ * @param path - Directory path to save report (default: "./logs/backtest")
2726
+ */
2727
+ async dump(strategyName, path = "./logs/backtest") {
2728
+ const markdown = this.getReport(strategyName);
2729
+ try {
2730
+ const dir = join(process.cwd(), path);
2731
+ await mkdir(dir, { recursive: true });
2732
+ const filename = `${strategyName}.md`;
2733
+ const filepath = join(dir, filename);
2734
+ await writeFile(filepath, markdown, "utf-8");
2735
+ console.log(`Backtest report saved: ${filepath}`);
2736
+ }
2737
+ catch (error) {
2738
+ console.error(`Failed to save markdown report:`, error);
2739
+ }
2740
+ }
2741
+ };
2742
+ /**
2743
+ * Service for generating and saving backtest markdown reports.
2744
+ *
2745
+ * Features:
2746
+ * - Listens to signal events via onTick callback
2747
+ * - Accumulates closed signals per strategy using memoized storage
2748
+ * - Generates markdown tables with detailed signal information
2749
+ * - Saves reports to disk in logs/backtest/{strategyName}.md
2750
+ *
2751
+ * @example
2752
+ * ```typescript
2753
+ * const service = new BacktestMarkdownService();
2754
+ *
2755
+ * // Add to strategy callbacks
2756
+ * addStrategy({
2757
+ * strategyName: "my-strategy",
2758
+ * callbacks: {
2759
+ * onTick: (symbol, result, backtest) => {
2760
+ * service.tick(result);
2761
+ * }
2762
+ * }
2763
+ * });
2764
+ *
2765
+ * // After backtest, generate and save report
2766
+ * await service.saveReport("my-strategy");
2767
+ * ```
2768
+ */
2769
+ class BacktestMarkdownService {
2770
+ constructor() {
2771
+ /** Logger service for debug output */
2772
+ this.loggerService = inject(TYPES.loggerService);
2773
+ /**
2774
+ * Memoized function to get or create ReportStorage for a strategy.
2775
+ * Each strategy gets its own isolated storage instance.
2776
+ */
2777
+ this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
2778
+ /**
2779
+ * Processes tick events and accumulates closed signals.
2780
+ * Should be called from IStrategyCallbacks.onTick.
2781
+ *
2782
+ * Only processes closed signals - opened signals are ignored.
2783
+ *
2784
+ * @param data - Tick result from strategy execution (opened or closed)
2785
+ *
2786
+ * @example
2787
+ * ```typescript
2788
+ * const service = new BacktestMarkdownService();
2789
+ *
2790
+ * callbacks: {
2791
+ * onTick: (symbol, result, backtest) => {
2792
+ * service.tick(result);
2793
+ * }
2794
+ * }
2795
+ * ```
2796
+ */
2797
+ this.tick = async (data) => {
2798
+ this.loggerService.log("backtestMarkdownService tick", {
2799
+ data,
392
2800
  });
2801
+ if (data.action !== "closed") {
2802
+ return;
2803
+ }
2804
+ const storage = this.getStorage(data.strategyName);
2805
+ storage.addSignal(data);
393
2806
  };
394
- this.formatQuantity = async (symbol, quantity, when, backtest) => {
395
- this.loggerService.log("exchangePublicService formatQuantity", {
396
- symbol,
397
- quantity,
398
- when,
399
- backtest,
2807
+ /**
2808
+ * Generates markdown report with all closed signals for a strategy.
2809
+ * Delegates to ReportStorage.generateReport().
2810
+ *
2811
+ * @param strategyName - Strategy name to generate report for
2812
+ * @returns Markdown formatted report string with table of all closed signals
2813
+ *
2814
+ * @example
2815
+ * ```typescript
2816
+ * const service = new BacktestMarkdownService();
2817
+ * const markdown = service.generateReport("my-strategy");
2818
+ * console.log(markdown);
2819
+ * ```
2820
+ */
2821
+ this.getReport = async (strategyName) => {
2822
+ this.loggerService.log("backtestMarkdownService getReport", {
2823
+ strategyName,
400
2824
  });
401
- return await ExecutionContextService.runInContext(async () => {
402
- return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
403
- }, {
404
- when,
405
- backtest,
2825
+ const storage = this.getStorage(strategyName);
2826
+ return storage.getReport(strategyName);
2827
+ };
2828
+ /**
2829
+ * Saves strategy report to disk.
2830
+ * Creates directory if it doesn't exist.
2831
+ * Delegates to ReportStorage.dump().
2832
+ *
2833
+ * @param strategyName - Strategy name to save report for
2834
+ * @param path - Directory path to save report (default: "./logs/backtest")
2835
+ *
2836
+ * @example
2837
+ * ```typescript
2838
+ * const service = new BacktestMarkdownService();
2839
+ *
2840
+ * // Save to default path: ./logs/backtest/my-strategy.md
2841
+ * await service.dump("my-strategy");
2842
+ *
2843
+ * // Save to custom path: ./custom/path/my-strategy.md
2844
+ * await service.dump("my-strategy", "./custom/path");
2845
+ * ```
2846
+ */
2847
+ this.dump = async (strategyName, path = "./logs/backtest") => {
2848
+ this.loggerService.log("backtestMarkdownService dump", {
2849
+ strategyName,
2850
+ path,
2851
+ });
2852
+ const storage = this.getStorage(strategyName);
2853
+ await storage.dump(strategyName, path);
2854
+ };
2855
+ /**
2856
+ * Clears accumulated signal data from storage.
2857
+ * If strategyName is provided, clears only that strategy's data.
2858
+ * If strategyName is omitted, clears all strategies' data.
2859
+ *
2860
+ * @param strategyName - Optional strategy name to clear specific strategy data
2861
+ *
2862
+ * @example
2863
+ * ```typescript
2864
+ * const service = new BacktestMarkdownService();
2865
+ *
2866
+ * // Clear specific strategy data
2867
+ * await service.clear("my-strategy");
2868
+ *
2869
+ * // Clear all strategies' data
2870
+ * await service.clear();
2871
+ * ```
2872
+ */
2873
+ this.clear = async (strategyName) => {
2874
+ this.loggerService.log("backtestMarkdownService clear", {
2875
+ strategyName,
406
2876
  });
2877
+ this.getStorage.clear(strategyName);
407
2878
  };
2879
+ /**
2880
+ * Initializes the service by subscribing to backtest signal events.
2881
+ * Uses singleshot to ensure initialization happens only once.
2882
+ * Automatically called on first use.
2883
+ *
2884
+ * @example
2885
+ * ```typescript
2886
+ * const service = new BacktestMarkdownService();
2887
+ * await service.init(); // Subscribe to backtest events
2888
+ * ```
2889
+ */
2890
+ this.init = singleshot(async () => {
2891
+ this.loggerService.log("backtestMarkdownService init");
2892
+ signalBacktestEmitter.subscribe(this.tick);
2893
+ });
408
2894
  }
409
2895
  }
410
2896
 
411
- class StrategyPublicService {
2897
+ const columns = [
2898
+ {
2899
+ key: "timestamp",
2900
+ label: "Timestamp",
2901
+ format: (data) => new Date(data.timestamp).toISOString(),
2902
+ },
2903
+ {
2904
+ key: "action",
2905
+ label: "Action",
2906
+ format: (data) => data.action.toUpperCase(),
2907
+ },
2908
+ {
2909
+ key: "symbol",
2910
+ label: "Symbol",
2911
+ format: (data) => data.symbol ?? "N/A",
2912
+ },
2913
+ {
2914
+ key: "signalId",
2915
+ label: "Signal ID",
2916
+ format: (data) => data.signalId ?? "N/A",
2917
+ },
2918
+ {
2919
+ key: "position",
2920
+ label: "Position",
2921
+ format: (data) => data.position?.toUpperCase() ?? "N/A",
2922
+ },
2923
+ {
2924
+ key: "note",
2925
+ label: "Note",
2926
+ format: (data) => data.note ?? "N/A",
2927
+ },
2928
+ {
2929
+ key: "currentPrice",
2930
+ label: "Current Price",
2931
+ format: (data) => `${data.currentPrice.toFixed(8)} USD`,
2932
+ },
2933
+ {
2934
+ key: "openPrice",
2935
+ label: "Open Price",
2936
+ format: (data) => data.openPrice !== undefined ? `${data.openPrice.toFixed(8)} USD` : "N/A",
2937
+ },
2938
+ {
2939
+ key: "takeProfit",
2940
+ label: "Take Profit",
2941
+ format: (data) => data.takeProfit !== undefined
2942
+ ? `${data.takeProfit.toFixed(8)} USD`
2943
+ : "N/A",
2944
+ },
2945
+ {
2946
+ key: "stopLoss",
2947
+ label: "Stop Loss",
2948
+ format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
2949
+ },
2950
+ {
2951
+ key: "pnl",
2952
+ label: "PNL (net)",
2953
+ format: (data) => {
2954
+ if (data.pnl === undefined)
2955
+ return "N/A";
2956
+ return `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`;
2957
+ },
2958
+ },
2959
+ {
2960
+ key: "closeReason",
2961
+ label: "Close Reason",
2962
+ format: (data) => data.closeReason ?? "N/A",
2963
+ },
2964
+ {
2965
+ key: "duration",
2966
+ label: "Duration (min)",
2967
+ format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
2968
+ },
2969
+ ];
2970
+ /** Maximum number of events to store in live trading reports */
2971
+ const MAX_EVENTS = 250;
2972
+ /**
2973
+ * Storage class for accumulating all tick events per strategy.
2974
+ * Maintains a chronological list of all events (idle, opened, active, closed).
2975
+ */
2976
+ class ReportStorage {
2977
+ constructor() {
2978
+ /** Internal list of all tick events for this strategy */
2979
+ this._eventList = [];
2980
+ }
2981
+ /**
2982
+ * Adds an idle event to the storage.
2983
+ * Replaces the last idle event only if there are no opened/active events after it.
2984
+ *
2985
+ * @param currentPrice - Current market price
2986
+ */
2987
+ addIdleEvent(currentPrice) {
2988
+ const newEvent = {
2989
+ timestamp: Date.now(),
2990
+ action: "idle",
2991
+ currentPrice,
2992
+ };
2993
+ const lastIdleIndex = this._eventList.findLastIndex((event) => event.action === "idle");
2994
+ const canReplaceLastIdle = lastIdleIndex !== -1 &&
2995
+ !this._eventList
2996
+ .slice(lastIdleIndex + 1)
2997
+ .some((event) => event.action === "opened" || event.action === "active");
2998
+ if (canReplaceLastIdle) {
2999
+ this._eventList[lastIdleIndex] = newEvent;
3000
+ return;
3001
+ }
3002
+ {
3003
+ this._eventList.push(newEvent);
3004
+ if (this._eventList.length > MAX_EVENTS) {
3005
+ this._eventList.shift();
3006
+ }
3007
+ }
3008
+ }
3009
+ /**
3010
+ * Adds an opened event to the storage.
3011
+ *
3012
+ * @param data - Opened tick result
3013
+ */
3014
+ addOpenedEvent(data) {
3015
+ this._eventList.push({
3016
+ timestamp: data.signal.timestamp,
3017
+ action: "opened",
3018
+ symbol: data.signal.symbol,
3019
+ signalId: data.signal.id,
3020
+ position: data.signal.position,
3021
+ note: data.signal.note,
3022
+ currentPrice: data.signal.priceOpen,
3023
+ openPrice: data.signal.priceOpen,
3024
+ takeProfit: data.signal.priceTakeProfit,
3025
+ stopLoss: data.signal.priceStopLoss,
3026
+ });
3027
+ // Trim queue if exceeded MAX_EVENTS
3028
+ if (this._eventList.length > MAX_EVENTS) {
3029
+ this._eventList.shift();
3030
+ }
3031
+ }
3032
+ /**
3033
+ * Updates or adds an active event to the storage.
3034
+ * Replaces the previous event with the same signalId.
3035
+ *
3036
+ * @param data - Active tick result
3037
+ */
3038
+ addActiveEvent(data) {
3039
+ // Find existing event with the same signalId
3040
+ const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
3041
+ const newEvent = {
3042
+ timestamp: Date.now(),
3043
+ action: "active",
3044
+ symbol: data.signal.symbol,
3045
+ signalId: data.signal.id,
3046
+ position: data.signal.position,
3047
+ note: data.signal.note,
3048
+ currentPrice: data.currentPrice,
3049
+ openPrice: data.signal.priceOpen,
3050
+ takeProfit: data.signal.priceTakeProfit,
3051
+ stopLoss: data.signal.priceStopLoss,
3052
+ };
3053
+ // Replace existing event or add new one
3054
+ if (existingIndex !== -1) {
3055
+ this._eventList[existingIndex] = newEvent;
3056
+ }
3057
+ else {
3058
+ this._eventList.push(newEvent);
3059
+ // Trim queue if exceeded MAX_EVENTS
3060
+ if (this._eventList.length > MAX_EVENTS) {
3061
+ this._eventList.shift();
3062
+ }
3063
+ }
3064
+ }
3065
+ /**
3066
+ * Updates or adds a closed event to the storage.
3067
+ * Replaces the previous event with the same signalId.
3068
+ *
3069
+ * @param data - Closed tick result
3070
+ */
3071
+ addClosedEvent(data) {
3072
+ const durationMs = data.closeTimestamp - data.signal.timestamp;
3073
+ const durationMin = Math.round(durationMs / 60000);
3074
+ // Find existing event with the same signalId
3075
+ const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
3076
+ const newEvent = {
3077
+ timestamp: data.closeTimestamp,
3078
+ action: "closed",
3079
+ symbol: data.signal.symbol,
3080
+ signalId: data.signal.id,
3081
+ position: data.signal.position,
3082
+ note: data.signal.note,
3083
+ currentPrice: data.currentPrice,
3084
+ openPrice: data.signal.priceOpen,
3085
+ takeProfit: data.signal.priceTakeProfit,
3086
+ stopLoss: data.signal.priceStopLoss,
3087
+ pnl: data.pnl.pnlPercentage,
3088
+ closeReason: data.closeReason,
3089
+ duration: durationMin,
3090
+ };
3091
+ // Replace existing event or add new one
3092
+ if (existingIndex !== -1) {
3093
+ this._eventList[existingIndex] = newEvent;
3094
+ }
3095
+ else {
3096
+ this._eventList.push(newEvent);
3097
+ // Trim queue if exceeded MAX_EVENTS
3098
+ if (this._eventList.length > MAX_EVENTS) {
3099
+ this._eventList.shift();
3100
+ }
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Generates markdown report with all tick events for a strategy.
3105
+ *
3106
+ * @param strategyName - Strategy name
3107
+ * @returns Markdown formatted report with all events
3108
+ */
3109
+ getReport(strategyName) {
3110
+ if (this._eventList.length === 0) {
3111
+ return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3112
+ }
3113
+ const header = columns.map((col) => col.label);
3114
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3115
+ const tableData = [header, ...rows];
3116
+ const table = str.table(tableData);
3117
+ // Calculate statistics
3118
+ const closedEvents = this._eventList.filter((e) => e.action === "closed");
3119
+ const totalClosed = closedEvents.length;
3120
+ const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
3121
+ const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
3122
+ const avgPnl = totalClosed > 0
3123
+ ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3124
+ : 0;
3125
+ return str.newline(`# Live Trading Report: ${strategyName}`, "", `Total events: ${this._eventList.length}`, `Closed signals: ${totalClosed}`, totalClosed > 0
3126
+ ? `Win rate: ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3127
+ : "", totalClosed > 0
3128
+ ? `Average PNL: ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3129
+ : "", "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
3130
+ }
3131
+ /**
3132
+ * Saves strategy report to disk.
3133
+ *
3134
+ * @param strategyName - Strategy name
3135
+ * @param path - Directory path to save report (default: "./logs/live")
3136
+ */
3137
+ async dump(strategyName, path = "./logs/live") {
3138
+ const markdown = this.getReport(strategyName);
3139
+ try {
3140
+ const dir = join(process.cwd(), path);
3141
+ await mkdir(dir, { recursive: true });
3142
+ const filename = `${strategyName}.md`;
3143
+ const filepath = join(dir, filename);
3144
+ await writeFile(filepath, markdown, "utf-8");
3145
+ console.log(`Live trading report saved: ${filepath}`);
3146
+ }
3147
+ catch (error) {
3148
+ console.error(`Failed to save markdown report:`, error);
3149
+ }
3150
+ }
3151
+ }
3152
+ /**
3153
+ * Service for generating and saving live trading markdown reports.
3154
+ *
3155
+ * Features:
3156
+ * - Listens to all signal events via onTick callback
3157
+ * - Accumulates all events (idle, opened, active, closed) per strategy
3158
+ * - Generates markdown tables with detailed event information
3159
+ * - Provides trading statistics (win rate, average PNL)
3160
+ * - Saves reports to disk in logs/live/{strategyName}.md
3161
+ *
3162
+ * @example
3163
+ * ```typescript
3164
+ * const service = new LiveMarkdownService();
3165
+ *
3166
+ * // Add to strategy callbacks
3167
+ * addStrategy({
3168
+ * strategyName: "my-strategy",
3169
+ * callbacks: {
3170
+ * onTick: (symbol, result, backtest) => {
3171
+ * if (!backtest) {
3172
+ * service.tick(result);
3173
+ * }
3174
+ * }
3175
+ * }
3176
+ * });
3177
+ *
3178
+ * // Later: generate and save report
3179
+ * await service.dump("my-strategy");
3180
+ * ```
3181
+ */
3182
+ class LiveMarkdownService {
412
3183
  constructor() {
3184
+ /** Logger service for debug output */
413
3185
  this.loggerService = inject(TYPES.loggerService);
414
- this.strategyConnectionService = inject(TYPES.strategyConnectionService);
415
- this.tick = async (symbol, when, backtest) => {
416
- this.loggerService.log("strategyPublicService tick", {
417
- symbol,
418
- when,
419
- backtest,
3186
+ /**
3187
+ * Memoized function to get or create ReportStorage for a strategy.
3188
+ * Each strategy gets its own isolated storage instance.
3189
+ */
3190
+ this.getStorage = memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage());
3191
+ /**
3192
+ * Processes tick events and accumulates all event types.
3193
+ * Should be called from IStrategyCallbacks.onTick.
3194
+ *
3195
+ * Processes all event types: idle, opened, active, closed.
3196
+ *
3197
+ * @param data - Tick result from strategy execution
3198
+ *
3199
+ * @example
3200
+ * ```typescript
3201
+ * const service = new LiveMarkdownService();
3202
+ *
3203
+ * callbacks: {
3204
+ * onTick: (symbol, result, backtest) => {
3205
+ * if (!backtest) {
3206
+ * service.tick(result);
3207
+ * }
3208
+ * }
3209
+ * }
3210
+ * ```
3211
+ */
3212
+ this.tick = async (data) => {
3213
+ this.loggerService.log("liveMarkdownService tick", {
3214
+ data,
420
3215
  });
421
- return await ExecutionContextService.runInContext(async () => {
422
- return await this.strategyConnectionService.tick(symbol);
423
- }, {
424
- when,
425
- backtest,
3216
+ const storage = this.getStorage(data.strategyName);
3217
+ if (data.action === "idle") {
3218
+ storage.addIdleEvent(data.currentPrice);
3219
+ }
3220
+ else if (data.action === "opened") {
3221
+ storage.addOpenedEvent(data);
3222
+ }
3223
+ else if (data.action === "active") {
3224
+ storage.addActiveEvent(data);
3225
+ }
3226
+ else if (data.action === "closed") {
3227
+ storage.addClosedEvent(data);
3228
+ }
3229
+ };
3230
+ /**
3231
+ * Generates markdown report with all events for a strategy.
3232
+ * Delegates to ReportStorage.getReport().
3233
+ *
3234
+ * @param strategyName - Strategy name to generate report for
3235
+ * @returns Markdown formatted report string with table of all events
3236
+ *
3237
+ * @example
3238
+ * ```typescript
3239
+ * const service = new LiveMarkdownService();
3240
+ * const markdown = await service.getReport("my-strategy");
3241
+ * console.log(markdown);
3242
+ * ```
3243
+ */
3244
+ this.getReport = async (strategyName) => {
3245
+ this.loggerService.log("liveMarkdownService getReport", {
3246
+ strategyName,
3247
+ });
3248
+ const storage = this.getStorage(strategyName);
3249
+ return storage.getReport(strategyName);
3250
+ };
3251
+ /**
3252
+ * Saves strategy report to disk.
3253
+ * Creates directory if it doesn't exist.
3254
+ * Delegates to ReportStorage.dump().
3255
+ *
3256
+ * @param strategyName - Strategy name to save report for
3257
+ * @param path - Directory path to save report (default: "./logs/live")
3258
+ *
3259
+ * @example
3260
+ * ```typescript
3261
+ * const service = new LiveMarkdownService();
3262
+ *
3263
+ * // Save to default path: ./logs/live/my-strategy.md
3264
+ * await service.dump("my-strategy");
3265
+ *
3266
+ * // Save to custom path: ./custom/path/my-strategy.md
3267
+ * await service.dump("my-strategy", "./custom/path");
3268
+ * ```
3269
+ */
3270
+ this.dump = async (strategyName, path = "./logs/live") => {
3271
+ this.loggerService.log("liveMarkdownService dump", {
3272
+ strategyName,
3273
+ path,
3274
+ });
3275
+ const storage = this.getStorage(strategyName);
3276
+ await storage.dump(strategyName, path);
3277
+ };
3278
+ /**
3279
+ * Clears accumulated event data from storage.
3280
+ * If strategyName is provided, clears only that strategy's data.
3281
+ * If strategyName is omitted, clears all strategies' data.
3282
+ *
3283
+ * @param strategyName - Optional strategy name to clear specific strategy data
3284
+ *
3285
+ * @example
3286
+ * ```typescript
3287
+ * const service = new LiveMarkdownService();
3288
+ *
3289
+ * // Clear specific strategy data
3290
+ * await service.clear("my-strategy");
3291
+ *
3292
+ * // Clear all strategies' data
3293
+ * await service.clear();
3294
+ * ```
3295
+ */
3296
+ this.clear = async (strategyName) => {
3297
+ this.loggerService.log("liveMarkdownService clear", {
3298
+ strategyName,
426
3299
  });
3300
+ this.getStorage.clear(strategyName);
427
3301
  };
3302
+ /**
3303
+ * Initializes the service by subscribing to live signal events.
3304
+ * Uses singleshot to ensure initialization happens only once.
3305
+ * Automatically called on first use.
3306
+ *
3307
+ * @example
3308
+ * ```typescript
3309
+ * const service = new LiveMarkdownService();
3310
+ * await service.init(); // Subscribe to live events
3311
+ * ```
3312
+ */
3313
+ this.init = singleshot(async () => {
3314
+ this.loggerService.log("liveMarkdownService init");
3315
+ signalLiveEmitter.subscribe(this.tick);
3316
+ });
428
3317
  }
429
3318
  }
430
3319
 
431
- class ExchangeSchemaService {
3320
+ /**
3321
+ * @class ExchangeValidationService
3322
+ * Service for managing and validating exchange configurations
3323
+ */
3324
+ class ExchangeValidationService {
432
3325
  constructor() {
3326
+ /**
3327
+ * @private
3328
+ * @readonly
3329
+ * Injected logger service instance
3330
+ */
433
3331
  this.loggerService = inject(TYPES.loggerService);
434
- this.getSchema = () => {
435
- this.loggerService.log("exchangeSchemaService getSchema");
436
- if (!this._exchangeSchema) {
437
- throw new Error("ExchangeSchemaService no exchange source provided");
3332
+ /**
3333
+ * @private
3334
+ * Map storing exchange schemas by exchange name
3335
+ */
3336
+ this._exchangeMap = new Map();
3337
+ /**
3338
+ * Adds an exchange schema to the validation service
3339
+ * @public
3340
+ * @throws {Error} If exchangeName already exists
3341
+ */
3342
+ this.addExchange = (exchangeName, exchangeSchema) => {
3343
+ this.loggerService.log("exchangeValidationService addExchange", {
3344
+ exchangeName,
3345
+ exchangeSchema,
3346
+ });
3347
+ if (this._exchangeMap.has(exchangeName)) {
3348
+ throw new Error(`exchange ${exchangeName} already exist`);
438
3349
  }
439
- return this._exchangeSchema;
440
- };
441
- this.addSchema = (exchangeSchema) => {
442
- this.loggerService.log("exchangeSchemaService addSchema");
443
- this._exchangeSchema = exchangeSchema;
3350
+ this._exchangeMap.set(exchangeName, exchangeSchema);
444
3351
  };
3352
+ /**
3353
+ * Validates the existence of an exchange
3354
+ * @public
3355
+ * @throws {Error} If exchangeName is not found
3356
+ * Memoized function to cache validation results
3357
+ */
3358
+ this.validate = memoize(([exchangeName]) => exchangeName, (exchangeName, source) => {
3359
+ this.loggerService.log("exchangeValidationService validate", {
3360
+ exchangeName,
3361
+ source,
3362
+ });
3363
+ const exchange = this._exchangeMap.get(exchangeName);
3364
+ if (!exchange) {
3365
+ throw new Error(`exchange ${exchangeName} not found source=${source}`);
3366
+ }
3367
+ return true;
3368
+ });
445
3369
  }
446
3370
  }
447
3371
 
448
- class StrategySchemaService {
3372
+ /**
3373
+ * @class StrategyValidationService
3374
+ * Service for managing and validating strategy configurations
3375
+ */
3376
+ class StrategyValidationService {
449
3377
  constructor() {
3378
+ /**
3379
+ * @private
3380
+ * @readonly
3381
+ * Injected logger service instance
3382
+ */
450
3383
  this.loggerService = inject(TYPES.loggerService);
451
- this.getSchema = () => {
452
- this.loggerService.log("strategySchemaService getSchema");
453
- if (!this._strategySchema) {
454
- throw new Error("StrategySchemaService no strategy provided");
3384
+ /**
3385
+ * @private
3386
+ * Map storing strategy schemas by strategy name
3387
+ */
3388
+ this._strategyMap = new Map();
3389
+ /**
3390
+ * Adds a strategy schema to the validation service
3391
+ * @public
3392
+ * @throws {Error} If strategyName already exists
3393
+ */
3394
+ this.addStrategy = (strategyName, strategySchema) => {
3395
+ this.loggerService.log("strategyValidationService addStrategy", {
3396
+ strategyName,
3397
+ strategySchema,
3398
+ });
3399
+ if (this._strategyMap.has(strategyName)) {
3400
+ throw new Error(`strategy ${strategyName} already exist`);
455
3401
  }
456
- return this._strategySchema;
3402
+ this._strategyMap.set(strategyName, strategySchema);
457
3403
  };
458
- this.addSchema = (strategySchema) => {
459
- this.loggerService.log("strategySchemaService addSchema");
460
- this._strategySchema = strategySchema;
3404
+ /**
3405
+ * Validates the existence of a strategy
3406
+ * @public
3407
+ * @throws {Error} If strategyName is not found
3408
+ * Memoized function to cache validation results
3409
+ */
3410
+ this.validate = memoize(([strategyName]) => strategyName, (strategyName, source) => {
3411
+ this.loggerService.log("strategyValidationService validate", {
3412
+ strategyName,
3413
+ source,
3414
+ });
3415
+ const strategy = this._strategyMap.get(strategyName);
3416
+ if (!strategy) {
3417
+ throw new Error(`strategy ${strategyName} not found source=${source}`);
3418
+ }
3419
+ return true;
3420
+ });
3421
+ }
3422
+ }
3423
+
3424
+ /**
3425
+ * @class FrameValidationService
3426
+ * Service for managing and validating frame configurations
3427
+ */
3428
+ class FrameValidationService {
3429
+ constructor() {
3430
+ /**
3431
+ * @private
3432
+ * @readonly
3433
+ * Injected logger service instance
3434
+ */
3435
+ this.loggerService = inject(TYPES.loggerService);
3436
+ /**
3437
+ * @private
3438
+ * Map storing frame schemas by frame name
3439
+ */
3440
+ this._frameMap = new Map();
3441
+ /**
3442
+ * Adds a frame schema to the validation service
3443
+ * @public
3444
+ * @throws {Error} If frameName already exists
3445
+ */
3446
+ this.addFrame = (frameName, frameSchema) => {
3447
+ this.loggerService.log("frameValidationService addFrame", {
3448
+ frameName,
3449
+ frameSchema,
3450
+ });
3451
+ if (this._frameMap.has(frameName)) {
3452
+ throw new Error(`frame ${frameName} already exist`);
3453
+ }
3454
+ this._frameMap.set(frameName, frameSchema);
461
3455
  };
3456
+ /**
3457
+ * Validates the existence of a frame
3458
+ * @public
3459
+ * @throws {Error} If frameName is not found
3460
+ * Memoized function to cache validation results
3461
+ */
3462
+ this.validate = memoize(([frameName]) => frameName, (frameName, source) => {
3463
+ this.loggerService.log("frameValidationService validate", {
3464
+ frameName,
3465
+ source,
3466
+ });
3467
+ const frame = this._frameMap.get(frameName);
3468
+ if (!frame) {
3469
+ throw new Error(`frame ${frameName} not found source=${source}`);
3470
+ }
3471
+ return true;
3472
+ });
462
3473
  }
463
3474
  }
464
3475
 
@@ -467,18 +3478,41 @@ class StrategySchemaService {
467
3478
  }
468
3479
  {
469
3480
  provide(TYPES.executionContextService, () => new ExecutionContextService());
3481
+ provide(TYPES.methodContextService, () => new MethodContextService());
470
3482
  }
471
3483
  {
472
3484
  provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
473
3485
  provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
3486
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
474
3487
  }
475
3488
  {
476
3489
  provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
477
3490
  provide(TYPES.strategySchemaService, () => new StrategySchemaService());
3491
+ provide(TYPES.frameSchemaService, () => new FrameSchemaService());
3492
+ }
3493
+ {
3494
+ provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
3495
+ provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
3496
+ provide(TYPES.frameGlobalService, () => new FrameGlobalService());
3497
+ provide(TYPES.liveGlobalService, () => new LiveGlobalService());
3498
+ provide(TYPES.backtestGlobalService, () => new BacktestGlobalService());
3499
+ }
3500
+ {
3501
+ provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
3502
+ provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
3503
+ }
3504
+ {
3505
+ provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
3506
+ provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
3507
+ }
3508
+ {
3509
+ provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
3510
+ provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
478
3511
  }
479
3512
  {
480
- provide(TYPES.exchangePublicService, () => new ExchangePublicService());
481
- provide(TYPES.strategyPublicService, () => new StrategyPublicService());
3513
+ provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
3514
+ provide(TYPES.strategyValidationService, () => new StrategyValidationService());
3515
+ provide(TYPES.frameValidationService, () => new FrameValidationService());
482
3516
  }
483
3517
 
484
3518
  const baseServices = {
@@ -486,159 +3520,840 @@ const baseServices = {
486
3520
  };
487
3521
  const contextServices = {
488
3522
  executionContextService: inject(TYPES.executionContextService),
3523
+ methodContextService: inject(TYPES.methodContextService),
489
3524
  };
490
3525
  const connectionServices = {
491
3526
  exchangeConnectionService: inject(TYPES.exchangeConnectionService),
492
3527
  strategyConnectionService: inject(TYPES.strategyConnectionService),
3528
+ frameConnectionService: inject(TYPES.frameConnectionService),
493
3529
  };
494
3530
  const schemaServices = {
495
3531
  exchangeSchemaService: inject(TYPES.exchangeSchemaService),
496
3532
  strategySchemaService: inject(TYPES.strategySchemaService),
3533
+ frameSchemaService: inject(TYPES.frameSchemaService),
3534
+ };
3535
+ const globalServices = {
3536
+ exchangeGlobalService: inject(TYPES.exchangeGlobalService),
3537
+ strategyGlobalService: inject(TYPES.strategyGlobalService),
3538
+ frameGlobalService: inject(TYPES.frameGlobalService),
3539
+ liveGlobalService: inject(TYPES.liveGlobalService),
3540
+ backtestGlobalService: inject(TYPES.backtestGlobalService),
497
3541
  };
498
- const publicServices = {
499
- exchangePublicService: inject(TYPES.exchangePublicService),
500
- strategyPublicService: inject(TYPES.strategyPublicService),
3542
+ const logicPrivateServices = {
3543
+ backtestLogicPrivateService: inject(TYPES.backtestLogicPrivateService),
3544
+ liveLogicPrivateService: inject(TYPES.liveLogicPrivateService),
3545
+ };
3546
+ const logicPublicServices = {
3547
+ backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
3548
+ liveLogicPublicService: inject(TYPES.liveLogicPublicService),
3549
+ };
3550
+ const markdownServices = {
3551
+ backtestMarkdownService: inject(TYPES.backtestMarkdownService),
3552
+ liveMarkdownService: inject(TYPES.liveMarkdownService),
3553
+ };
3554
+ const validationServices = {
3555
+ exchangeValidationService: inject(TYPES.exchangeValidationService),
3556
+ strategyValidationService: inject(TYPES.strategyValidationService),
3557
+ frameValidationService: inject(TYPES.frameValidationService),
501
3558
  };
502
3559
  const backtest = {
503
3560
  ...baseServices,
504
3561
  ...contextServices,
505
3562
  ...connectionServices,
506
3563
  ...schemaServices,
507
- ...publicServices,
3564
+ ...globalServices,
3565
+ ...logicPrivateServices,
3566
+ ...logicPublicServices,
3567
+ ...markdownServices,
3568
+ ...validationServices,
508
3569
  };
509
3570
  init();
3571
+ var backtest$1 = backtest;
510
3572
 
511
- function addStrategy(strategySchema) {
512
- backtest.strategySchemaService.addSchema(strategySchema);
513
- }
514
- function addExchange(exchangeSchema) {
515
- backtest.exchangeSchemaService.addSchema(exchangeSchema);
3573
+ /**
3574
+ * Sets custom logger implementation for the framework.
3575
+ *
3576
+ * All log messages from internal services will be forwarded to the provided logger
3577
+ * with automatic context injection (strategyName, exchangeName, symbol, etc.).
3578
+ *
3579
+ * @param logger - Custom logger implementing ILogger interface
3580
+ *
3581
+ * @example
3582
+ * ```typescript
3583
+ * setLogger({
3584
+ * log: (topic, ...args) => console.log(topic, args),
3585
+ * debug: (topic, ...args) => console.debug(topic, args),
3586
+ * info: (topic, ...args) => console.info(topic, args),
3587
+ * });
3588
+ * ```
3589
+ */
3590
+ async function setLogger(logger) {
3591
+ backtest$1.loggerService.setLogger(logger);
516
3592
  }
517
3593
 
518
- async function runBacktest(symbol, timeframes) {
519
- const results = [];
520
- for (const when of timeframes) {
521
- const result = await backtest.strategyPublicService.tick(symbol, when, true);
522
- // Сохраняем только результаты closed
523
- if (result.action === "closed") {
524
- results.push(result);
525
- }
526
- }
527
- return {
528
- symbol,
529
- results,
530
- };
3594
+ const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
3595
+ const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
3596
+ const ADD_FRAME_METHOD_NAME = "add.addFrame";
3597
+ /**
3598
+ * Registers a trading strategy in the framework.
3599
+ *
3600
+ * The strategy will be validated for:
3601
+ * - Signal validation (prices, TP/SL logic, timestamps)
3602
+ * - Interval throttling (prevents signal spam)
3603
+ * - Crash-safe persistence in live mode
3604
+ *
3605
+ * @param strategySchema - Strategy configuration object
3606
+ * @param strategySchema.strategyName - Unique strategy identifier
3607
+ * @param strategySchema.interval - Signal generation interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h")
3608
+ * @param strategySchema.getSignal - Async function that generates trading signals
3609
+ * @param strategySchema.callbacks - Optional lifecycle callbacks (onOpen, onClose)
3610
+ *
3611
+ * @example
3612
+ * ```typescript
3613
+ * addStrategy({
3614
+ * strategyName: "my-strategy",
3615
+ * interval: "5m",
3616
+ * getSignal: async (symbol) => ({
3617
+ * position: "long",
3618
+ * priceOpen: 50000,
3619
+ * priceTakeProfit: 51000,
3620
+ * priceStopLoss: 49000,
3621
+ * minuteEstimatedTime: 60,
3622
+ * timestamp: Date.now(),
3623
+ * }),
3624
+ * callbacks: {
3625
+ * onOpen: (backtest, symbol, signal) => console.log("Signal opened"),
3626
+ * onClose: (backtest, symbol, priceClose, signal) => console.log("Signal closed"),
3627
+ * },
3628
+ * });
3629
+ * ```
3630
+ */
3631
+ function addStrategy(strategySchema) {
3632
+ backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
3633
+ strategySchema,
3634
+ });
3635
+ backtest$1.strategyValidationService.addStrategy(strategySchema.strategyName, strategySchema);
3636
+ backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
531
3637
  }
532
- async function runBacktestGUI(symbol, timeframes) {
533
- const backtestResult = await runBacktest(symbol, timeframes);
534
- const { results } = backtestResult;
535
- // Таблица для терминала
536
- const table = new Table({
537
- head: ["#", "Time", "Note", "Price", "Reason", "PNL %"],
3638
+ /**
3639
+ * Registers an exchange data source in the framework.
3640
+ *
3641
+ * The exchange provides:
3642
+ * - Historical candle data via getCandles
3643
+ * - Price/quantity formatting for the exchange
3644
+ * - VWAP calculation from last 5 1m candles
3645
+ *
3646
+ * @param exchangeSchema - Exchange configuration object
3647
+ * @param exchangeSchema.exchangeName - Unique exchange identifier
3648
+ * @param exchangeSchema.getCandles - Async function to fetch candle data
3649
+ * @param exchangeSchema.formatPrice - Async function to format prices
3650
+ * @param exchangeSchema.formatQuantity - Async function to format quantities
3651
+ * @param exchangeSchema.callbacks - Optional callback for candle data events
3652
+ *
3653
+ * @example
3654
+ * ```typescript
3655
+ * addExchange({
3656
+ * exchangeName: "binance",
3657
+ * getCandles: async (symbol, interval, since, limit) => {
3658
+ * // Fetch from Binance API or database
3659
+ * return [{
3660
+ * timestamp: Date.now(),
3661
+ * open: 50000,
3662
+ * high: 51000,
3663
+ * low: 49000,
3664
+ * close: 50500,
3665
+ * volume: 1000,
3666
+ * }];
3667
+ * },
3668
+ * formatPrice: async (symbol, price) => price.toFixed(2),
3669
+ * formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
3670
+ * });
3671
+ * ```
3672
+ */
3673
+ function addExchange(exchangeSchema) {
3674
+ backtest$1.loggerService.info(ADD_EXCHANGE_METHOD_NAME, {
3675
+ exchangeSchema,
538
3676
  });
539
- let totalPnl = 0;
540
- let winCount = 0;
541
- let lossCount = 0;
542
- results.forEach((result, index) => {
543
- if (result.action === "closed") {
544
- const pnl = result.pnl.pnlPercentage;
545
- totalPnl += pnl;
546
- if (pnl > 0)
547
- winCount++;
548
- else if (pnl < 0)
549
- lossCount++;
550
- const pnlFormatted = pnl > 0 ? `+${pnl.toFixed(2)}%` : `${pnl.toFixed(2)}%`;
551
- const emoji = pnl > 0 ? "🟢" : pnl < 0 ? "🔴" : "⚪";
552
- table.push([
553
- index + 1,
554
- new Date(result.signal.timestamp).toISOString(),
555
- result.signal.note,
556
- result.currentPrice.toFixed(2),
557
- result.closeReason,
558
- `${emoji} ${pnlFormatted}`,
559
- ]);
560
- }
3677
+ backtest$1.exchangeValidationService.addExchange(exchangeSchema.exchangeName, exchangeSchema);
3678
+ backtest$1.exchangeSchemaService.register(exchangeSchema.exchangeName, exchangeSchema);
3679
+ }
3680
+ /**
3681
+ * Registers a timeframe generator for backtesting.
3682
+ *
3683
+ * The frame defines:
3684
+ * - Start and end dates for backtest period
3685
+ * - Interval for timeframe generation
3686
+ * - Callback for timeframe generation events
3687
+ *
3688
+ * @param frameSchema - Frame configuration object
3689
+ * @param frameSchema.frameName - Unique frame identifier
3690
+ * @param frameSchema.interval - Timeframe interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d" | "3d")
3691
+ * @param frameSchema.startDate - Start date for timeframe generation
3692
+ * @param frameSchema.endDate - End date for timeframe generation
3693
+ * @param frameSchema.callbacks - Optional callback for timeframe events
3694
+ *
3695
+ * @example
3696
+ * ```typescript
3697
+ * addFrame({
3698
+ * frameName: "1d-backtest",
3699
+ * interval: "1m",
3700
+ * startDate: new Date("2024-01-01T00:00:00Z"),
3701
+ * endDate: new Date("2024-01-02T00:00:00Z"),
3702
+ * callbacks: {
3703
+ * onTimeframe: (timeframe, startDate, endDate, interval) => {
3704
+ * console.log(`Generated ${timeframe.length} timeframes`);
3705
+ * },
3706
+ * },
3707
+ * });
3708
+ * ```
3709
+ */
3710
+ function addFrame(frameSchema) {
3711
+ backtest$1.loggerService.info(ADD_FRAME_METHOD_NAME, {
3712
+ frameSchema,
561
3713
  });
562
- // Добавляем статистику
563
- const closedCount = results.length;
564
- table.push([]);
565
- table.push([
566
- "TOTAL",
567
- `${closedCount} trades`,
568
- `Win: ${winCount}`,
569
- `Loss: ${lossCount}`,
570
- "-",
571
- `WR: ${closedCount > 0 ? ((winCount / closedCount) * 100).toFixed(1) : 0}%`,
572
- `${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`,
573
- ]);
574
- console.log("\n");
575
- console.log(table.toString());
576
- console.log("\n");
577
- }
578
-
579
- async function reduce(symbol, timeframes, callback, initialValue) {
580
- let accumulator = initialValue;
581
- for (let i = 0; i < timeframes.length; i++) {
582
- const when = timeframes[i];
583
- accumulator = await callback(accumulator, i, when, symbol);
584
- }
585
- return {
586
- symbol,
587
- accumulator,
588
- totalTicks: timeframes.length,
589
- };
3714
+ backtest$1.frameValidationService.addFrame(frameSchema.frameName, frameSchema);
3715
+ backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
590
3716
  }
591
3717
 
592
- const instances = new Map();
593
- function startRun(config) {
594
- const { symbol, interval } = config;
595
- // Останавливаем предыдущий инстанс для этого символа
596
- if (instances.has(symbol)) {
597
- stopRun(symbol);
598
- }
599
- const doWork = singlerun(async () => {
600
- const now = new Date();
601
- const result = await backtest.strategyPublicService.tick(symbol, now, false);
602
- const instance = instances.get(symbol);
603
- if (instance) {
604
- instance.tickCount++;
605
- }
606
- await sleep(interval);
607
- return result;
608
- });
609
- const intervalId = setInterval(doWork, interval);
610
- instances.set(symbol, {
611
- config,
612
- tickCount: 0,
613
- intervalId,
614
- doWork,
615
- });
3718
+ const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
3719
+ const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
3720
+ const LISTEN_SIGNAL_LIVE_METHOD_NAME = "event.listenSignalLive";
3721
+ const LISTEN_SIGNAL_LIVE_ONCE_METHOD_NAME = "event.listenSignalLiveOnce";
3722
+ const LISTEN_SIGNAL_BACKTEST_METHOD_NAME = "event.listenSignalBacktest";
3723
+ const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce";
3724
+ const LISTEN_ERROR_METHOD_NAME = "event.listenError";
3725
+ /**
3726
+ * Subscribes to all signal events with queued async processing.
3727
+ *
3728
+ * Events are processed sequentially in order received, even if callback is async.
3729
+ * Uses queued wrapper to prevent concurrent execution of the callback.
3730
+ *
3731
+ * @param fn - Callback function to handle signal events (idle, opened, active, closed)
3732
+ * @returns Unsubscribe function to stop listening
3733
+ *
3734
+ * @example
3735
+ * ```typescript
3736
+ * import { listenSignal } from "./function/event";
3737
+ *
3738
+ * const unsubscribe = listenSignal((event) => {
3739
+ * if (event.action === "opened") {
3740
+ * console.log("New signal opened:", event.signal);
3741
+ * } else if (event.action === "closed") {
3742
+ * console.log("Signal closed with PNL:", event.pnl.pnlPercentage);
3743
+ * }
3744
+ * });
3745
+ *
3746
+ * // Later: stop listening
3747
+ * unsubscribe();
3748
+ * ```
3749
+ */
3750
+ function listenSignal(fn) {
3751
+ backtest$1.loggerService.log(LISTEN_SIGNAL_METHOD_NAME);
3752
+ return signalEmitter.subscribe(queued(async (event) => fn(event)));
616
3753
  }
617
- function stopRun(symbol) {
618
- const instance = instances.get(symbol);
619
- if (instance) {
620
- clearInterval(instance.intervalId);
621
- instances.delete(symbol);
622
- }
3754
+ /**
3755
+ * Subscribes to filtered signal events with one-time execution.
3756
+ *
3757
+ * Listens for events matching the filter predicate, then executes callback once
3758
+ * and automatically unsubscribes. Useful for waiting for specific signal conditions.
3759
+ *
3760
+ * @param filterFn - Predicate to filter which events trigger the callback
3761
+ * @param fn - Callback function to handle the filtered event (called only once)
3762
+ * @returns Unsubscribe function to cancel the listener before it fires
3763
+ *
3764
+ * @example
3765
+ * ```typescript
3766
+ * import { listenSignalOnce } from "./function/event";
3767
+ *
3768
+ * // Wait for first take profit hit
3769
+ * listenSignalOnce(
3770
+ * (event) => event.action === "closed" && event.closeReason === "take_profit",
3771
+ * (event) => {
3772
+ * console.log("Take profit hit! PNL:", event.pnl.pnlPercentage);
3773
+ * }
3774
+ * );
3775
+ *
3776
+ * // Wait for any signal to close on BTCUSDT
3777
+ * const cancel = listenSignalOnce(
3778
+ * (event) => event.action === "closed" && event.signal.symbol === "BTCUSDT",
3779
+ * (event) => console.log("BTCUSDT signal closed")
3780
+ * );
3781
+ *
3782
+ * // Cancel if needed before event fires
3783
+ * cancel();
3784
+ * ```
3785
+ */
3786
+ function listenSignalOnce(filterFn, fn) {
3787
+ backtest$1.loggerService.log(LISTEN_SIGNAL_ONCE_METHOD_NAME);
3788
+ return signalEmitter.filter(filterFn).once(fn);
623
3789
  }
624
- function stopAll() {
625
- instances.forEach((instance) => {
626
- clearInterval(instance.intervalId);
627
- });
628
- instances.clear();
3790
+ /**
3791
+ * Subscribes to live trading signal events with queued async processing.
3792
+ *
3793
+ * Only receives events from Live.run() execution.
3794
+ * Events are processed sequentially in order received.
3795
+ *
3796
+ * @param fn - Callback function to handle live signal events
3797
+ * @returns Unsubscribe function to stop listening
3798
+ *
3799
+ * @example
3800
+ * ```typescript
3801
+ * import { listenSignalLive } from "./function/event";
3802
+ *
3803
+ * const unsubscribe = listenSignalLive((event) => {
3804
+ * if (event.action === "closed") {
3805
+ * console.log("Live signal closed:", event.pnl.pnlPercentage);
3806
+ * }
3807
+ * });
3808
+ * ```
3809
+ */
3810
+ function listenSignalLive(fn) {
3811
+ backtest$1.loggerService.log(LISTEN_SIGNAL_LIVE_METHOD_NAME);
3812
+ return signalLiveEmitter.subscribe(queued(async (event) => fn(event)));
3813
+ }
3814
+ /**
3815
+ * Subscribes to filtered live signal events with one-time execution.
3816
+ *
3817
+ * Only receives events from Live.run() execution.
3818
+ * Executes callback once and automatically unsubscribes.
3819
+ *
3820
+ * @param filterFn - Predicate to filter which events trigger the callback
3821
+ * @param fn - Callback function to handle the filtered event (called only once)
3822
+ * @returns Unsubscribe function to cancel the listener before it fires
3823
+ *
3824
+ * @example
3825
+ * ```typescript
3826
+ * import { listenSignalLiveOnce } from "./function/event";
3827
+ *
3828
+ * // Wait for first live take profit hit
3829
+ * listenSignalLiveOnce(
3830
+ * (event) => event.action === "closed" && event.closeReason === "take_profit",
3831
+ * (event) => console.log("Live take profit:", event.pnl.pnlPercentage)
3832
+ * );
3833
+ * ```
3834
+ */
3835
+ function listenSignalLiveOnce(filterFn, fn) {
3836
+ backtest$1.loggerService.log(LISTEN_SIGNAL_LIVE_ONCE_METHOD_NAME);
3837
+ return signalLiveEmitter.filter(filterFn).once(fn);
3838
+ }
3839
+ /**
3840
+ * Subscribes to backtest signal events with queued async processing.
3841
+ *
3842
+ * Only receives events from Backtest.run() execution.
3843
+ * Events are processed sequentially in order received.
3844
+ *
3845
+ * @param fn - Callback function to handle backtest signal events
3846
+ * @returns Unsubscribe function to stop listening
3847
+ *
3848
+ * @example
3849
+ * ```typescript
3850
+ * import { listenSignalBacktest } from "./function/event";
3851
+ *
3852
+ * const unsubscribe = listenSignalBacktest((event) => {
3853
+ * if (event.action === "closed") {
3854
+ * console.log("Backtest signal closed:", event.pnl.pnlPercentage);
3855
+ * }
3856
+ * });
3857
+ * ```
3858
+ */
3859
+ function listenSignalBacktest(fn) {
3860
+ backtest$1.loggerService.log(LISTEN_SIGNAL_BACKTEST_METHOD_NAME);
3861
+ return signalBacktestEmitter.subscribe(queued(async (event) => fn(event)));
3862
+ }
3863
+ /**
3864
+ * Subscribes to filtered backtest signal events with one-time execution.
3865
+ *
3866
+ * Only receives events from Backtest.run() execution.
3867
+ * Executes callback once and automatically unsubscribes.
3868
+ *
3869
+ * @param filterFn - Predicate to filter which events trigger the callback
3870
+ * @param fn - Callback function to handle the filtered event (called only once)
3871
+ * @returns Unsubscribe function to cancel the listener before it fires
3872
+ *
3873
+ * @example
3874
+ * ```typescript
3875
+ * import { listenSignalBacktestOnce } from "./function/event";
3876
+ *
3877
+ * // Wait for first backtest stop loss hit
3878
+ * listenSignalBacktestOnce(
3879
+ * (event) => event.action === "closed" && event.closeReason === "stop_loss",
3880
+ * (event) => console.log("Backtest stop loss:", event.pnl.pnlPercentage)
3881
+ * );
3882
+ * ```
3883
+ */
3884
+ function listenSignalBacktestOnce(filterFn, fn) {
3885
+ backtest$1.loggerService.log(LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME);
3886
+ return signalBacktestEmitter.filter(filterFn).once(fn);
3887
+ }
3888
+ /**
3889
+ * Subscribes to background execution errors with queued async processing.
3890
+ *
3891
+ * Listens to errors caught in Live.background() and Backtest.background() execution.
3892
+ * Events are processed sequentially in order received, even if callback is async.
3893
+ * Uses queued wrapper to prevent concurrent execution of the callback.
3894
+ *
3895
+ * @param fn - Callback function to handle error events
3896
+ * @returns Unsubscribe function to stop listening
3897
+ *
3898
+ * @example
3899
+ * ```typescript
3900
+ * import { listenError } from "./function/event";
3901
+ *
3902
+ * const unsubscribe = listenError((error) => {
3903
+ * console.error("Background execution error:", error.message);
3904
+ * // Log to monitoring service, send alerts, etc.
3905
+ * });
3906
+ *
3907
+ * // Later: stop listening
3908
+ * unsubscribe();
3909
+ * ```
3910
+ */
3911
+ function listenError(fn) {
3912
+ backtest$1.loggerService.log(LISTEN_ERROR_METHOD_NAME);
3913
+ return errorEmitter.subscribe(queued(async (error) => fn(error)));
629
3914
  }
630
3915
 
3916
+ const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
3917
+ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
3918
+ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
3919
+ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
3920
+ const GET_DATE_METHOD_NAME = "exchange.getDate";
3921
+ const GET_MODE_METHOD_NAME = "exchange.getMode";
3922
+ /**
3923
+ * Fetches historical candle data from the registered exchange.
3924
+ *
3925
+ * Candles are fetched backwards from the current execution context time.
3926
+ * Uses the exchange's getCandles implementation.
3927
+ *
3928
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3929
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
3930
+ * @param limit - Number of candles to fetch
3931
+ * @returns Promise resolving to array of candle data
3932
+ *
3933
+ * @example
3934
+ * ```typescript
3935
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
3936
+ * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
3937
+ * ```
3938
+ */
631
3939
  async function getCandles(symbol, interval, limit) {
632
- return await backtest.exchangeConnectionService.getCandles(symbol, interval, limit);
3940
+ backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
3941
+ symbol,
3942
+ interval,
3943
+ limit,
3944
+ });
3945
+ return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
633
3946
  }
3947
+ /**
3948
+ * Calculates VWAP (Volume Weighted Average Price) for a symbol.
3949
+ *
3950
+ * Uses the last 5 1-minute candles to calculate:
3951
+ * - Typical Price = (high + low + close) / 3
3952
+ * - VWAP = sum(typical_price * volume) / sum(volume)
3953
+ *
3954
+ * If volume is zero, returns simple average of close prices.
3955
+ *
3956
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3957
+ * @returns Promise resolving to VWAP price
3958
+ *
3959
+ * @example
3960
+ * ```typescript
3961
+ * const vwap = await getAveragePrice("BTCUSDT");
3962
+ * console.log(vwap); // 50125.43
3963
+ * ```
3964
+ */
634
3965
  async function getAveragePrice(symbol) {
635
- return await backtest.exchangeConnectionService.getAveragePrice(symbol);
3966
+ backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
3967
+ symbol,
3968
+ });
3969
+ return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
636
3970
  }
3971
+ /**
3972
+ * Formats a price value according to exchange rules.
3973
+ *
3974
+ * Uses the exchange's formatPrice implementation for proper decimal places.
3975
+ *
3976
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3977
+ * @param price - Raw price value
3978
+ * @returns Promise resolving to formatted price string
3979
+ *
3980
+ * @example
3981
+ * ```typescript
3982
+ * const formatted = await formatPrice("BTCUSDT", 50000.123456);
3983
+ * console.log(formatted); // "50000.12"
3984
+ * ```
3985
+ */
637
3986
  async function formatPrice(symbol, price) {
638
- return await backtest.exchangeConnectionService.formatPrice(symbol, price);
3987
+ backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
3988
+ symbol,
3989
+ price,
3990
+ });
3991
+ return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
639
3992
  }
3993
+ /**
3994
+ * Formats a quantity value according to exchange rules.
3995
+ *
3996
+ * Uses the exchange's formatQuantity implementation for proper decimal places.
3997
+ *
3998
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3999
+ * @param quantity - Raw quantity value
4000
+ * @returns Promise resolving to formatted quantity string
4001
+ *
4002
+ * @example
4003
+ * ```typescript
4004
+ * const formatted = await formatQuantity("BTCUSDT", 0.123456789);
4005
+ * console.log(formatted); // "0.12345678"
4006
+ * ```
4007
+ */
640
4008
  async function formatQuantity(symbol, quantity) {
641
- return await backtest.exchangeConnectionService.formatQuantity(symbol, quantity);
4009
+ backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
4010
+ symbol,
4011
+ quantity,
4012
+ });
4013
+ return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
4014
+ }
4015
+ /**
4016
+ * Gets the current date from execution context.
4017
+ *
4018
+ * In backtest mode: returns the current timeframe date being processed
4019
+ * In live mode: returns current real-time date
4020
+ *
4021
+ * @returns Promise resolving to current execution context date
4022
+ *
4023
+ * @example
4024
+ * ```typescript
4025
+ * const date = await getDate();
4026
+ * console.log(date); // 2024-01-01T12:00:00.000Z
4027
+ * ```
4028
+ */
4029
+ async function getDate() {
4030
+ backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
4031
+ const { when } = backtest$1.executionContextService.context;
4032
+ return new Date(when.getTime());
4033
+ }
4034
+ /**
4035
+ * Gets the current execution mode.
4036
+ *
4037
+ * @returns Promise resolving to "backtest" or "live"
4038
+ *
4039
+ * @example
4040
+ * ```typescript
4041
+ * const mode = await getMode();
4042
+ * if (mode === "backtest") {
4043
+ * console.log("Running in backtest mode");
4044
+ * } else {
4045
+ * console.log("Running in live mode");
4046
+ * }
4047
+ * ```
4048
+ */
4049
+ async function getMode() {
4050
+ backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
4051
+ const { backtest: bt } = backtest$1.executionContextService.context;
4052
+ return bt ? "backtest" : "live";
4053
+ }
4054
+
4055
+ const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
4056
+ const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
4057
+ const BACKTEST_METHOD_NAME_GET_REPORT = "BacktestUtils.getReport";
4058
+ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
4059
+ /**
4060
+ * Utility class for backtest operations.
4061
+ *
4062
+ * Provides simplified access to backtestGlobalService.run() with logging.
4063
+ * Exported as singleton instance for convenient usage.
4064
+ *
4065
+ * @example
4066
+ * ```typescript
4067
+ * import { Backtest } from "./classes/Backtest";
4068
+ *
4069
+ * for await (const result of Backtest.run("BTCUSDT", {
4070
+ * strategyName: "my-strategy",
4071
+ * exchangeName: "my-exchange",
4072
+ * frameName: "1d-backtest"
4073
+ * })) {
4074
+ * console.log("Closed signal PNL:", result.pnl.pnlPercentage);
4075
+ * }
4076
+ * ```
4077
+ */
4078
+ class BacktestUtils {
4079
+ constructor() {
4080
+ /**
4081
+ * Runs backtest for a symbol with context propagation.
4082
+ *
4083
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4084
+ * @param context - Execution context with strategy, exchange, and frame names
4085
+ * @returns Async generator yielding closed signals with PNL
4086
+ */
4087
+ this.run = (symbol, context) => {
4088
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_RUN, {
4089
+ symbol,
4090
+ context,
4091
+ });
4092
+ return backtest$1.backtestGlobalService.run(symbol, context);
4093
+ };
4094
+ /**
4095
+ * Runs backtest in background without yielding results.
4096
+ *
4097
+ * Consumes all backtest results internally without exposing them.
4098
+ * Useful for running backtests for side effects only (callbacks, logging).
4099
+ *
4100
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4101
+ * @param context - Execution context with strategy, exchange, and frame names
4102
+ * @returns Cancellation closure
4103
+ *
4104
+ * @example
4105
+ * ```typescript
4106
+ * // Run backtest silently, only callbacks will fire
4107
+ * await Backtest.background("BTCUSDT", {
4108
+ * strategyName: "my-strategy",
4109
+ * exchangeName: "my-exchange",
4110
+ * frameName: "1d-backtest"
4111
+ * });
4112
+ * console.log("Backtest completed");
4113
+ * ```
4114
+ */
4115
+ this.background = async (symbol, context) => {
4116
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_BACKGROUND, {
4117
+ symbol,
4118
+ context,
4119
+ });
4120
+ const iterator = this.run(symbol, context);
4121
+ let isStopped = false;
4122
+ const task = async () => {
4123
+ while (true) {
4124
+ const { done } = await iterator.next();
4125
+ if (done) {
4126
+ break;
4127
+ }
4128
+ if (isStopped) {
4129
+ break;
4130
+ }
4131
+ }
4132
+ };
4133
+ task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4134
+ return () => {
4135
+ isStopped = true;
4136
+ };
4137
+ };
4138
+ /**
4139
+ * Generates markdown report with all closed signals for a strategy.
4140
+ *
4141
+ * @param strategyName - Strategy name to generate report for
4142
+ * @returns Promise resolving to markdown formatted report string
4143
+ *
4144
+ * @example
4145
+ * ```typescript
4146
+ * const markdown = await Backtest.getReport("my-strategy");
4147
+ * console.log(markdown);
4148
+ * ```
4149
+ */
4150
+ this.getReport = async (strategyName) => {
4151
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_GET_REPORT, {
4152
+ strategyName,
4153
+ });
4154
+ return await backtest$1.backtestMarkdownService.getReport(strategyName);
4155
+ };
4156
+ /**
4157
+ * Saves strategy report to disk.
4158
+ *
4159
+ * @param strategyName - Strategy name to save report for
4160
+ * @param path - Optional directory path to save report (default: "./logs/backtest")
4161
+ *
4162
+ * @example
4163
+ * ```typescript
4164
+ * // Save to default path: ./logs/backtest/my-strategy.md
4165
+ * await Backtest.dump("my-strategy");
4166
+ *
4167
+ * // Save to custom path: ./custom/path/my-strategy.md
4168
+ * await Backtest.dump("my-strategy", "./custom/path");
4169
+ * ```
4170
+ */
4171
+ this.dump = async (strategyName, path) => {
4172
+ backtest$1.loggerService.info(BACKTEST_METHOD_NAME_DUMP, {
4173
+ strategyName,
4174
+ path,
4175
+ });
4176
+ await backtest$1.backtestMarkdownService.dump(strategyName, path);
4177
+ };
4178
+ }
4179
+ }
4180
+ /**
4181
+ * Singleton instance of BacktestUtils for convenient backtest operations.
4182
+ *
4183
+ * @example
4184
+ * ```typescript
4185
+ * import { Backtest } from "./classes/Backtest";
4186
+ *
4187
+ * for await (const result of Backtest.run("BTCUSDT", {
4188
+ * strategyName: "my-strategy",
4189
+ * exchangeName: "my-exchange",
4190
+ * frameName: "1d-backtest"
4191
+ * })) {
4192
+ * if (result.action === "closed") {
4193
+ * console.log("PNL:", result.pnl.pnlPercentage);
4194
+ * }
4195
+ * }
4196
+ * ```
4197
+ */
4198
+ const Backtest = new BacktestUtils();
4199
+
4200
+ const LIVE_METHOD_NAME_RUN = "LiveUtils.run";
4201
+ const LIVE_METHOD_NAME_BACKGROUND = "LiveUtils.background";
4202
+ const LIVE_METHOD_NAME_GET_REPORT = "LiveUtils.getReport";
4203
+ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
4204
+ /**
4205
+ * Utility class for live trading operations.
4206
+ *
4207
+ * Provides simplified access to liveGlobalService.run() with logging.
4208
+ * Exported as singleton instance for convenient usage.
4209
+ *
4210
+ * Features:
4211
+ * - Infinite async generator (never completes)
4212
+ * - Crash recovery via persisted state
4213
+ * - Real-time progression with Date.now()
4214
+ *
4215
+ * @example
4216
+ * ```typescript
4217
+ * import { Live } from "./classes/Live";
4218
+ *
4219
+ * // Infinite loop - use Ctrl+C to stop
4220
+ * for await (const result of Live.run("BTCUSDT", {
4221
+ * strategyName: "my-strategy",
4222
+ * exchangeName: "my-exchange",
4223
+ * frameName: ""
4224
+ * })) {
4225
+ * if (result.action === "opened") {
4226
+ * console.log("Signal opened:", result.signal);
4227
+ * } else if (result.action === "closed") {
4228
+ * console.log("PNL:", result.pnl.pnlPercentage);
4229
+ * }
4230
+ * }
4231
+ * ```
4232
+ */
4233
+ class LiveUtils {
4234
+ constructor() {
4235
+ /**
4236
+ * Runs live trading for a symbol with context propagation.
4237
+ *
4238
+ * Infinite async generator with crash recovery support.
4239
+ * Process can crash and restart - state will be recovered from disk.
4240
+ *
4241
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4242
+ * @param context - Execution context with strategy and exchange names
4243
+ * @returns Infinite async generator yielding opened and closed signals
4244
+ */
4245
+ this.run = (symbol, context) => {
4246
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_RUN, {
4247
+ symbol,
4248
+ context,
4249
+ });
4250
+ return backtest$1.liveGlobalService.run(symbol, context);
4251
+ };
4252
+ /**
4253
+ * Runs live trading in background without yielding results.
4254
+ *
4255
+ * Consumes all live trading results internally without exposing them.
4256
+ * Infinite loop - will run until process is stopped or crashes.
4257
+ * Useful for running live trading for side effects only (callbacks, persistence).
4258
+ *
4259
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4260
+ * @param context - Execution context with strategy and exchange names
4261
+ * @returns Cancellation closure
4262
+ *
4263
+ * @example
4264
+ * ```typescript
4265
+ * // Run live trading silently in background, only callbacks will fire
4266
+ * // This will run forever until Ctrl+C
4267
+ * await Live.background("BTCUSDT", {
4268
+ * strategyName: "my-strategy",
4269
+ * exchangeName: "my-exchange"
4270
+ * });
4271
+ * ```
4272
+ */
4273
+ this.background = async (symbol, context) => {
4274
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_BACKGROUND, {
4275
+ symbol,
4276
+ context,
4277
+ });
4278
+ const iterator = this.run(symbol, context);
4279
+ let isStopped = false;
4280
+ let lastValue = null;
4281
+ const task = async () => {
4282
+ while (true) {
4283
+ const { value, done } = await iterator.next();
4284
+ if (value) {
4285
+ lastValue = value;
4286
+ }
4287
+ if (done) {
4288
+ break;
4289
+ }
4290
+ if (lastValue?.action === "closed" && isStopped) {
4291
+ break;
4292
+ }
4293
+ }
4294
+ };
4295
+ task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4296
+ return () => {
4297
+ isStopped = true;
4298
+ };
4299
+ };
4300
+ /**
4301
+ * Generates markdown report with all events for a strategy.
4302
+ *
4303
+ * @param strategyName - Strategy name to generate report for
4304
+ * @returns Promise resolving to markdown formatted report string
4305
+ *
4306
+ * @example
4307
+ * ```typescript
4308
+ * const markdown = await Live.getReport("my-strategy");
4309
+ * console.log(markdown);
4310
+ * ```
4311
+ */
4312
+ this.getReport = async (strategyName) => {
4313
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_GET_REPORT, {
4314
+ strategyName,
4315
+ });
4316
+ return await backtest$1.liveMarkdownService.getReport(strategyName);
4317
+ };
4318
+ /**
4319
+ * Saves strategy report to disk.
4320
+ *
4321
+ * @param strategyName - Strategy name to save report for
4322
+ * @param path - Optional directory path to save report (default: "./logs/live")
4323
+ *
4324
+ * @example
4325
+ * ```typescript
4326
+ * // Save to default path: ./logs/live/my-strategy.md
4327
+ * await Live.dump("my-strategy");
4328
+ *
4329
+ * // Save to custom path: ./custom/path/my-strategy.md
4330
+ * await Live.dump("my-strategy", "./custom/path");
4331
+ * ```
4332
+ */
4333
+ this.dump = async (strategyName, path) => {
4334
+ backtest$1.loggerService.info(LIVE_METHOD_NAME_DUMP, {
4335
+ strategyName,
4336
+ path,
4337
+ });
4338
+ await backtest$1.liveMarkdownService.dump(strategyName, path);
4339
+ };
4340
+ }
642
4341
  }
4342
+ /**
4343
+ * Singleton instance of LiveUtils for convenient live trading operations.
4344
+ *
4345
+ * @example
4346
+ * ```typescript
4347
+ * import { Live } from "./classes/Live";
4348
+ *
4349
+ * for await (const result of Live.run("BTCUSDT", {
4350
+ * strategyName: "my-strategy",
4351
+ * exchangeName: "my-exchange",
4352
+ * })) {
4353
+ * console.log("Result:", result.action);
4354
+ * }
4355
+ * ```
4356
+ */
4357
+ const Live = new LiveUtils();
643
4358
 
644
- export { ExecutionContextService, addExchange, addStrategy, backtest, formatPrice, formatQuantity, getAveragePrice, getCandles, reduce, runBacktest, runBacktestGUI, startRun, stopAll, stopRun };
4359
+ export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listenError, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };