backtest-kit 1.1.3 โ†’ 1.1.4

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/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
  - ๐Ÿ”Œ **Flexible Architecture** - Plug your own exchanges and strategies
21
21
  - ๐Ÿ“ **Markdown Reports** - Auto-generated trading reports with statistics (win rate, avg PNL)
22
22
  - ๐Ÿ›‘ **Graceful Shutdown** - Live.background() waits for open positions to close before stopping
23
+ - ๐Ÿ’‰ **Strategy Dependency Injection** - addStrategy() enables DI pattern for trading strategies
24
+ - ๐Ÿงช **Comprehensive Test Coverage** - 30+ unit tests covering validation, PNL, callbacks, reports, and event system
23
25
 
24
26
  ## Installation
25
27
 
@@ -829,23 +831,6 @@ src/
829
831
 
830
832
  ## Advanced Examples
831
833
 
832
- ### Custom Persistence Adapter
833
-
834
- ```typescript
835
- import { PersistSignalAdaper, PersistBase } from "backtest-kit";
836
-
837
- class RedisPersist extends PersistBase {
838
- async readValue(entityId) {
839
- return JSON.parse(await redis.get(entityId));
840
- }
841
- async writeValue(entityId, entity) {
842
- await redis.set(entityId, JSON.stringify(entity));
843
- }
844
- }
845
-
846
- PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
847
- ```
848
-
849
834
  ### Multi-Symbol Live Trading
850
835
 
851
836
  ```typescript
package/build/index.cjs CHANGED
@@ -1104,6 +1104,32 @@ class PersistSignalUtils {
1104
1104
  */
1105
1105
  const PersistSignalAdaper = new PersistSignalUtils();
1106
1106
 
1107
+ /**
1108
+ * Global signal emitter for all trading events (live + backtest).
1109
+ * Emits all signal events regardless of execution mode.
1110
+ */
1111
+ const signalEmitter = new functoolsKit.Subject();
1112
+ /**
1113
+ * Live trading signal emitter.
1114
+ * Emits only signals from live trading execution.
1115
+ */
1116
+ const signalLiveEmitter = new functoolsKit.Subject();
1117
+ /**
1118
+ * Backtest signal emitter.
1119
+ * Emits only signals from backtest execution.
1120
+ */
1121
+ const signalBacktestEmitter = new functoolsKit.Subject();
1122
+ /**
1123
+ * Error emitter for background execution errors.
1124
+ * Emits errors caught in background tasks (Live.background, Backtest.background).
1125
+ */
1126
+ const errorEmitter = new functoolsKit.Subject();
1127
+ /**
1128
+ * Done emitter for background execution completion.
1129
+ * Emits when background tasks complete (Live.background, Backtest.background).
1130
+ */
1131
+ const doneEmitter = new functoolsKit.Subject();
1132
+
1107
1133
  const INTERVAL_MINUTES$1 = {
1108
1134
  "1m": 1,
1109
1135
  "3m": 3,
@@ -1155,6 +1181,9 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1155
1181
  }
1156
1182
  };
1157
1183
  const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1184
+ if (self._isStopped) {
1185
+ return null;
1186
+ }
1158
1187
  const currentTime = self.params.execution.context.when.getTime();
1159
1188
  {
1160
1189
  const intervalMinutes = INTERVAL_MINUTES$1[self.params.interval];
@@ -1183,6 +1212,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1183
1212
  return signalRow;
1184
1213
  }, {
1185
1214
  defaultValue: null,
1215
+ fallback: (error) => {
1216
+ backtest$1.loggerService.warn("ClientStrategy exception thrown", {
1217
+ error: functoolsKit.errorData(error),
1218
+ message: functoolsKit.getErrorMessage(error),
1219
+ });
1220
+ errorEmitter.next(error);
1221
+ },
1186
1222
  });
1187
1223
  const GET_AVG_PRICE_FN = (candles) => {
1188
1224
  const sumPriceVolume = candles.reduce((acc, c) => {
@@ -1241,6 +1277,7 @@ const WAIT_FOR_INIT_FN = async (self) => {
1241
1277
  class ClientStrategy {
1242
1278
  constructor(params) {
1243
1279
  this.params = params;
1280
+ this._isStopped = false;
1244
1281
  this._pendingSignal = null;
1245
1282
  this._lastSignalTimestamp = null;
1246
1283
  /**
@@ -1563,34 +1600,32 @@ class ClientStrategy {
1563
1600
  }
1564
1601
  return result;
1565
1602
  }
1603
+ /**
1604
+ * Stops the strategy from generating new signals.
1605
+ *
1606
+ * Sets internal flag to prevent getSignal from being called.
1607
+ * Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
1608
+ *
1609
+ * Use case: Graceful shutdown in live trading without forcing position closure.
1610
+ *
1611
+ * @returns Promise that resolves immediately when stop flag is set
1612
+ *
1613
+ * @example
1614
+ * ```typescript
1615
+ * // In Live.background() cancellation
1616
+ * await strategy.stop();
1617
+ * // Existing signal will continue until natural close
1618
+ * ```
1619
+ */
1620
+ stop() {
1621
+ this.params.logger.debug("ClientStrategy stop", {
1622
+ hasPendingSignal: this._pendingSignal !== null,
1623
+ });
1624
+ this._isStopped = true;
1625
+ return Promise.resolve();
1626
+ }
1566
1627
  }
1567
1628
 
1568
- /**
1569
- * Global signal emitter for all trading events (live + backtest).
1570
- * Emits all signal events regardless of execution mode.
1571
- */
1572
- const signalEmitter = new functoolsKit.Subject();
1573
- /**
1574
- * Live trading signal emitter.
1575
- * Emits only signals from live trading execution.
1576
- */
1577
- const signalLiveEmitter = new functoolsKit.Subject();
1578
- /**
1579
- * Backtest signal emitter.
1580
- * Emits only signals from backtest execution.
1581
- */
1582
- const signalBacktestEmitter = new functoolsKit.Subject();
1583
- /**
1584
- * Error emitter for background execution errors.
1585
- * Emits errors caught in background tasks (Live.background, Backtest.background).
1586
- */
1587
- const errorEmitter = new functoolsKit.Subject();
1588
- /**
1589
- * Done emitter for background execution completion.
1590
- * Emits when background tasks complete (Live.background, Backtest.background).
1591
- */
1592
- const doneEmitter = new functoolsKit.Subject();
1593
-
1594
1629
  /**
1595
1630
  * Connection service routing strategy operations to correct ClientStrategy instance.
1596
1631
  *
@@ -1689,6 +1724,36 @@ class StrategyConnectionService {
1689
1724
  }
1690
1725
  return tick;
1691
1726
  };
1727
+ /**
1728
+ * Stops the specified strategy from generating new signals.
1729
+ *
1730
+ * Delegates to ClientStrategy.stop() which sets internal flag to prevent
1731
+ * getSignal from being called on subsequent ticks.
1732
+ *
1733
+ * @param strategyName - Name of strategy to stop
1734
+ * @returns Promise that resolves when stop flag is set
1735
+ */
1736
+ this.stop = async (strategyName) => {
1737
+ this.loggerService.log("strategyConnectionService stop", {
1738
+ strategyName,
1739
+ });
1740
+ const strategy = this.getStrategy(strategyName);
1741
+ await strategy.stop();
1742
+ };
1743
+ /**
1744
+ * Clears the memoized ClientStrategy instance from cache.
1745
+ *
1746
+ * Forces re-initialization of strategy on next getStrategy call.
1747
+ * Useful for resetting strategy state or releasing resources.
1748
+ *
1749
+ * @param strategyName - Name of strategy to clear from cache
1750
+ */
1751
+ this.clear = async (strategyName) => {
1752
+ this.loggerService.log("strategyConnectionService clear", {
1753
+ strategyName,
1754
+ });
1755
+ this.getStrategy.clear(strategyName);
1756
+ };
1692
1757
  }
1693
1758
  }
1694
1759
 
@@ -2032,6 +2097,35 @@ class StrategyGlobalService {
2032
2097
  backtest,
2033
2098
  });
2034
2099
  };
2100
+ /**
2101
+ * Stops the strategy from generating new signals.
2102
+ *
2103
+ * Delegates to StrategyConnectionService.stop() to set internal flag.
2104
+ * Does not require execution context.
2105
+ *
2106
+ * @param strategyName - Name of strategy to stop
2107
+ * @returns Promise that resolves when stop flag is set
2108
+ */
2109
+ this.stop = async (strategyName) => {
2110
+ this.loggerService.log("strategyGlobalService stop", {
2111
+ strategyName,
2112
+ });
2113
+ return await this.strategyConnectionService.stop(strategyName);
2114
+ };
2115
+ /**
2116
+ * Clears the memoized ClientStrategy instance from cache.
2117
+ *
2118
+ * Delegates to StrategyConnectionService.clear() to remove strategy from cache.
2119
+ * Forces re-initialization of strategy on next operation.
2120
+ *
2121
+ * @param strategyName - Name of strategy to clear from cache
2122
+ */
2123
+ this.clear = async (strategyName) => {
2124
+ this.loggerService.log("strategyGlobalService clear", {
2125
+ strategyName,
2126
+ });
2127
+ return await this.strategyConnectionService.clear(strategyName);
2128
+ };
2035
2129
  }
2036
2130
  }
2037
2131
 
@@ -2262,7 +2356,7 @@ class FrameSchemaService {
2262
2356
  register(key, value) {
2263
2357
  this.loggerService.log(`frameSchemaService register`, { key });
2264
2358
  this.validateShallow(value);
2265
- this._registry.register(key, value);
2359
+ this._registry = this._registry.register(key, value);
2266
2360
  }
2267
2361
  /**
2268
2362
  * Overrides an existing frame schema with partial updates.
@@ -2273,7 +2367,8 @@ class FrameSchemaService {
2273
2367
  */
2274
2368
  override(key, value) {
2275
2369
  this.loggerService.log(`frameSchemaService override`, { key });
2276
- this._registry.override(key, value);
2370
+ this._registry = this._registry.override(key, value);
2371
+ return this._registry.get(key);
2277
2372
  }
2278
2373
  /**
2279
2374
  * Retrieves a frame schema by name.
@@ -2720,10 +2815,17 @@ let ReportStorage$1 = class ReportStorage {
2720
2815
  return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2721
2816
  }
2722
2817
  const header = columns$1.map((col) => col.label);
2818
+ const separator = columns$1.map(() => "---");
2723
2819
  const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2724
- const tableData = [header, ...rows];
2725
- const table = functoolsKit.str.table(tableData);
2726
- return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", `Total signals: ${this._signalList.length}`, "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
2820
+ const tableData = [header, separator, ...rows];
2821
+ const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2822
+ // Calculate statistics
2823
+ const totalSignals = this._signalList.length;
2824
+ const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2825
+ const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2826
+ const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2827
+ const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2828
+ return functoolsKit.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)}%`);
2727
2829
  }
2728
2830
  /**
2729
2831
  * Saves strategy report to disk.
@@ -3118,9 +3220,10 @@ class ReportStorage {
3118
3220
  return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3119
3221
  }
3120
3222
  const header = columns.map((col) => col.label);
3223
+ const separator = columns.map(() => "---");
3121
3224
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3122
- const tableData = [header, ...rows];
3123
- const table = functoolsKit.str.table(tableData);
3225
+ const tableData = [header, separator, ...rows];
3226
+ const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3124
3227
  // Calculate statistics
3125
3228
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3126
3229
  const totalClosed = closedEvents.length;
@@ -3129,11 +3232,14 @@ class ReportStorage {
3129
3232
  const avgPnl = totalClosed > 0
3130
3233
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3131
3234
  : 0;
3132
- return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", `Total events: ${this._eventList.length}`, `Closed signals: ${totalClosed}`, totalClosed > 0
3133
- ? `Win rate: ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3235
+ const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3236
+ return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3237
+ ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3134
3238
  : "", totalClosed > 0
3135
- ? `Average PNL: ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3136
- : "", "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
3239
+ ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3240
+ : "", totalClosed > 0
3241
+ ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3242
+ : "");
3137
3243
  }
3138
3244
  /**
3139
3245
  * Saves strategy report to disk.
@@ -4164,6 +4270,7 @@ class BacktestUtils {
4164
4270
  context,
4165
4271
  });
4166
4272
  backtest$1.backtestMarkdownService.clear(context.strategyName);
4273
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4167
4274
  return backtest$1.backtestGlobalService.run(symbol, context);
4168
4275
  };
4169
4276
  /**
@@ -4192,14 +4299,9 @@ class BacktestUtils {
4192
4299
  symbol,
4193
4300
  context,
4194
4301
  });
4195
- const iterator = this.run(symbol, context);
4196
4302
  let isStopped = false;
4197
4303
  const task = async () => {
4198
- while (true) {
4199
- const { done } = await iterator.next();
4200
- if (done) {
4201
- break;
4202
- }
4304
+ for await (const _ of this.run(symbol, context)) {
4203
4305
  if (isStopped) {
4204
4306
  break;
4205
4307
  }
@@ -4213,6 +4315,7 @@ class BacktestUtils {
4213
4315
  };
4214
4316
  task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
4215
4317
  return () => {
4318
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4216
4319
  isStopped = true;
4217
4320
  };
4218
4321
  };
@@ -4329,6 +4432,7 @@ class LiveUtils {
4329
4432
  context,
4330
4433
  });
4331
4434
  backtest$1.liveMarkdownService.clear(context.strategyName);
4435
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4332
4436
  return backtest$1.liveGlobalService.run(symbol, context);
4333
4437
  };
4334
4438
  /**
@@ -4357,19 +4461,10 @@ class LiveUtils {
4357
4461
  symbol,
4358
4462
  context,
4359
4463
  });
4360
- const iterator = this.run(symbol, context);
4361
4464
  let isStopped = false;
4362
- let lastValue = null;
4363
4465
  const task = async () => {
4364
- while (true) {
4365
- const { value, done } = await iterator.next();
4366
- if (value) {
4367
- lastValue = value;
4368
- }
4369
- if (done) {
4370
- break;
4371
- }
4372
- if (lastValue?.action === "closed" && isStopped) {
4466
+ for await (const signal of this.run(symbol, context)) {
4467
+ if (signal?.action === "closed" && isStopped) {
4373
4468
  break;
4374
4469
  }
4375
4470
  }
@@ -4382,6 +4477,7 @@ class LiveUtils {
4382
4477
  };
4383
4478
  task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
4384
4479
  return () => {
4480
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4385
4481
  isStopped = true;
4386
4482
  };
4387
4483
  };
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,32 @@ 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
+
1105
1131
  const INTERVAL_MINUTES$1 = {
1106
1132
  "1m": 1,
1107
1133
  "3m": 3,
@@ -1153,6 +1179,9 @@ const VALIDATE_SIGNAL_FN = (signal) => {
1153
1179
  }
1154
1180
  };
1155
1181
  const GET_SIGNAL_FN = trycatch(async (self) => {
1182
+ if (self._isStopped) {
1183
+ return null;
1184
+ }
1156
1185
  const currentTime = self.params.execution.context.when.getTime();
1157
1186
  {
1158
1187
  const intervalMinutes = INTERVAL_MINUTES$1[self.params.interval];
@@ -1181,6 +1210,13 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1181
1210
  return signalRow;
1182
1211
  }, {
1183
1212
  defaultValue: null,
1213
+ fallback: (error) => {
1214
+ backtest$1.loggerService.warn("ClientStrategy exception thrown", {
1215
+ error: errorData(error),
1216
+ message: getErrorMessage(error),
1217
+ });
1218
+ errorEmitter.next(error);
1219
+ },
1184
1220
  });
1185
1221
  const GET_AVG_PRICE_FN = (candles) => {
1186
1222
  const sumPriceVolume = candles.reduce((acc, c) => {
@@ -1239,6 +1275,7 @@ const WAIT_FOR_INIT_FN = async (self) => {
1239
1275
  class ClientStrategy {
1240
1276
  constructor(params) {
1241
1277
  this.params = params;
1278
+ this._isStopped = false;
1242
1279
  this._pendingSignal = null;
1243
1280
  this._lastSignalTimestamp = null;
1244
1281
  /**
@@ -1561,34 +1598,32 @@ class ClientStrategy {
1561
1598
  }
1562
1599
  return result;
1563
1600
  }
1601
+ /**
1602
+ * Stops the strategy from generating new signals.
1603
+ *
1604
+ * Sets internal flag to prevent getSignal from being called.
1605
+ * Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
1606
+ *
1607
+ * Use case: Graceful shutdown in live trading without forcing position closure.
1608
+ *
1609
+ * @returns Promise that resolves immediately when stop flag is set
1610
+ *
1611
+ * @example
1612
+ * ```typescript
1613
+ * // In Live.background() cancellation
1614
+ * await strategy.stop();
1615
+ * // Existing signal will continue until natural close
1616
+ * ```
1617
+ */
1618
+ stop() {
1619
+ this.params.logger.debug("ClientStrategy stop", {
1620
+ hasPendingSignal: this._pendingSignal !== null,
1621
+ });
1622
+ this._isStopped = true;
1623
+ return Promise.resolve();
1624
+ }
1564
1625
  }
1565
1626
 
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
1627
  /**
1593
1628
  * Connection service routing strategy operations to correct ClientStrategy instance.
1594
1629
  *
@@ -1687,6 +1722,36 @@ class StrategyConnectionService {
1687
1722
  }
1688
1723
  return tick;
1689
1724
  };
1725
+ /**
1726
+ * Stops the specified strategy from generating new signals.
1727
+ *
1728
+ * Delegates to ClientStrategy.stop() which sets internal flag to prevent
1729
+ * getSignal from being called on subsequent ticks.
1730
+ *
1731
+ * @param strategyName - Name of strategy to stop
1732
+ * @returns Promise that resolves when stop flag is set
1733
+ */
1734
+ this.stop = async (strategyName) => {
1735
+ this.loggerService.log("strategyConnectionService stop", {
1736
+ strategyName,
1737
+ });
1738
+ const strategy = this.getStrategy(strategyName);
1739
+ await strategy.stop();
1740
+ };
1741
+ /**
1742
+ * Clears the memoized ClientStrategy instance from cache.
1743
+ *
1744
+ * Forces re-initialization of strategy on next getStrategy call.
1745
+ * Useful for resetting strategy state or releasing resources.
1746
+ *
1747
+ * @param strategyName - Name of strategy to clear from cache
1748
+ */
1749
+ this.clear = async (strategyName) => {
1750
+ this.loggerService.log("strategyConnectionService clear", {
1751
+ strategyName,
1752
+ });
1753
+ this.getStrategy.clear(strategyName);
1754
+ };
1690
1755
  }
1691
1756
  }
1692
1757
 
@@ -2030,6 +2095,35 @@ class StrategyGlobalService {
2030
2095
  backtest,
2031
2096
  });
2032
2097
  };
2098
+ /**
2099
+ * Stops the strategy from generating new signals.
2100
+ *
2101
+ * Delegates to StrategyConnectionService.stop() to set internal flag.
2102
+ * Does not require execution context.
2103
+ *
2104
+ * @param strategyName - Name of strategy to stop
2105
+ * @returns Promise that resolves when stop flag is set
2106
+ */
2107
+ this.stop = async (strategyName) => {
2108
+ this.loggerService.log("strategyGlobalService stop", {
2109
+ strategyName,
2110
+ });
2111
+ return await this.strategyConnectionService.stop(strategyName);
2112
+ };
2113
+ /**
2114
+ * Clears the memoized ClientStrategy instance from cache.
2115
+ *
2116
+ * Delegates to StrategyConnectionService.clear() to remove strategy from cache.
2117
+ * Forces re-initialization of strategy on next operation.
2118
+ *
2119
+ * @param strategyName - Name of strategy to clear from cache
2120
+ */
2121
+ this.clear = async (strategyName) => {
2122
+ this.loggerService.log("strategyGlobalService clear", {
2123
+ strategyName,
2124
+ });
2125
+ return await this.strategyConnectionService.clear(strategyName);
2126
+ };
2033
2127
  }
2034
2128
  }
2035
2129
 
@@ -2260,7 +2354,7 @@ class FrameSchemaService {
2260
2354
  register(key, value) {
2261
2355
  this.loggerService.log(`frameSchemaService register`, { key });
2262
2356
  this.validateShallow(value);
2263
- this._registry.register(key, value);
2357
+ this._registry = this._registry.register(key, value);
2264
2358
  }
2265
2359
  /**
2266
2360
  * Overrides an existing frame schema with partial updates.
@@ -2271,7 +2365,8 @@ class FrameSchemaService {
2271
2365
  */
2272
2366
  override(key, value) {
2273
2367
  this.loggerService.log(`frameSchemaService override`, { key });
2274
- this._registry.override(key, value);
2368
+ this._registry = this._registry.override(key, value);
2369
+ return this._registry.get(key);
2275
2370
  }
2276
2371
  /**
2277
2372
  * Retrieves a frame schema by name.
@@ -2718,10 +2813,17 @@ let ReportStorage$1 = class ReportStorage {
2718
2813
  return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2719
2814
  }
2720
2815
  const header = columns$1.map((col) => col.label);
2816
+ const separator = columns$1.map(() => "---");
2721
2817
  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()}*`);
2818
+ const tableData = [header, separator, ...rows];
2819
+ const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2820
+ // Calculate statistics
2821
+ const totalSignals = this._signalList.length;
2822
+ const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2823
+ const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2824
+ const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2825
+ const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2826
+ 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
2827
  }
2726
2828
  /**
2727
2829
  * Saves strategy report to disk.
@@ -3116,9 +3218,10 @@ class ReportStorage {
3116
3218
  return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3117
3219
  }
3118
3220
  const header = columns.map((col) => col.label);
3221
+ const separator = columns.map(() => "---");
3119
3222
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3120
- const tableData = [header, ...rows];
3121
- const table = str.table(tableData);
3223
+ const tableData = [header, separator, ...rows];
3224
+ const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3122
3225
  // Calculate statistics
3123
3226
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3124
3227
  const totalClosed = closedEvents.length;
@@ -3127,11 +3230,14 @@ class ReportStorage {
3127
3230
  const avgPnl = totalClosed > 0
3128
3231
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3129
3232
  : 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)`
3233
+ const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3234
+ return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3235
+ ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3132
3236
  : "", totalClosed > 0
3133
- ? `Average PNL: ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3134
- : "", "", table, "", "", `*Generated: ${new Date().toISOString()}*`);
3237
+ ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3238
+ : "", totalClosed > 0
3239
+ ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3240
+ : "");
3135
3241
  }
3136
3242
  /**
3137
3243
  * Saves strategy report to disk.
@@ -4162,6 +4268,7 @@ class BacktestUtils {
4162
4268
  context,
4163
4269
  });
4164
4270
  backtest$1.backtestMarkdownService.clear(context.strategyName);
4271
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4165
4272
  return backtest$1.backtestGlobalService.run(symbol, context);
4166
4273
  };
4167
4274
  /**
@@ -4190,14 +4297,9 @@ class BacktestUtils {
4190
4297
  symbol,
4191
4298
  context,
4192
4299
  });
4193
- const iterator = this.run(symbol, context);
4194
4300
  let isStopped = false;
4195
4301
  const task = async () => {
4196
- while (true) {
4197
- const { done } = await iterator.next();
4198
- if (done) {
4199
- break;
4200
- }
4302
+ for await (const _ of this.run(symbol, context)) {
4201
4303
  if (isStopped) {
4202
4304
  break;
4203
4305
  }
@@ -4211,6 +4313,7 @@ class BacktestUtils {
4211
4313
  };
4212
4314
  task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4213
4315
  return () => {
4316
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4214
4317
  isStopped = true;
4215
4318
  };
4216
4319
  };
@@ -4327,6 +4430,7 @@ class LiveUtils {
4327
4430
  context,
4328
4431
  });
4329
4432
  backtest$1.liveMarkdownService.clear(context.strategyName);
4433
+ backtest$1.strategyGlobalService.clear(context.strategyName);
4330
4434
  return backtest$1.liveGlobalService.run(symbol, context);
4331
4435
  };
4332
4436
  /**
@@ -4355,19 +4459,10 @@ class LiveUtils {
4355
4459
  symbol,
4356
4460
  context,
4357
4461
  });
4358
- const iterator = this.run(symbol, context);
4359
4462
  let isStopped = false;
4360
- let lastValue = null;
4361
4463
  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) {
4464
+ for await (const signal of this.run(symbol, context)) {
4465
+ if (signal?.action === "closed" && isStopped) {
4371
4466
  break;
4372
4467
  }
4373
4468
  }
@@ -4380,6 +4475,7 @@ class LiveUtils {
4380
4475
  };
4381
4476
  task().catch((error) => errorEmitter.next(new Error(getErrorMessage(error))));
4382
4477
  return () => {
4478
+ backtest$1.strategyGlobalService.stop(context.strategyName);
4383
4479
  isStopped = true;
4384
4480
  };
4385
4481
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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",
package/types.d.ts CHANGED
@@ -536,6 +536,27 @@ interface IStrategy {
536
536
  * @returns Promise resolving to closed result (always completes signal)
537
537
  */
538
538
  backtest: (candles: ICandleData[]) => Promise<IStrategyBacktestResult>;
539
+ /**
540
+ * Stops the strategy from generating new signals.
541
+ *
542
+ * Sets internal flag to prevent getSignal from being called on subsequent ticks.
543
+ * Does NOT force-close active pending signals - they continue monitoring until natural closure (TP/SL/time_expired).
544
+ *
545
+ * Use case: Graceful shutdown in live trading mode without abandoning open positions.
546
+ *
547
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
548
+ * @returns Promise that resolves immediately when stop flag is set
549
+ *
550
+ * @example
551
+ * ```typescript
552
+ * // Graceful shutdown in Live.background() cancellation
553
+ * const cancel = await Live.background("BTCUSDT", { ... });
554
+ *
555
+ * // Later: stop new signals, let existing ones close naturally
556
+ * await cancel();
557
+ * ```
558
+ */
559
+ stop: (symbol: string) => Promise<void>;
539
560
  }
540
561
  /**
541
562
  * Unique strategy identifier.
@@ -1737,6 +1758,25 @@ declare class StrategyConnectionService implements IStrategy {
1737
1758
  * @returns Promise resolving to backtest result (signal or idle)
1738
1759
  */
1739
1760
  backtest: (candles: ICandleData[]) => Promise<IStrategyBacktestResult>;
1761
+ /**
1762
+ * Stops the specified strategy from generating new signals.
1763
+ *
1764
+ * Delegates to ClientStrategy.stop() which sets internal flag to prevent
1765
+ * getSignal from being called on subsequent ticks.
1766
+ *
1767
+ * @param strategyName - Name of strategy to stop
1768
+ * @returns Promise that resolves when stop flag is set
1769
+ */
1770
+ stop: (strategyName: StrategyName) => Promise<void>;
1771
+ /**
1772
+ * Clears the memoized ClientStrategy instance from cache.
1773
+ *
1774
+ * Forces re-initialization of strategy on next getStrategy call.
1775
+ * Useful for resetting strategy state or releasing resources.
1776
+ *
1777
+ * @param strategyName - Name of strategy to clear from cache
1778
+ */
1779
+ clear: (strategyName: StrategyName) => Promise<void>;
1740
1780
  }
1741
1781
 
1742
1782
  /**
@@ -1912,6 +1952,25 @@ declare class StrategyGlobalService {
1912
1952
  * @returns Closed signal result with PNL
1913
1953
  */
1914
1954
  backtest: (symbol: string, candles: ICandleData[], when: Date, backtest: boolean) => Promise<IStrategyBacktestResult>;
1955
+ /**
1956
+ * Stops the strategy from generating new signals.
1957
+ *
1958
+ * Delegates to StrategyConnectionService.stop() to set internal flag.
1959
+ * Does not require execution context.
1960
+ *
1961
+ * @param strategyName - Name of strategy to stop
1962
+ * @returns Promise that resolves when stop flag is set
1963
+ */
1964
+ stop: (strategyName: StrategyName) => Promise<void>;
1965
+ /**
1966
+ * Clears the memoized ClientStrategy instance from cache.
1967
+ *
1968
+ * Delegates to StrategyConnectionService.clear() to remove strategy from cache.
1969
+ * Forces re-initialization of strategy on next operation.
1970
+ *
1971
+ * @param strategyName - Name of strategy to clear from cache
1972
+ */
1973
+ clear: (strategyName: StrategyName) => Promise<void>;
1915
1974
  }
1916
1975
 
1917
1976
  /**
@@ -2066,7 +2125,7 @@ declare class FrameSchemaService {
2066
2125
  * @param value - Partial schema updates
2067
2126
  * @throws Error if frame name doesn't exist
2068
2127
  */
2069
- override(key: FrameName, value: Partial<IFrameSchema>): void;
2128
+ override(key: FrameName, value: Partial<IFrameSchema>): IFrameSchema;
2070
2129
  /**
2071
2130
  * Retrieves a frame schema by name.
2072
2131
  *