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