backtest-kit 1.1.3 → 1.1.5

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.
package/build/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, randomString, Subject, ToolRegistry, sleep, str, queued } from 'functools-kit';
3
+ import { memoize, makeExtendable, singleshot, getErrorMessage, not, trycatch, retry, Subject, randomString, errorData, ToolRegistry, sleep, str, queued } from 'functools-kit';
4
4
  import fs, { mkdir, writeFile } from 'fs/promises';
5
5
  import path, { join } from 'path';
6
6
  import crypto from 'crypto';
@@ -1102,6 +1102,37 @@ class PersistSignalUtils {
1102
1102
  */
1103
1103
  const PersistSignalAdaper = new PersistSignalUtils();
1104
1104
 
1105
+ /**
1106
+ * Global signal emitter for all trading events (live + backtest).
1107
+ * Emits all signal events regardless of execution mode.
1108
+ */
1109
+ const signalEmitter = new Subject();
1110
+ /**
1111
+ * Live trading signal emitter.
1112
+ * Emits only signals from live trading execution.
1113
+ */
1114
+ const signalLiveEmitter = new Subject();
1115
+ /**
1116
+ * Backtest signal emitter.
1117
+ * Emits only signals from backtest execution.
1118
+ */
1119
+ const signalBacktestEmitter = new Subject();
1120
+ /**
1121
+ * Error emitter for background execution errors.
1122
+ * Emits errors caught in background tasks (Live.background, Backtest.background).
1123
+ */
1124
+ const errorEmitter = new Subject();
1125
+ /**
1126
+ * Done emitter for background execution completion.
1127
+ * Emits when background tasks complete (Live.background, Backtest.background).
1128
+ */
1129
+ const doneEmitter = new Subject();
1130
+ /**
1131
+ * Progress emitter for backtest execution progress.
1132
+ * Emits progress updates during backtest execution.
1133
+ */
1134
+ const progressEmitter = new Subject();
1135
+
1105
1136
  const INTERVAL_MINUTES$1 = {
1106
1137
  "1m": 1,
1107
1138
  "3m": 3,
@@ -1153,6 +1184,9 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1153
1184
  }
1154
1185
  };
1155
1186
  const GET_SIGNAL_FN = trycatch(async (self) => {
1187
+ if (self._isStopped) {
1188
+ return null;
1189
+ }
1156
1190
  const currentTime = self.params.execution.context.when.getTime();
1157
1191
  {
1158
1192
  const intervalMinutes = INTERVAL_MINUTES$1[self.params.interval];
@@ -1181,6 +1215,13 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1181
1215
  return signalRow;
1182
1216
  }, {
1183
1217
  defaultValue: null,
1218
+ fallback: (error) => {
1219
+ backtest$1.loggerService.warn("ClientStrategy exception thrown", {
1220
+ error: errorData(error),
1221
+ message: getErrorMessage(error),
1222
+ });
1223
+ errorEmitter.next(error);
1224
+ },
1184
1225
  });
1185
1226
  const GET_AVG_PRICE_FN = (candles) => {
1186
1227
  const sumPriceVolume = candles.reduce((acc, c) => {
@@ -1239,6 +1280,7 @@ const WAIT_FOR_INIT_FN = async (self) => {
1239
1280
  class ClientStrategy {
1240
1281
  constructor(params) {
1241
1282
  this.params = params;
1283
+ this._isStopped = false;
1242
1284
  this._pendingSignal = null;
1243
1285
  this._lastSignalTimestamp = null;
1244
1286
  /**
@@ -1561,34 +1603,32 @@ class ClientStrategy {
1561
1603
  }
1562
1604
  return result;
1563
1605
  }
1606
+ /**
1607
+ * Stops the strategy from generating new signals.
1608
+ *
1609
+ * Sets internal flag to prevent getSignal from being called.
1610
+ * Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
1611
+ *
1612
+ * Use case: Graceful shutdown in live trading without forcing position closure.
1613
+ *
1614
+ * @returns Promise that resolves immediately when stop flag is set
1615
+ *
1616
+ * @example
1617
+ * ```typescript
1618
+ * // In Live.background() cancellation
1619
+ * await strategy.stop();
1620
+ * // Existing signal will continue until natural close
1621
+ * ```
1622
+ */
1623
+ stop() {
1624
+ this.params.logger.debug("ClientStrategy stop", {
1625
+ hasPendingSignal: this._pendingSignal !== null,
1626
+ });
1627
+ this._isStopped = true;
1628
+ return Promise.resolve();
1629
+ }
1564
1630
  }
1565
1631
 
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
- * Done emitter for background execution completion.
1588
- * Emits when background tasks complete (Live.background, Backtest.background).
1589
- */
1590
- const doneEmitter = new Subject();
1591
-
1592
1632
  /**
1593
1633
  * Connection service routing strategy operations to correct ClientStrategy instance.
1594
1634
  *
@@ -1687,6 +1727,36 @@ class StrategyConnectionService {
1687
1727
  }
1688
1728
  return tick;
1689
1729
  };
1730
+ /**
1731
+ * Stops the specified strategy from generating new signals.
1732
+ *
1733
+ * Delegates to ClientStrategy.stop() which sets internal flag to prevent
1734
+ * getSignal from being called on subsequent ticks.
1735
+ *
1736
+ * @param strategyName - Name of strategy to stop
1737
+ * @returns Promise that resolves when stop flag is set
1738
+ */
1739
+ this.stop = async (strategyName) => {
1740
+ this.loggerService.log("strategyConnectionService stop", {
1741
+ strategyName,
1742
+ });
1743
+ const strategy = this.getStrategy(strategyName);
1744
+ await strategy.stop();
1745
+ };
1746
+ /**
1747
+ * Clears the memoized ClientStrategy instance from cache.
1748
+ *
1749
+ * Forces re-initialization of strategy on next getStrategy call.
1750
+ * Useful for resetting strategy state or releasing resources.
1751
+ *
1752
+ * @param strategyName - Name of strategy to clear from cache
1753
+ */
1754
+ this.clear = async (strategyName) => {
1755
+ this.loggerService.log("strategyConnectionService clear", {
1756
+ strategyName,
1757
+ });
1758
+ this.getStrategy.clear(strategyName);
1759
+ };
1690
1760
  }
1691
1761
  }
1692
1762
 
@@ -2030,6 +2100,35 @@ class StrategyGlobalService {
2030
2100
  backtest,
2031
2101
  });
2032
2102
  };
2103
+ /**
2104
+ * Stops the strategy from generating new signals.
2105
+ *
2106
+ * Delegates to StrategyConnectionService.stop() to set internal flag.
2107
+ * Does not require execution context.
2108
+ *
2109
+ * @param strategyName - Name of strategy to stop
2110
+ * @returns Promise that resolves when stop flag is set
2111
+ */
2112
+ this.stop = async (strategyName) => {
2113
+ this.loggerService.log("strategyGlobalService stop", {
2114
+ strategyName,
2115
+ });
2116
+ return await this.strategyConnectionService.stop(strategyName);
2117
+ };
2118
+ /**
2119
+ * Clears the memoized ClientStrategy instance from cache.
2120
+ *
2121
+ * Delegates to StrategyConnectionService.clear() to remove strategy from cache.
2122
+ * Forces re-initialization of strategy on next operation.
2123
+ *
2124
+ * @param strategyName - Name of strategy to clear from cache
2125
+ */
2126
+ this.clear = async (strategyName) => {
2127
+ this.loggerService.log("strategyGlobalService clear", {
2128
+ strategyName,
2129
+ });
2130
+ return await this.strategyConnectionService.clear(strategyName);
2131
+ };
2033
2132
  }
2034
2133
  }
2035
2134
 
@@ -2260,7 +2359,7 @@ class FrameSchemaService {
2260
2359
  register(key, value) {
2261
2360
  this.loggerService.log(`frameSchemaService register`, { key });
2262
2361
  this.validateShallow(value);
2263
- this._registry.register(key, value);
2362
+ this._registry = this._registry.register(key, value);
2264
2363
  }
2265
2364
  /**
2266
2365
  * Overrides an existing frame schema with partial updates.
@@ -2271,7 +2370,8 @@ class FrameSchemaService {
2271
2370
  */
2272
2371
  override(key, value) {
2273
2372
  this.loggerService.log(`frameSchemaService override`, { key });
2274
- this._registry.override(key, value);
2373
+ this._registry = this._registry.override(key, value);
2374
+ return this._registry.get(key);
2275
2375
  }
2276
2376
  /**
2277
2377
  * Retrieves a frame schema by name.
@@ -2305,6 +2405,7 @@ class BacktestLogicPrivateService {
2305
2405
  this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2306
2406
  this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
2307
2407
  this.frameGlobalService = inject(TYPES.frameGlobalService);
2408
+ this.methodContextService = inject(TYPES.methodContextService);
2308
2409
  }
2309
2410
  /**
2310
2411
  * Runs backtest for a symbol, streaming closed signals as async generator.
@@ -2325,9 +2426,21 @@ class BacktestLogicPrivateService {
2325
2426
  symbol,
2326
2427
  });
2327
2428
  const timeframes = await this.frameGlobalService.getTimeframe(symbol);
2429
+ const totalFrames = timeframes.length;
2328
2430
  let i = 0;
2329
2431
  while (i < timeframes.length) {
2330
2432
  const when = timeframes[i];
2433
+ // Emit progress event if context is available
2434
+ {
2435
+ await progressEmitter.next({
2436
+ exchangeName: this.methodContextService.context.exchangeName,
2437
+ strategyName: this.methodContextService.context.strategyName,
2438
+ symbol,
2439
+ totalFrames,
2440
+ processedFrames: i,
2441
+ progress: totalFrames > 0 ? i / totalFrames : 0,
2442
+ });
2443
+ }
2331
2444
  const result = await this.strategyGlobalService.tick(symbol, when, true);
2332
2445
  // Если сигнал открыт, вызываем backtest
2333
2446
  if (result.action === "opened") {
@@ -2364,6 +2477,17 @@ class BacktestLogicPrivateService {
2364
2477
  }
2365
2478
  i++;
2366
2479
  }
2480
+ // Emit final progress event (100%)
2481
+ {
2482
+ await progressEmitter.next({
2483
+ exchangeName: this.methodContextService.context.exchangeName,
2484
+ strategyName: this.methodContextService.context.strategyName,
2485
+ symbol,
2486
+ totalFrames,
2487
+ processedFrames: totalFrames,
2488
+ progress: 1.0,
2489
+ });
2490
+ }
2367
2491
  }
2368
2492
  }
2369
2493
 
@@ -2718,10 +2842,17 @@ let ReportStorage$1 = class ReportStorage {
2718
2842
  return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2719
2843
  }
2720
2844
  const header = columns$1.map((col) => col.label);
2845
+ const separator = columns$1.map(() => "---");
2721
2846
  const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2722
- const tableData = [header, ...rows];
2723
- const table = str.table(tableData);
2724
- return str.newline(`# Backtest Report: ${strategyName}`, "", `Total signals: ${this._signalList.length}`, "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
2847
+ const tableData = [header, separator, ...rows];
2848
+ const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2849
+ // Calculate statistics
2850
+ const totalSignals = this._signalList.length;
2851
+ const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2852
+ const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2853
+ const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2854
+ const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2855
+ return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${totalSignals}`, `**Closed signals:** ${totalSignals}`, `**Win rate:** ${((winCount / totalSignals) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`, `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`, `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`);
2725
2856
  }
2726
2857
  /**
2727
2858
  * Saves strategy report to disk.
@@ -3116,9 +3247,10 @@ class ReportStorage {
3116
3247
  return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3117
3248
  }
3118
3249
  const header = columns.map((col) => col.label);
3250
+ const separator = columns.map(() => "---");
3119
3251
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3120
- const tableData = [header, ...rows];
3121
- const table = str.table(tableData);
3252
+ const tableData = [header, separator, ...rows];
3253
+ const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3122
3254
  // Calculate statistics
3123
3255
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3124
3256
  const totalClosed = closedEvents.length;
@@ -3127,11 +3259,14 @@ class ReportStorage {
3127
3259
  const avgPnl = totalClosed > 0
3128
3260
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3129
3261
  : 0;
3130
- return str.newline(`# Live Trading Report: ${strategyName}`, "", `Total events: ${this._eventList.length}`, `Closed signals: ${totalClosed}`, totalClosed > 0
3131
- ? `Win rate: ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3262
+ const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3263
+ return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3264
+ ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3265
+ : "", totalClosed > 0
3266
+ ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3132
3267
  : "", totalClosed > 0
3133
- ? `Average PNL: ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3134
- : "", "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
3268
+ ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3269
+ : "");
3135
3270
  }
3136
3271
  /**
3137
3272
  * Saves strategy report to disk.
@@ -3371,6 +3506,15 @@ class ExchangeValidationService {
3371
3506
  }
3372
3507
  return true;
3373
3508
  });
3509
+ /**
3510
+ * Returns a list of all registered exchange schemas
3511
+ * @public
3512
+ * @returns Array of exchange schemas with their configurations
3513
+ */
3514
+ this.list = async () => {
3515
+ this.loggerService.log("exchangeValidationService list");
3516
+ return Array.from(this._exchangeMap.values());
3517
+ };
3374
3518
  }
3375
3519
  }
3376
3520
 
@@ -3423,6 +3567,15 @@ class StrategyValidationService {
3423
3567
  }
3424
3568
  return true;
3425
3569
  });
3570
+ /**
3571
+ * Returns a list of all registered strategy schemas
3572
+ * @public
3573
+ * @returns Array of strategy schemas with their configurations
3574
+ */
3575
+ this.list = async () => {
3576
+ this.loggerService.log("strategyValidationService list");
3577
+ return Array.from(this._strategyMap.values());
3578
+ };
3426
3579
  }
3427
3580
  }
3428
3581
 
@@ -3475,6 +3628,15 @@ class FrameValidationService {
3475
3628
  }
3476
3629
  return true;
3477
3630
  });
3631
+ /**
3632
+ * Returns a list of all registered frame schemas
3633
+ * @public
3634
+ * @returns Array of frame schemas with their configurations
3635
+ */
3636
+ this.list = async () => {
3637
+ this.loggerService.log("frameValidationService list");
3638
+ return Array.from(this._frameMap.values());
3639
+ };
3478
3640
  }
3479
3641
  }
3480
3642
 
@@ -3720,6 +3882,102 @@ function addFrame(frameSchema) {
3720
3882
  backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
3721
3883
  }
3722
3884
 
3885
+ const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
3886
+ const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
3887
+ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
3888
+ /**
3889
+ * Returns a list of all registered exchange schemas.
3890
+ *
3891
+ * Retrieves all exchanges that have been registered via addExchange().
3892
+ * Useful for debugging, documentation, or building dynamic UIs.
3893
+ *
3894
+ * @returns Array of exchange schemas with their configurations
3895
+ *
3896
+ * @example
3897
+ * ```typescript
3898
+ * import { listExchanges, addExchange } from "backtest-kit";
3899
+ *
3900
+ * addExchange({
3901
+ * exchangeName: "binance",
3902
+ * note: "Binance cryptocurrency exchange",
3903
+ * getCandles: async (symbol, interval, since, limit) => [...],
3904
+ * formatPrice: async (symbol, price) => price.toFixed(2),
3905
+ * formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
3906
+ * });
3907
+ *
3908
+ * const exchanges = listExchanges();
3909
+ * console.log(exchanges);
3910
+ * // [{ exchangeName: "binance", note: "Binance cryptocurrency exchange", ... }]
3911
+ * ```
3912
+ */
3913
+ async function listExchanges() {
3914
+ backtest$1.loggerService.log(LIST_EXCHANGES_METHOD_NAME);
3915
+ return await backtest$1.exchangeValidationService.list();
3916
+ }
3917
+ /**
3918
+ * Returns a list of all registered strategy schemas.
3919
+ *
3920
+ * Retrieves all strategies that have been registered via addStrategy().
3921
+ * Useful for debugging, documentation, or building dynamic UIs.
3922
+ *
3923
+ * @returns Array of strategy schemas with their configurations
3924
+ *
3925
+ * @example
3926
+ * ```typescript
3927
+ * import { listStrategies, addStrategy } from "backtest-kit";
3928
+ *
3929
+ * addStrategy({
3930
+ * strategyName: "my-strategy",
3931
+ * note: "Simple moving average crossover strategy",
3932
+ * interval: "5m",
3933
+ * getSignal: async (symbol) => ({
3934
+ * position: "long",
3935
+ * priceOpen: 50000,
3936
+ * priceTakeProfit: 51000,
3937
+ * priceStopLoss: 49000,
3938
+ * minuteEstimatedTime: 60,
3939
+ * }),
3940
+ * });
3941
+ *
3942
+ * const strategies = listStrategies();
3943
+ * console.log(strategies);
3944
+ * // [{ strategyName: "my-strategy", note: "Simple moving average...", ... }]
3945
+ * ```
3946
+ */
3947
+ async function listStrategies() {
3948
+ backtest$1.loggerService.log(LIST_STRATEGIES_METHOD_NAME);
3949
+ return await backtest$1.strategyValidationService.list();
3950
+ }
3951
+ /**
3952
+ * Returns a list of all registered frame schemas.
3953
+ *
3954
+ * Retrieves all frames that have been registered via addFrame().
3955
+ * Useful for debugging, documentation, or building dynamic UIs.
3956
+ *
3957
+ * @returns Array of frame schemas with their configurations
3958
+ *
3959
+ * @example
3960
+ * ```typescript
3961
+ * import { listFrames, addFrame } from "backtest-kit";
3962
+ *
3963
+ * addFrame({
3964
+ * frameName: "1d-backtest",
3965
+ * note: "One day backtest period for testing",
3966
+ * interval: "1m",
3967
+ * startDate: new Date("2024-01-01T00:00:00Z"),
3968
+ * endDate: new Date("2024-01-02T00:00:00Z"),
3969
+ * });
3970
+ *
3971
+ * const frames = listFrames();
3972
+ * console.log(frames);
3973
+ * // [{ frameName: "1d-backtest", note: "One day backtest...", ... }]
3974
+ * ```
3975
+ */
3976
+ async function listFrames() {
3977
+ backtest$1.loggerService.log(LIST_FRAMES_METHOD_NAME);
3978
+ return await backtest$1.frameValidationService.list();
3979
+ }
3980
+
3723
3981
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
3724
3982
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
3725
3983
  const LISTEN_SIGNAL_LIVE_METHOD_NAME = "event.listenSignalLive";
@@ -3729,6 +3987,7 @@ const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce"
3729
3987
  const LISTEN_ERROR_METHOD_NAME = "event.listenError";
3730
3988
  const LISTEN_DONE_METHOD_NAME = "event.listenDone";
3731
3989
  const LISTEN_DONE_ONCE_METHOD_NAME = "event.listenDoneOnce";
3990
+ const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
3732
3991
  /**
3733
3992
  * Subscribes to all signal events with queued async processing.
3734
3993
  *
@@ -3984,6 +4243,40 @@ function listenDoneOnce(filterFn, fn) {
3984
4243
  backtest$1.loggerService.log(LISTEN_DONE_ONCE_METHOD_NAME);
3985
4244
  return doneEmitter.filter(filterFn).once(fn);
3986
4245
  }
4246
+ /**
4247
+ * Subscribes to backtest progress events with queued async processing.
4248
+ *
4249
+ * Emits during Backtest.background() execution to track progress.
4250
+ * Events are processed sequentially in order received, even if callback is async.
4251
+ * Uses queued wrapper to prevent concurrent execution of the callback.
4252
+ *
4253
+ * @param fn - Callback function to handle progress events
4254
+ * @returns Unsubscribe function to stop listening to events
4255
+ *
4256
+ * @example
4257
+ * ```typescript
4258
+ * import { listenProgress, Backtest } from "backtest-kit";
4259
+ *
4260
+ * const unsubscribe = listenProgress((event) => {
4261
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
4262
+ * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
4263
+ * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
4264
+ * });
4265
+ *
4266
+ * Backtest.background("BTCUSDT", {
4267
+ * strategyName: "my-strategy",
4268
+ * exchangeName: "binance",
4269
+ * frameName: "1d-backtest"
4270
+ * });
4271
+ *
4272
+ * // Later: stop listening
4273
+ * unsubscribe();
4274
+ * ```
4275
+ */
4276
+ function listenProgress(fn) {
4277
+ backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
4278
+ return progressEmitter.subscribe(queued(async (event) => fn(event)));
4279
+ }
3987
4280
 
3988
4281
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
3989
4282
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -4162,6 +4455,7 @@ class BacktestUtils {
4162
4455
  context,
4163
4456
  });
4164
4457
  backtest$1.backtestMarkdownService.clear(context.strategyName);
4458
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4165
4459
  return backtest$1.backtestGlobalService.run(symbol, context);
4166
4460
  };
4167
4461
  /**
@@ -4190,14 +4484,9 @@ class BacktestUtils {
4190
4484
  symbol,
4191
4485
  context,
4192
4486
  });
4193
- const iterator = this.run(symbol, context);
4194
4487
  let isStopped = false;
4195
4488
  const task = async () => {
4196
- while (true) {
4197
- const { done } = await iterator.next();
4198
- if (done) {
4199
- break;
4200
- }
4489
+ for await (const _ of this.run(symbol, context)) {
4201
4490
  if (isStopped) {
4202
4491
  break;
4203
4492
  }
@@ -4211,6 +4500,7 @@ class BacktestUtils {
4211
4500
  };
4212
4501
  task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4213
4502
  return () => {
4503
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4214
4504
  isStopped = true;
4215
4505
  };
4216
4506
  };
@@ -4327,6 +4617,7 @@ class LiveUtils {
4327
4617
  context,
4328
4618
  });
4329
4619
  backtest$1.liveMarkdownService.clear(context.strategyName);
4620
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4330
4621
  return backtest$1.liveGlobalService.run(symbol, context);
4331
4622
  };
4332
4623
  /**
@@ -4355,19 +4646,10 @@ class LiveUtils {
4355
4646
  symbol,
4356
4647
  context,
4357
4648
  });
4358
- const iterator = this.run(symbol, context);
4359
4649
  let isStopped = false;
4360
- let lastValue = null;
4361
4650
  const task = async () => {
4362
- while (true) {
4363
- const { value, done } = await iterator.next();
4364
- if (value) {
4365
- lastValue = value;
4366
- }
4367
- if (done) {
4368
- break;
4369
- }
4370
- if (lastValue?.action === "closed" && isStopped) {
4651
+ for await (const signal of this.run(symbol, context)) {
4652
+ if (signal?.action === "closed" && isStopped) {
4371
4653
  break;
4372
4654
  }
4373
4655
  }
@@ -4380,6 +4662,7 @@ class LiveUtils {
4380
4662
  };
4381
4663
  task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4382
4664
  return () => {
4665
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4383
4666
  isStopped = true;
4384
4667
  };
4385
4668
  };
@@ -4442,4 +4725,4 @@ class LiveUtils {
4442
4725
  */
4443
4726
  const Live = new LiveUtils();
4444
4727
 
4445
- export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listenDone, listenDoneOnce, listenError, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };
4728
+ export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listStrategies, listenDone, listenDoneOnce, listenError, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "rollup -c",
42
+ "test": "npm run build && node ./test/index.mjs",
42
43
  "build:docs": "rimraf docs && mkdir docs && node ./scripts/dts-docs.cjs ./types.d.ts ./docs",
43
44
  "docs:gpt": "npm run build && node ./scripts/gpt-docs.mjs",
44
45
  "docs:uml": "npm run build && node ./scripts/uml.mjs",