backtest-kit 1.0.4 → 1.1.0

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