backtest-kit 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +707 -153
  2. package/build/index.cjs +2906 -192
  3. package/build/index.mjs +2901 -188
  4. package/package.json +2 -9
  5. package/types.d.ts +2205 -60
package/build/index.cjs CHANGED
@@ -7,10 +7,29 @@ var fs = require('fs/promises');
7
7
  var path = require('path');
8
8
  var crypto = require('crypto');
9
9
  var os = require('os');
10
- var Table = require('cli-table3');
11
10
 
12
11
  const { init, inject, provide } = diKit.createActivator("backtest");
13
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
+ */
14
33
  const MethodContextService = diScoped.scoped(class {
15
34
  constructor(context) {
16
35
  this.context = context;
@@ -49,6 +68,15 @@ const logicPublicServices$1 = {
49
68
  backtestLogicPublicService: Symbol('backtestLogicPublicService'),
50
69
  liveLogicPublicService: Symbol('liveLogicPublicService'),
51
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'),
79
+ };
52
80
  const TYPES = {
53
81
  ...baseServices$1,
54
82
  ...contextServices$1,
@@ -57,14 +85,39 @@ const TYPES = {
57
85
  ...globalServices$1,
58
86
  ...logicPrivateServices$1,
59
87
  ...logicPublicServices$1,
88
+ ...markdownServices$1,
89
+ ...validationServices$1,
60
90
  };
61
91
 
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
+ */
62
111
  const ExecutionContextService = diScoped.scoped(class {
63
112
  constructor(context) {
64
113
  this.context = context;
65
114
  }
66
115
  });
67
116
 
117
+ /**
118
+ * No-op logger implementation used as default.
119
+ * Silently discards all log messages.
120
+ */
68
121
  const NOOP_LOGGER = {
69
122
  log() {
70
123
  },
@@ -72,31 +125,84 @@ const NOOP_LOGGER = {
72
125
  },
73
126
  info() {
74
127
  },
128
+ warn() {
129
+ },
75
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
+ */
76
142
  class LoggerService {
77
143
  constructor() {
78
144
  this.methodContextService = inject(TYPES.methodContextService);
79
145
  this.executionContextService = inject(TYPES.executionContextService);
80
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
+ */
81
153
  this.log = async (topic, ...args) => {
82
154
  await this._commonLogger.log(topic, ...args, this.methodContext, this.executionContext);
83
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
+ */
84
162
  this.debug = async (topic, ...args) => {
85
163
  await this._commonLogger.debug(topic, ...args, this.methodContext, this.executionContext);
86
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
+ */
87
171
  this.info = async (topic, ...args) => {
88
172
  await this._commonLogger.info(topic, ...args, this.methodContext, this.executionContext);
89
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
+ */
90
188
  this.setLogger = (logger) => {
91
189
  this._commonLogger = logger;
92
190
  };
93
191
  }
192
+ /**
193
+ * Gets current method context if available.
194
+ * Contains strategyName, exchangeName, frameName from MethodContextService.
195
+ */
94
196
  get methodContext() {
95
197
  if (MethodContextService.hasContext()) {
96
198
  return this.methodContextService.context;
97
199
  }
98
200
  return {};
99
201
  }
202
+ /**
203
+ * Gets current execution context if available.
204
+ * Contains symbol, when, backtest from ExecutionContextService.
205
+ */
100
206
  get executionContext() {
101
207
  if (ExecutionContextService.hasContext()) {
102
208
  return this.executionContextService.context;
@@ -117,10 +223,44 @@ const INTERVAL_MINUTES$2 = {
117
223
  "6h": 360,
118
224
  "8h": 480,
119
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
+ */
120
252
  class ClientExchange {
121
253
  constructor(params) {
122
254
  this.params = params;
123
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
+ */
124
264
  async getCandles(symbol, interval, limit) {
125
265
  this.params.logger.debug(`ClientExchange getCandles`, {
126
266
  symbol,
@@ -128,17 +268,34 @@ class ClientExchange {
128
268
  limit,
129
269
  });
130
270
  const step = INTERVAL_MINUTES$2[interval];
131
- const adjust = step * limit - 1;
271
+ const adjust = step * limit - step;
132
272
  if (!adjust) {
133
273
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
134
274
  }
135
275
  const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
136
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
+ }
137
284
  if (this.params.callbacks?.onCandleData) {
138
- this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
285
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
139
286
  }
140
- return data;
287
+ return filteredData;
141
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
+ */
142
299
  async getNextCandles(symbol, interval, limit) {
143
300
  this.params.logger.debug(`ClientExchange getNextCandles`, {
144
301
  symbol,
@@ -155,11 +312,30 @@ class ClientExchange {
155
312
  return [];
156
313
  }
157
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
+ }
158
321
  if (this.params.callbacks?.onCandleData) {
159
- this.params.callbacks.onCandleData(symbol, interval, since, limit, data);
322
+ this.params.callbacks.onCandleData(symbol, interval, since, limit, filteredData);
160
323
  }
161
- return data;
324
+ return filteredData;
162
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
+ */
163
339
  async getAveragePrice(symbol) {
164
340
  this.params.logger.debug(`ClientExchange getAveragePrice`, {
165
341
  symbol,
@@ -199,13 +375,44 @@ class ClientExchange {
199
375
  }
200
376
  }
201
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
+ */
202
400
  class ExchangeConnectionService {
203
401
  constructor() {
204
402
  this.loggerService = inject(TYPES.loggerService);
205
403
  this.executionContextService = inject(TYPES.executionContextService);
206
404
  this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
207
405
  this.methodContextService = inject(TYPES.methodContextService);
208
- this.getExchange = functoolsKit.memoize((exchangeName) => `${exchangeName}`, (exchangeName) => {
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) => {
209
416
  const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
210
417
  return new ClientExchange({
211
418
  execution: this.executionContextService,
@@ -217,6 +424,16 @@ class ExchangeConnectionService {
217
424
  callbacks,
218
425
  });
219
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
+ */
220
437
  this.getCandles = async (symbol, interval, limit) => {
221
438
  this.loggerService.log("exchangeConnectionService getCandles", {
222
439
  symbol,
@@ -225,6 +442,17 @@ class ExchangeConnectionService {
225
442
  });
226
443
  return await this.getExchange(this.methodContextService.context.exchangeName).getCandles(symbol, interval, limit);
227
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
+ */
228
456
  this.getNextCandles = async (symbol, interval, limit) => {
229
457
  this.loggerService.log("exchangeConnectionService getNextCandles", {
230
458
  symbol,
@@ -233,12 +461,30 @@ class ExchangeConnectionService {
233
461
  });
234
462
  return await this.getExchange(this.methodContextService.context.exchangeName).getNextCandles(symbol, interval, limit);
235
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
+ */
236
473
  this.getAveragePrice = async (symbol) => {
237
474
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
238
475
  symbol,
239
476
  });
240
477
  return await this.getExchange(this.methodContextService.context.exchangeName).getAveragePrice(symbol);
241
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
+ */
242
488
  this.formatPrice = async (symbol, price) => {
243
489
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
244
490
  symbol,
@@ -246,6 +492,15 @@ class ExchangeConnectionService {
246
492
  });
247
493
  return await this.getExchange(this.methodContextService.context.exchangeName).formatPrice(symbol, price);
248
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
+ */
249
504
  this.formatQuantity = async (symbol, quantity) => {
250
505
  this.loggerService.log("exchangeConnectionService getAveragePrice", {
251
506
  symbol,
@@ -256,8 +511,45 @@ class ExchangeConnectionService {
256
511
  }
257
512
  }
258
513
 
514
+ /**
515
+ * Slippage percentage applied to entry and exit prices.
516
+ * Simulates market impact and order book depth.
517
+ */
259
518
  const PERCENT_SLIPPAGE = 0.1;
519
+ /**
520
+ * Fee percentage charged per transaction.
521
+ * Applied twice (entry and exit) for total fee calculation.
522
+ */
260
523
  const PERCENT_FEE = 0.1;
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
+ */
261
553
  const toProfitLossDto = (signal, priceClose) => {
262
554
  const priceOpen = signal.priceOpen;
263
555
  let priceOpenWithSlippage;
@@ -433,9 +725,9 @@ const BASE_WAIT_FOR_INIT_FN = async (self) => {
433
725
  }
434
726
  catch {
435
727
  const filePath = self._getFilePath(key);
436
- console.error(`agent-swarm PersistBase found invalid document for filePath=${filePath} entityName=${self.entityName}`);
728
+ console.error(`backtest-kit PersistBase found invalid document for filePath=${filePath} entityName=${self.entityName}`);
437
729
  if (await functoolsKit.not(BASE_WAIT_FOR_INIT_UNLINK_FN(filePath))) {
438
- console.error(`agent-swarm PersistBase failed to remove invalid document for filePath=${filePath} entityName=${self.entityName}`);
730
+ console.error(`backtest-kit PersistBase failed to remove invalid document for filePath=${filePath} entityName=${self.entityName}`);
439
731
  }
440
732
  }
441
733
  }
@@ -446,13 +738,36 @@ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => functoolsKit.trycatch(f
446
738
  return true;
447
739
  }
448
740
  catch (error) {
449
- console.error(`agent-swarm PersistBase unlink failed for filePath=${filePath} error=${functoolsKit.getErrorMessage(error)}`);
741
+ console.error(`backtest-kit PersistBase unlink failed for filePath=${filePath} error=${functoolsKit.getErrorMessage(error)}`);
450
742
  throw error;
451
743
  }
452
744
  }, BASE_UNLINK_RETRY_COUNT, BASE_UNLINK_RETRY_DELAY), {
453
745
  defaultValue: false,
454
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
+ */
455
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
+ */
456
771
  constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
457
772
  this.entityName = entityName;
458
773
  this.baseDir = baseDir;
@@ -463,6 +778,12 @@ const PersistBase = functoolsKit.makeExtendable(class {
463
778
  });
464
779
  this._directory = path.join(this.baseDir, this.entityName);
465
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
+ */
466
787
  _getFilePath(entityId) {
467
788
  return path.join(this.baseDir, this.entityName, `${entityId}.json`);
468
789
  }
@@ -473,6 +794,11 @@ const PersistBase = functoolsKit.makeExtendable(class {
473
794
  });
474
795
  await this[BASE_WAIT_FOR_INIT_SYMBOL]();
475
796
  }
797
+ /**
798
+ * Returns count of persisted entities.
799
+ *
800
+ * @returns Promise resolving to number of .json files in directory
801
+ */
476
802
  async getCount() {
477
803
  const files = await fs.readdir(this._directory);
478
804
  const { length } = files.filter((file) => file.endsWith(".json"));
@@ -526,6 +852,13 @@ const PersistBase = functoolsKit.makeExtendable(class {
526
852
  throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
527
853
  }
528
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
+ */
529
862
  async removeValue(entityId) {
530
863
  backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_VALUE, {
531
864
  entityName: this.entityName,
@@ -542,6 +875,12 @@ const PersistBase = functoolsKit.makeExtendable(class {
542
875
  throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
543
876
  }
544
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
+ */
545
884
  async removeAll() {
546
885
  backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_ALL, {
547
886
  entityName: this.entityName,
@@ -557,6 +896,13 @@ const PersistBase = functoolsKit.makeExtendable(class {
557
896
  throw new Error(`Failed to remove values for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
558
897
  }
559
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
+ */
560
906
  async *values() {
561
907
  backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_VALUES, {
562
908
  entityName: this.entityName,
@@ -579,6 +925,13 @@ const PersistBase = functoolsKit.makeExtendable(class {
579
925
  throw new Error(`Failed to read values for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
580
926
  }
581
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
+ */
582
935
  async *keys() {
583
936
  backtest$1.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
584
937
  entityName: this.entityName,
@@ -600,11 +953,23 @@ const PersistBase = functoolsKit.makeExtendable(class {
600
953
  throw new Error(`Failed to read keys for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
601
954
  }
602
955
  }
956
+ /**
957
+ * Async iterator implementation.
958
+ * Delegates to values() generator.
959
+ *
960
+ * @returns AsyncIterableIterator yielding entities
961
+ */
603
962
  async *[(_a = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
604
963
  for await (const entity of this.values()) {
605
964
  yield entity;
606
965
  }
607
966
  }
967
+ /**
968
+ * Filters entities by predicate function.
969
+ *
970
+ * @param predicate - Filter function
971
+ * @returns AsyncGenerator yielding filtered entities
972
+ */
608
973
  async *filter(predicate) {
609
974
  for await (const entity of this.values()) {
610
975
  if (predicate(entity)) {
@@ -612,6 +977,13 @@ const PersistBase = functoolsKit.makeExtendable(class {
612
977
  }
613
978
  }
614
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
+ */
615
987
  async *take(total, predicate) {
616
988
  let count = 0;
617
989
  if (predicate) {
@@ -637,6 +1009,17 @@ const PersistBase = functoolsKit.makeExtendable(class {
637
1009
  }
638
1010
  }
639
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
+ */
640
1023
  class PersistSignalUtils {
641
1024
  constructor() {
642
1025
  this.PersistSignalFactory = PersistBase;
@@ -644,6 +1027,16 @@ class PersistSignalUtils {
644
1027
  strategyName,
645
1028
  `./logs/data/signal/`,
646
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
+ */
647
1040
  this.readSignalData = async (strategyName, symbol) => {
648
1041
  backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
649
1042
  const isInitial = !this.getSignalStorage.has(strategyName);
@@ -655,6 +1048,17 @@ class PersistSignalUtils {
655
1048
  }
656
1049
  return null;
657
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
+ */
658
1062
  this.writeSignalData = async (signalRow, strategyName, symbol) => {
659
1063
  backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
660
1064
  const isInitial = !this.getSignalStorage.has(strategyName);
@@ -663,11 +1067,41 @@ class PersistSignalUtils {
663
1067
  await stateStorage.writeValue(symbol, { signalRow });
664
1068
  };
665
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
+ */
666
1084
  usePersistSignalAdapter(Ctor) {
667
1085
  backtest$1.loggerService.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
668
1086
  this.PersistSignalFactory = Ctor;
669
1087
  }
670
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
+ */
671
1105
  const PersistSignalAdaper = new PersistSignalUtils();
672
1106
 
673
1107
  const INTERVAL_MINUTES$1 = {
@@ -678,6 +1112,48 @@ const INTERVAL_MINUTES$1 = {
678
1112
  "30m": 30,
679
1113
  "1h": 60,
680
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
+ };
681
1157
  const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
682
1158
  const currentTime = self.params.execution.context.when.getTime();
683
1159
  {
@@ -694,10 +1170,17 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
694
1170
  if (!signal) {
695
1171
  return null;
696
1172
  }
697
- return {
1173
+ const signalRow = {
698
1174
  id: functoolsKit.randomString(),
699
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,
700
1180
  };
1181
+ // Валидируем сигнал перед возвратом
1182
+ VALIDATE_SIGNAL_FN(signalRow);
1183
+ return signalRow;
701
1184
  }, {
702
1185
  defaultValue: null,
703
1186
  });
@@ -716,15 +1199,70 @@ const WAIT_FOR_INIT_FN = async (self) => {
716
1199
  if (self.params.execution.context.backtest) {
717
1200
  return;
718
1201
  }
719
- self._pendingSignal = await PersistSignalAdaper.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
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;
720
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
+ */
721
1241
  class ClientStrategy {
722
1242
  constructor(params) {
723
1243
  this.params = params;
724
1244
  this._pendingSignal = null;
725
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
+ */
726
1255
  this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN(this));
727
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
+ */
728
1266
  async setPendingSignal(pendingSignal) {
729
1267
  this.params.logger.debug("ClientStrategy setPendingSignal", {
730
1268
  pendingSignal,
@@ -735,6 +1273,29 @@ class ClientStrategy {
735
1273
  }
736
1274
  await PersistSignalAdaper.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
737
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
+ */
738
1299
  async tick() {
739
1300
  this.params.logger.debug("ClientStrategy tick");
740
1301
  if (!this._pendingSignal) {
@@ -742,17 +1303,35 @@ class ClientStrategy {
742
1303
  await this.setPendingSignal(pendingSignal);
743
1304
  if (this._pendingSignal) {
744
1305
  if (this.params.callbacks?.onOpen) {
745
- this.params.callbacks.onOpen(this.params.execution.context.backtest, this.params.execution.context.symbol, this._pendingSignal);
1306
+ this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
746
1307
  }
747
- return {
1308
+ const result = {
748
1309
  action: "opened",
749
1310
  signal: this._pendingSignal,
1311
+ strategyName: this.params.method.context.strategyName,
1312
+ exchangeName: this.params.method.context.exchangeName,
1313
+ currentPrice: this._pendingSignal.priceOpen,
750
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;
751
1319
  }
752
- return {
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 = {
753
1325
  action: "idle",
754
1326
  signal: null,
1327
+ strategyName: this.params.method.context.strategyName,
1328
+ exchangeName: this.params.method.context.exchangeName,
1329
+ currentPrice,
755
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;
756
1335
  }
757
1336
  const when = this.params.execution.context.when;
758
1337
  const signal = this._pendingSignal;
@@ -798,6 +1377,14 @@ class ClientStrategy {
798
1377
  if (shouldClose) {
799
1378
  const pnl = toProfitLossDto(signal, averagePrice);
800
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
+ }
801
1388
  this.params.logger.debug("ClientStrategy closing", {
802
1389
  symbol: this.params.execution.context.symbol,
803
1390
  signalId: signal.id,
@@ -807,24 +1394,58 @@ class ClientStrategy {
807
1394
  pnlPercentage: pnl.pnlPercentage,
808
1395
  });
809
1396
  if (this.params.callbacks?.onClose) {
810
- this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, averagePrice, signal);
1397
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
811
1398
  }
812
1399
  await this.setPendingSignal(null);
813
- return {
1400
+ const result = {
814
1401
  action: "closed",
815
1402
  signal: signal,
816
1403
  currentPrice: averagePrice,
817
1404
  closeReason: closeReason,
818
1405
  closeTimestamp: closeTimestamp,
819
1406
  pnl: pnl,
1407
+ strategyName: this.params.method.context.strategyName,
1408
+ exchangeName: this.params.method.context.exchangeName,
820
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);
821
1417
  }
822
- return {
1418
+ const result = {
823
1419
  action: "active",
824
1420
  signal: signal,
825
1421
  currentPrice: averagePrice,
1422
+ strategyName: this.params.method.context.strategyName,
1423
+ exchangeName: this.params.method.context.exchangeName,
826
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;
827
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
+ */
828
1449
  async backtest(candles) {
829
1450
  this.params.logger.debug("ClientStrategy backtest", {
830
1451
  symbol: this.params.execution.context.symbol,
@@ -837,6 +1458,10 @@ class ClientStrategy {
837
1458
  if (!this.params.execution.context.backtest) {
838
1459
  throw new Error("ClientStrategy backtest: running in live context");
839
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
+ }
840
1465
  // Проверяем каждую свечу на достижение TP/SL
841
1466
  // Начинаем с индекса 4 (пятая свеча), чтобы было минимум 5 свечей для VWAP
842
1467
  for (let i = 4; i < candles.length; i++) {
@@ -879,18 +1504,28 @@ class ClientStrategy {
879
1504
  closeTimestamp,
880
1505
  pnlPercentage: pnl.pnlPercentage,
881
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
+ }
882
1511
  if (this.params.callbacks?.onClose) {
883
- this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, averagePrice, signal);
1512
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
884
1513
  }
885
1514
  await this.setPendingSignal(null);
886
- return {
1515
+ const result = {
887
1516
  action: "closed",
888
1517
  signal: signal,
889
1518
  currentPrice: averagePrice,
890
1519
  closeReason: closeReason,
891
1520
  closeTimestamp: closeTimestamp,
892
1521
  pnl: pnl,
1522
+ strategyName: this.params.method.context.strategyName,
1523
+ exchangeName: this.params.method.context.exchangeName,
893
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;
894
1529
  }
895
1530
  }
896
1531
  // Если TP/SL не достигнут за период, вычисляем VWAP из последних 5 свечей
@@ -905,21 +1540,73 @@ class ClientStrategy {
905
1540
  closeTimestamp,
906
1541
  pnlPercentage: pnl.pnlPercentage,
907
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
+ }
908
1547
  if (this.params.callbacks?.onClose) {
909
- this.params.callbacks.onClose(this.params.execution.context.backtest, this.params.execution.context.symbol, lastPrice, signal);
1548
+ this.params.callbacks.onClose(this.params.execution.context.symbol, signal, lastPrice, this.params.execution.context.backtest);
910
1549
  }
911
1550
  await this.setPendingSignal(null);
912
- return {
1551
+ const result = {
913
1552
  action: "closed",
914
1553
  signal: signal,
915
1554
  currentPrice: lastPrice,
916
1555
  closeReason: "time_expired",
917
1556
  closeTimestamp: closeTimestamp,
918
1557
  pnl: pnl,
1558
+ strategyName: this.params.method.context.strategyName,
1559
+ exchangeName: this.params.method.context.exchangeName,
919
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;
920
1565
  }
921
1566
  }
922
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
+ */
923
1610
  class StrategyConnectionService {
924
1611
  constructor() {
925
1612
  this.loggerService = inject(TYPES.loggerService);
@@ -927,11 +1614,21 @@ class StrategyConnectionService {
927
1614
  this.strategySchemaService = inject(TYPES.strategySchemaService);
928
1615
  this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
929
1616
  this.methodContextService = inject(TYPES.methodContextService);
930
- this.getStrategy = functoolsKit.memoize((strategyName) => `${strategyName}`, (strategyName) => {
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) => {
931
1627
  const { getSignal, interval, callbacks } = this.strategySchemaService.get(strategyName);
932
1628
  return new ClientStrategy({
933
1629
  interval,
934
1630
  execution: this.executionContextService,
1631
+ method: this.methodContextService,
935
1632
  logger: this.loggerService,
936
1633
  exchange: this.exchangeConnectionService,
937
1634
  strategyName,
@@ -939,21 +1636,61 @@ class StrategyConnectionService {
939
1636
  callbacks,
940
1637
  });
941
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
+ */
942
1647
  this.tick = async () => {
943
1648
  this.loggerService.log("strategyConnectionService tick");
944
1649
  const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
945
1650
  await strategy.waitForInit();
946
- return await strategy.tick();
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;
947
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
+ */
948
1672
  this.backtest = async (candles) => {
949
- this.loggerService.log("strategyConnectionService backtest");
1673
+ this.loggerService.log("strategyConnectionService backtest", {
1674
+ candleCount: candles.length,
1675
+ });
950
1676
  const strategy = await this.getStrategy(this.methodContextService.context.strategyName);
951
1677
  await strategy.waitForInit();
952
- return await strategy.backtest(candles);
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;
953
1686
  };
954
1687
  }
955
1688
  }
956
1689
 
1690
+ /**
1691
+ * Maps FrameInterval to minutes for timestamp calculation.
1692
+ * Used to generate timeframe arrays with proper spacing.
1693
+ */
957
1694
  const INTERVAL_MINUTES = {
958
1695
  "1m": 1,
959
1696
  "3m": 3,
@@ -969,6 +1706,15 @@ const INTERVAL_MINUTES = {
969
1706
  "1d": 1440,
970
1707
  "3d": 4320,
971
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
+ */
972
1718
  const GET_TIMEFRAME_FN = async (symbol, self) => {
973
1719
  self.params.logger.debug("ClientFrame getTimeframe", {
974
1720
  symbol,
@@ -989,19 +1735,69 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
989
1735
  }
990
1736
  return timeframes;
991
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
+ */
992
1749
  class ClientFrame {
993
1750
  constructor(params) {
994
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
+ */
995
1760
  this.getTimeframe = functoolsKit.singleshot(async (symbol) => await GET_TIMEFRAME_FN(symbol, this));
996
1761
  }
997
1762
  }
998
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
+ */
999
1786
  class FrameConnectionService {
1000
1787
  constructor() {
1001
1788
  this.loggerService = inject(TYPES.loggerService);
1002
1789
  this.frameSchemaService = inject(TYPES.frameSchemaService);
1003
1790
  this.methodContextService = inject(TYPES.methodContextService);
1004
- this.getFrame = functoolsKit.memoize((frameName) => `${frameName}`, (frameName) => {
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) => {
1005
1801
  const { endDate, interval, startDate, callbacks } = this.frameSchemaService.get(frameName);
1006
1802
  return new ClientFrame({
1007
1803
  frameName,
@@ -1012,6 +1808,15 @@ class FrameConnectionService {
1012
1808
  callbacks,
1013
1809
  });
1014
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
+ */
1015
1820
  this.getTimeframe = async (symbol) => {
1016
1821
  this.loggerService.log("frameConnectionService getTimeframe", {
1017
1822
  symbol,
@@ -1021,10 +1826,28 @@ class FrameConnectionService {
1021
1826
  }
1022
1827
  }
1023
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
+ */
1024
1837
  class ExchangeGlobalService {
1025
1838
  constructor() {
1026
1839
  this.loggerService = inject(TYPES.loggerService);
1027
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
+ */
1028
1851
  this.getCandles = async (symbol, interval, limit, when, backtest) => {
1029
1852
  this.loggerService.log("exchangeGlobalService getCandles", {
1030
1853
  symbol,
@@ -1041,6 +1864,16 @@ class ExchangeGlobalService {
1041
1864
  backtest,
1042
1865
  });
1043
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
+ */
1044
1877
  this.getNextCandles = async (symbol, interval, limit, when, backtest) => {
1045
1878
  this.loggerService.log("exchangeGlobalService getNextCandles", {
1046
1879
  symbol,
@@ -1057,6 +1890,14 @@ class ExchangeGlobalService {
1057
1890
  backtest,
1058
1891
  });
1059
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
+ */
1060
1901
  this.getAveragePrice = async (symbol, when, backtest) => {
1061
1902
  this.loggerService.log("exchangeGlobalService getAveragePrice", {
1062
1903
  symbol,
@@ -1071,6 +1912,15 @@ class ExchangeGlobalService {
1071
1912
  backtest,
1072
1913
  });
1073
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
+ */
1074
1924
  this.formatPrice = async (symbol, price, when, backtest) => {
1075
1925
  this.loggerService.log("exchangeGlobalService formatPrice", {
1076
1926
  symbol,
@@ -1086,6 +1936,15 @@ class ExchangeGlobalService {
1086
1936
  backtest,
1087
1937
  });
1088
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
+ */
1089
1948
  this.formatQuantity = async (symbol, quantity, when, backtest) => {
1090
1949
  this.loggerService.log("exchangeGlobalService formatQuantity", {
1091
1950
  symbol,
@@ -1104,10 +1963,29 @@ class ExchangeGlobalService {
1104
1963
  }
1105
1964
  }
1106
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
+ */
1107
1974
  class StrategyGlobalService {
1108
1975
  constructor() {
1109
1976
  this.loggerService = inject(TYPES.loggerService);
1110
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
+ */
1111
1989
  this.tick = async (symbol, when, backtest) => {
1112
1990
  this.loggerService.log("strategyGlobalService tick", {
1113
1991
  symbol,
@@ -1122,9 +2000,22 @@ class StrategyGlobalService {
1122
2000
  backtest,
1123
2001
  });
1124
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
+ */
1125
2015
  this.backtest = async (symbol, candles, when, backtest) => {
1126
2016
  this.loggerService.log("strategyGlobalService backtest", {
1127
2017
  symbol,
2018
+ candleCount: candles.length,
1128
2019
  when,
1129
2020
  backtest,
1130
2021
  });
@@ -1139,10 +2030,22 @@ class StrategyGlobalService {
1139
2030
  }
1140
2031
  }
1141
2032
 
2033
+ /**
2034
+ * Global service for frame operations.
2035
+ *
2036
+ * Wraps FrameConnectionService for timeframe generation.
2037
+ * Used internally by BacktestLogicPrivateService.
2038
+ */
1142
2039
  class FrameGlobalService {
1143
2040
  constructor() {
1144
2041
  this.loggerService = inject(TYPES.loggerService);
1145
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
+ */
1146
2049
  this.getTimeframe = async (symbol) => {
1147
2050
  this.loggerService.log("frameGlobalService getTimeframe", {
1148
2051
  symbol,
@@ -1152,61 +2055,247 @@ class FrameGlobalService {
1152
2055
  }
1153
2056
  }
1154
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
+ */
1155
2064
  class ExchangeSchemaService {
1156
2065
  constructor() {
1157
2066
  this.loggerService = inject(TYPES.loggerService);
1158
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
+ */
1159
2075
  this.register = (key, value) => {
1160
- this.loggerService.info(`exchangeSchemaService register`, { key });
2076
+ this.loggerService.log(`exchangeSchemaService register`, { key });
2077
+ this.validateShallow(value);
1161
2078
  this._registry = this._registry.register(key, value);
1162
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
+ */
1163
2117
  this.override = (key, value) => {
1164
- this.loggerService.info(`exchangeSchemaService override`, { key });
2118
+ this.loggerService.log(`exchangeSchemaService override`, { key });
1165
2119
  this._registry = this._registry.override(key, value);
1166
2120
  return this._registry.get(key);
1167
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
+ */
1168
2129
  this.get = (key) => {
1169
- this.loggerService.info(`exchangeSchemaService get`, { key });
2130
+ this.loggerService.log(`exchangeSchemaService get`, { key });
1170
2131
  return this._registry.get(key);
1171
2132
  };
1172
2133
  }
1173
2134
  }
1174
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
+ */
1175
2142
  class StrategySchemaService {
1176
2143
  constructor() {
1177
2144
  this.loggerService = inject(TYPES.loggerService);
1178
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
+ */
1179
2153
  this.register = (key, value) => {
1180
- this.loggerService.info(`strategySchemaService register`, { key });
2154
+ this.loggerService.log(`strategySchemaService register`, { key });
2155
+ this.validateShallow(value);
1181
2156
  this._registry = this._registry.register(key, value);
1182
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
+ */
1183
2191
  this.override = (key, value) => {
1184
- this.loggerService.info(`strategySchemaService override`, { key });
2192
+ this.loggerService.log(`strategySchemaService override`, { key });
1185
2193
  this._registry = this._registry.override(key, value);
1186
2194
  return this._registry.get(key);
1187
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
+ */
1188
2203
  this.get = (key) => {
1189
- this.loggerService.info(`strategySchemaService get`, { key });
2204
+ this.loggerService.log(`strategySchemaService get`, { key });
1190
2205
  return this._registry.get(key);
1191
2206
  };
1192
2207
  }
1193
2208
  }
1194
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
+ */
1195
2216
  class FrameSchemaService {
1196
2217
  constructor() {
2218
+ this.loggerService = inject(TYPES.loggerService);
1197
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
+ };
1198
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
+ */
1199
2257
  register(key, value) {
2258
+ this.loggerService.log(`frameSchemaService register`, { key });
2259
+ this.validateShallow(value);
1200
2260
  this._registry.register(key, value);
1201
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
+ */
1202
2269
  override(key, value) {
2270
+ this.loggerService.log(`frameSchemaService override`, { key });
1203
2271
  this._registry.override(key, value);
1204
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
+ */
1205
2280
  get(key) {
2281
+ this.loggerService.log(`frameSchemaService get`, { key });
1206
2282
  return this._registry.get(key);
1207
2283
  }
1208
2284
  }
1209
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
+ */
1210
2299
  class BacktestLogicPrivateService {
1211
2300
  constructor() {
1212
2301
  this.loggerService = inject(TYPES.loggerService);
@@ -1214,6 +2303,20 @@ class BacktestLogicPrivateService {
1214
2303
  this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
1215
2304
  this.frameGlobalService = inject(TYPES.frameGlobalService);
1216
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
+ */
1217
2320
  async *run(symbol) {
1218
2321
  this.loggerService.log("backtestLogicPrivateService run", {
1219
2322
  symbol,
@@ -1262,11 +2365,49 @@ class BacktestLogicPrivateService {
1262
2365
  }
1263
2366
 
1264
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
+ */
1265
2384
  class LiveLogicPrivateService {
1266
2385
  constructor() {
1267
2386
  this.loggerService = inject(TYPES.loggerService);
1268
2387
  this.strategyGlobalService = inject(TYPES.strategyGlobalService);
1269
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
+ */
1270
2411
  async *run(symbol) {
1271
2412
  this.loggerService.log("liveLogicPrivateService run", {
1272
2413
  symbol,
@@ -1292,10 +2433,44 @@ class LiveLogicPrivateService {
1292
2433
  }
1293
2434
  }
1294
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
+ */
1295
2460
  class BacktestLogicPublicService {
1296
2461
  constructor() {
1297
2462
  this.loggerService = inject(TYPES.loggerService);
1298
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
+ */
1299
2474
  this.run = (symbol, context) => {
1300
2475
  this.loggerService.log("backtestLogicPublicService run", {
1301
2476
  symbol,
@@ -1310,10 +2485,52 @@ class BacktestLogicPublicService {
1310
2485
  }
1311
2486
  }
1312
2487
 
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
+ */
1313
2519
  class LiveLogicPublicService {
1314
2520
  constructor() {
1315
2521
  this.loggerService = inject(TYPES.loggerService);
1316
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
+ */
1317
2534
  this.run = (symbol, context) => {
1318
2535
  this.loggerService.log("liveLogicPublicService run", {
1319
2536
  symbol,
@@ -1328,78 +2545,989 @@ class LiveLogicPublicService {
1328
2545
  }
1329
2546
  }
1330
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
+ */
1331
2555
  class LiveGlobalService {
1332
2556
  constructor() {
1333
2557
  this.loggerService = inject(TYPES.loggerService);
1334
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
+ */
1335
2570
  this.run = (symbol, context) => {
1336
- this.loggerService.log("liveGlobalService run", {
2571
+ this.loggerService.log(METHOD_NAME_RUN$1, {
1337
2572
  symbol,
1338
2573
  context,
1339
2574
  });
2575
+ this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
2576
+ this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
1340
2577
  return this.liveLogicPublicService.run(symbol, context);
1341
2578
  };
1342
2579
  }
1343
2580
  }
1344
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
+ */
1345
2589
  class BacktestGlobalService {
1346
2590
  constructor() {
1347
2591
  this.loggerService = inject(TYPES.loggerService);
1348
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
+ */
1349
2603
  this.run = (symbol, context) => {
1350
- this.loggerService.log("backtestGlobalService run", {
2604
+ this.loggerService.log(METHOD_NAME_RUN, {
1351
2605
  symbol,
1352
2606
  context,
1353
2607
  });
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);
1354
2611
  return this.backtestLogicPublicService.run(symbol, context);
1355
2612
  };
1356
2613
  }
1357
2614
  }
1358
2615
 
1359
- {
1360
- provide(TYPES.loggerService, () => new LoggerService());
1361
- }
1362
- {
1363
- provide(TYPES.executionContextService, () => new ExecutionContextService());
1364
- provide(TYPES.methodContextService, () => new MethodContextService());
1365
- }
1366
- {
1367
- provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
1368
- provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
1369
- provide(TYPES.frameConnectionService, () => new FrameConnectionService());
1370
- }
1371
- {
1372
- provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
1373
- provide(TYPES.strategySchemaService, () => new StrategySchemaService());
1374
- provide(TYPES.frameSchemaService, () => new FrameSchemaService());
1375
- }
1376
- {
1377
- provide(TYPES.exchangeGlobalService, () => new ExchangeGlobalService());
1378
- provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
1379
- provide(TYPES.frameGlobalService, () => new FrameGlobalService());
1380
- provide(TYPES.liveGlobalService, () => new LiveGlobalService());
1381
- provide(TYPES.backtestGlobalService, () => new BacktestGlobalService());
1382
- }
1383
- {
1384
- provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
1385
- provide(TYPES.liveLogicPrivateService, () => new LiveLogicPrivateService());
1386
- }
1387
- {
1388
- provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
1389
- provide(TYPES.liveLogicPublicService, () => new LiveLogicPublicService());
1390
- }
1391
-
1392
- const baseServices = {
1393
- loggerService: inject(TYPES.loggerService),
1394
- };
1395
- const contextServices = {
1396
- executionContextService: inject(TYPES.executionContextService),
1397
- methodContextService: inject(TYPES.methodContextService),
1398
- };
1399
- const connectionServices = {
1400
- exchangeConnectionService: inject(TYPES.exchangeConnectionService),
1401
- strategyConnectionService: inject(TYPES.strategyConnectionService),
1402
- frameConnectionService: inject(TYPES.frameConnectionService),
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,
2802
+ });
2803
+ if (data.action !== "closed") {
2804
+ return;
2805
+ }
2806
+ const storage = this.getStorage(data.strategyName);
2807
+ storage.addSignal(data);
2808
+ };
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,
2826
+ });
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,
2878
+ });
2879
+ this.getStorage.clear(strategyName);
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
+ });
2896
+ }
2897
+ }
2898
+
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 {
3185
+ constructor() {
3186
+ /** Logger service for debug output */
3187
+ this.loggerService = inject(TYPES.loggerService);
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,
3217
+ });
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,
3301
+ });
3302
+ this.getStorage.clear(strategyName);
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
+ });
3319
+ }
3320
+ }
3321
+
3322
+ /**
3323
+ * @class ExchangeValidationService
3324
+ * Service for managing and validating exchange configurations
3325
+ */
3326
+ class ExchangeValidationService {
3327
+ constructor() {
3328
+ /**
3329
+ * @private
3330
+ * @readonly
3331
+ * Injected logger service instance
3332
+ */
3333
+ this.loggerService = inject(TYPES.loggerService);
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`);
3351
+ }
3352
+ this._exchangeMap.set(exchangeName, exchangeSchema);
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
+ });
3371
+ }
3372
+ }
3373
+
3374
+ /**
3375
+ * @class StrategyValidationService
3376
+ * Service for managing and validating strategy configurations
3377
+ */
3378
+ class StrategyValidationService {
3379
+ constructor() {
3380
+ /**
3381
+ * @private
3382
+ * @readonly
3383
+ * Injected logger service instance
3384
+ */
3385
+ this.loggerService = inject(TYPES.loggerService);
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`);
3403
+ }
3404
+ this._strategyMap.set(strategyName, strategySchema);
3405
+ };
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);
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
+ });
3475
+ }
3476
+ }
3477
+
3478
+ {
3479
+ provide(TYPES.loggerService, () => new LoggerService());
3480
+ }
3481
+ {
3482
+ provide(TYPES.executionContextService, () => new ExecutionContextService());
3483
+ provide(TYPES.methodContextService, () => new MethodContextService());
3484
+ }
3485
+ {
3486
+ provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
3487
+ provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
3488
+ provide(TYPES.frameConnectionService, () => new FrameConnectionService());
3489
+ }
3490
+ {
3491
+ provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
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());
3513
+ }
3514
+ {
3515
+ provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
3516
+ provide(TYPES.strategyValidationService, () => new StrategyValidationService());
3517
+ provide(TYPES.frameValidationService, () => new FrameValidationService());
3518
+ }
3519
+
3520
+ const baseServices = {
3521
+ loggerService: inject(TYPES.loggerService),
3522
+ };
3523
+ const contextServices = {
3524
+ executionContextService: inject(TYPES.executionContextService),
3525
+ methodContextService: inject(TYPES.methodContextService),
3526
+ };
3527
+ const connectionServices = {
3528
+ exchangeConnectionService: inject(TYPES.exchangeConnectionService),
3529
+ strategyConnectionService: inject(TYPES.strategyConnectionService),
3530
+ frameConnectionService: inject(TYPES.frameConnectionService),
1403
3531
  };
1404
3532
  const schemaServices = {
1405
3533
  exchangeSchemaService: inject(TYPES.exchangeSchemaService),
@@ -1421,6 +3549,15 @@ const logicPublicServices = {
1421
3549
  backtestLogicPublicService: inject(TYPES.backtestLogicPublicService),
1422
3550
  liveLogicPublicService: inject(TYPES.liveLogicPublicService),
1423
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),
3560
+ };
1424
3561
  const backtest = {
1425
3562
  ...baseServices,
1426
3563
  ...contextServices,
@@ -1429,10 +3566,29 @@ const backtest = {
1429
3566
  ...globalServices,
1430
3567
  ...logicPrivateServices,
1431
3568
  ...logicPublicServices,
3569
+ ...markdownServices,
3570
+ ...validationServices,
1432
3571
  };
1433
3572
  init();
1434
3573
  var backtest$1 = backtest;
1435
3574
 
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
+ */
1436
3592
  async function setLogger(logger) {
1437
3593
  backtest$1.loggerService.setLogger(logger);
1438
3594
  }
@@ -1440,136 +3596,323 @@ async function setLogger(logger) {
1440
3596
  const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
1441
3597
  const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
1442
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
+ */
1443
3633
  function addStrategy(strategySchema) {
1444
3634
  backtest$1.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
1445
3635
  strategySchema,
1446
3636
  });
3637
+ backtest$1.strategyValidationService.addStrategy(strategySchema.strategyName, strategySchema);
1447
3638
  backtest$1.strategySchemaService.register(strategySchema.strategyName, strategySchema);
1448
3639
  }
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
+ */
1449
3675
  function addExchange(exchangeSchema) {
1450
3676
  backtest$1.loggerService.info(ADD_EXCHANGE_METHOD_NAME, {
1451
3677
  exchangeSchema,
1452
3678
  });
3679
+ backtest$1.exchangeValidationService.addExchange(exchangeSchema.exchangeName, exchangeSchema);
1453
3680
  backtest$1.exchangeSchemaService.register(exchangeSchema.exchangeName, exchangeSchema);
1454
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
+ */
1455
3712
  function addFrame(frameSchema) {
1456
3713
  backtest$1.loggerService.info(ADD_FRAME_METHOD_NAME, {
1457
3714
  frameSchema,
1458
3715
  });
3716
+ backtest$1.frameValidationService.addFrame(frameSchema.frameName, frameSchema);
1459
3717
  backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
1460
3718
  }
1461
3719
 
1462
- async function runBacktest(symbol, timeframes) {
1463
- const results = [];
1464
- for (const when of timeframes) {
1465
- const result = await backtest$1.strategyGlobalService.tick(symbol, when, true);
1466
- // Сохраняем только результаты closed
1467
- if (result.action === "closed") {
1468
- results.push(result);
1469
- }
1470
- }
1471
- return {
1472
- symbol,
1473
- results,
1474
- };
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)));
1475
3755
  }
1476
- async function runBacktestGUI(symbol, timeframes) {
1477
- const backtestResult = await runBacktest(symbol, timeframes);
1478
- const { results } = backtestResult;
1479
- // Таблица для терминала
1480
- const table = new Table({
1481
- head: ["#", "Time", "Note", "Price", "Reason", "PNL %"],
1482
- });
1483
- let totalPnl = 0;
1484
- let winCount = 0;
1485
- let lossCount = 0;
1486
- results.forEach((result, index) => {
1487
- if (result.action === "closed") {
1488
- const pnl = result.pnl.pnlPercentage;
1489
- totalPnl += pnl;
1490
- if (pnl > 0)
1491
- winCount++;
1492
- else if (pnl < 0)
1493
- lossCount++;
1494
- const pnlFormatted = pnl > 0 ? `+${pnl.toFixed(2)}%` : `${pnl.toFixed(2)}%`;
1495
- const emoji = pnl > 0 ? "🟢" : pnl < 0 ? "🔴" : "⚪";
1496
- table.push([
1497
- index + 1,
1498
- new Date(result.signal.timestamp).toISOString(),
1499
- result.signal.note,
1500
- result.currentPrice.toFixed(2),
1501
- result.closeReason,
1502
- `${emoji} ${pnlFormatted}`,
1503
- ]);
1504
- }
1505
- });
1506
- // Добавляем статистику
1507
- const closedCount = results.length;
1508
- table.push([]);
1509
- table.push([
1510
- "TOTAL",
1511
- `${closedCount} trades`,
1512
- `Win: ${winCount}`,
1513
- `Loss: ${lossCount}`,
1514
- "-",
1515
- `WR: ${closedCount > 0 ? ((winCount / closedCount) * 100).toFixed(1) : 0}%`,
1516
- `${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`,
1517
- ]);
1518
- console.log("\n");
1519
- console.log(table.toString());
1520
- console.log("\n");
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);
1521
3791
  }
1522
-
1523
- async function reduce(symbol, timeframes, callback, initialValue) {
1524
- let accumulator = initialValue;
1525
- for (let i = 0; i < timeframes.length; i++) {
1526
- const when = timeframes[i];
1527
- accumulator = await callback(accumulator, i, when, symbol);
1528
- }
1529
- return {
1530
- symbol,
1531
- accumulator,
1532
- totalTicks: timeframes.length,
1533
- };
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)));
1534
3815
  }
1535
-
1536
- const instances = new Map();
1537
- function startRun(config) {
1538
- const { symbol, interval } = config;
1539
- // Останавливаем предыдущий инстанс для этого символа
1540
- if (instances.has(symbol)) {
1541
- stopRun(symbol);
1542
- }
1543
- const doWork = functoolsKit.singlerun(async () => {
1544
- const now = new Date();
1545
- const result = await backtest$1.strategyGlobalService.tick(symbol, now, false);
1546
- const instance = instances.get(symbol);
1547
- if (instance) {
1548
- instance.tickCount++;
1549
- }
1550
- await functoolsKit.sleep(interval);
1551
- return result;
1552
- });
1553
- const intervalId = setInterval(doWork, interval);
1554
- instances.set(symbol, {
1555
- config,
1556
- tickCount: 0,
1557
- intervalId,
1558
- doWork,
1559
- });
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);
1560
3840
  }
1561
- function stopRun(symbol) {
1562
- const instance = instances.get(symbol);
1563
- if (instance) {
1564
- clearInterval(instance.intervalId);
1565
- instances.delete(symbol);
1566
- }
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)));
1567
3864
  }
1568
- function stopAll() {
1569
- instances.forEach((instance) => {
1570
- clearInterval(instance.intervalId);
1571
- });
1572
- instances.clear();
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)));
1573
3916
  }
1574
3917
 
1575
3918
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
@@ -1578,6 +3921,23 @@ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
1578
3921
  const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
1579
3922
  const GET_DATE_METHOD_NAME = "exchange.getDate";
1580
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
+ */
1581
3941
  async function getCandles(symbol, interval, limit) {
1582
3942
  backtest$1.loggerService.info(GET_CANDLES_METHOD_NAME, {
1583
3943
  symbol,
@@ -1586,12 +3946,45 @@ async function getCandles(symbol, interval, limit) {
1586
3946
  });
1587
3947
  return await backtest$1.exchangeConnectionService.getCandles(symbol, interval, limit);
1588
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
+ */
1589
3967
  async function getAveragePrice(symbol) {
1590
3968
  backtest$1.loggerService.info(GET_AVERAGE_PRICE_METHOD_NAME, {
1591
3969
  symbol,
1592
3970
  });
1593
3971
  return await backtest$1.exchangeConnectionService.getAveragePrice(symbol);
1594
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
+ */
1595
3988
  async function formatPrice(symbol, price) {
1596
3989
  backtest$1.loggerService.info(FORMAT_PRICE_METHOD_NAME, {
1597
3990
  symbol,
@@ -1599,6 +3992,21 @@ async function formatPrice(symbol, price) {
1599
3992
  });
1600
3993
  return await backtest$1.exchangeConnectionService.formatPrice(symbol, price);
1601
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
+ */
1602
4010
  async function formatQuantity(symbol, quantity) {
1603
4011
  backtest$1.loggerService.info(FORMAT_QUANTITY_METHOD_NAME, {
1604
4012
  symbol,
@@ -1606,11 +4014,40 @@ async function formatQuantity(symbol, quantity) {
1606
4014
  });
1607
4015
  return await backtest$1.exchangeConnectionService.formatQuantity(symbol, quantity);
1608
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
+ */
1609
4031
  async function getDate() {
1610
4032
  backtest$1.loggerService.info(GET_DATE_METHOD_NAME);
1611
4033
  const { when } = backtest$1.executionContextService.context;
1612
4034
  return new Date(when.getTime());
1613
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
+ */
1614
4051
  async function getMode() {
1615
4052
  backtest$1.loggerService.info(GET_MODE_METHOD_NAME);
1616
4053
  const { backtest: bt } = backtest$1.executionContextService.context;
@@ -1618,8 +4055,37 @@ async function getMode() {
1618
4055
  }
1619
4056
 
1620
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
+ */
1621
4080
  class BacktestUtils {
1622
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
+ */
1623
4089
  this.run = (symbol, context) => {
1624
4090
  backtest$1.loggerService.info(BACKTEST_METHOD_NAME_RUN, {
1625
4091
  symbol,
@@ -1627,13 +4093,157 @@ class BacktestUtils {
1627
4093
  });
1628
4094
  return backtest$1.backtestGlobalService.run(symbol, context);
1629
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 = (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
+ };
1630
4180
  }
1631
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
+ */
1632
4200
  const Backtest = new BacktestUtils();
1633
4201
 
1634
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
+ */
1635
4235
  class LiveUtils {
1636
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
+ */
1637
4247
  this.run = (symbol, context) => {
1638
4248
  backtest$1.loggerService.info(LIVE_METHOD_NAME_RUN, {
1639
4249
  symbol,
@@ -1641,8 +4251,111 @@ class LiveUtils {
1641
4251
  });
1642
4252
  return backtest$1.liveGlobalService.run(symbol, context);
1643
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 = (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
+ };
1644
4342
  }
1645
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
+ */
1646
4359
  const Live = new LiveUtils();
1647
4360
 
1648
4361
  exports.Backtest = Backtest;
@@ -1654,17 +4367,18 @@ exports.PersistSignalAdaper = PersistSignalAdaper;
1654
4367
  exports.addExchange = addExchange;
1655
4368
  exports.addFrame = addFrame;
1656
4369
  exports.addStrategy = addStrategy;
1657
- exports.backtest = backtest;
1658
4370
  exports.formatPrice = formatPrice;
1659
4371
  exports.formatQuantity = formatQuantity;
1660
4372
  exports.getAveragePrice = getAveragePrice;
1661
4373
  exports.getCandles = getCandles;
1662
4374
  exports.getDate = getDate;
1663
4375
  exports.getMode = getMode;
1664
- exports.reduce = reduce;
1665
- exports.runBacktest = runBacktest;
1666
- exports.runBacktestGUI = runBacktestGUI;
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;
1667
4384
  exports.setLogger = setLogger;
1668
- exports.startRun = startRun;
1669
- exports.stopAll = stopAll;
1670
- exports.stopRun = stopRun;