backtest-kit 9.0.2 → 9.0.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/build/index.cjs CHANGED
@@ -1458,7 +1458,7 @@ class PersistRiskInstance {
1458
1458
  *
1459
1459
  * @returns Promise resolving to positions (empty array if none persisted)
1460
1460
  */
1461
- async readPositionData() {
1461
+ async readPositionData(_when) {
1462
1462
  if (await this._storage.hasValue(PersistRiskInstance.STORAGE_KEY)) {
1463
1463
  return await this._storage.readValue(PersistRiskInstance.STORAGE_KEY);
1464
1464
  }
@@ -1468,9 +1468,10 @@ class PersistRiskInstance {
1468
1468
  * Writes the positions array using the fixed STORAGE_KEY.
1469
1469
  *
1470
1470
  * @param riskRow - Position entries to persist
1471
+ * @param when - Logical timestamp (reserved for API consistency; not used)
1471
1472
  * @returns Promise that resolves when write is complete
1472
1473
  */
1473
- async writePositionData(riskRow) {
1474
+ async writePositionData(riskRow, _when) {
1474
1475
  await this._storage.writeValue(PersistRiskInstance.STORAGE_KEY, riskRow);
1475
1476
  }
1476
1477
  }
@@ -1495,12 +1496,12 @@ class PersistRiskDummyInstance {
1495
1496
  * Always returns empty positions array.
1496
1497
  * @returns Promise resolving to []
1497
1498
  */
1498
- async readPositionData() { return []; }
1499
+ async readPositionData(_when) { return []; }
1499
1500
  /**
1500
1501
  * No-op write (discards positions).
1501
1502
  * @returns Promise that resolves immediately
1502
1503
  */
1503
- async writePositionData(_riskRow) { }
1504
+ async writePositionData(_riskRow, _when) { }
1504
1505
  }
1505
1506
  /**
1506
1507
  * Utility class for managing risk active positions persistence.
@@ -1530,15 +1531,16 @@ class PersistRiskUtils {
1530
1531
  *
1531
1532
  * @param riskName - Risk profile identifier
1532
1533
  * @param exchangeName - Exchange identifier
1534
+ * @param when - Logical timestamp at which the read is happening (reserved for API consistency)
1533
1535
  * @returns Promise resolving to position entries (empty array if none)
1534
1536
  */
1535
- this.readPositionData = async (riskName, exchangeName) => {
1537
+ this.readPositionData = async (riskName, exchangeName, when) => {
1536
1538
  LOGGER_SERVICE$7.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1537
1539
  const key = `${riskName}:${exchangeName}`;
1538
1540
  const isInitial = !this.getRiskStorage.has(key);
1539
1541
  const instance = this.getRiskStorage(riskName, exchangeName);
1540
1542
  await instance.waitForInit(isInitial);
1541
- return instance.readPositionData();
1543
+ return instance.readPositionData(when);
1542
1544
  };
1543
1545
  /**
1544
1546
  * Writes active positions for the given risk context.
@@ -1547,15 +1549,16 @@ class PersistRiskUtils {
1547
1549
  * @param riskRow - Position entries to persist
1548
1550
  * @param riskName - Risk profile identifier
1549
1551
  * @param exchangeName - Exchange identifier
1552
+ * @param when - Logical timestamp this write belongs to (reserved for API consistency)
1550
1553
  * @returns Promise that resolves when write is complete
1551
1554
  */
1552
- this.writePositionData = async (riskRow, riskName, exchangeName) => {
1555
+ this.writePositionData = async (riskRow, riskName, exchangeName, when) => {
1553
1556
  LOGGER_SERVICE$7.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1554
1557
  const key = `${riskName}:${exchangeName}`;
1555
1558
  const isInitial = !this.getRiskStorage.has(key);
1556
1559
  const instance = this.getRiskStorage(riskName, exchangeName);
1557
1560
  await instance.waitForInit(isInitial);
1558
- return instance.writePositionData(riskRow);
1561
+ return instance.writePositionData(riskRow, when);
1559
1562
  };
1560
1563
  }
1561
1564
  /**
@@ -1849,7 +1852,7 @@ class PersistPartialInstance {
1849
1852
  * @param signalId - Signal identifier
1850
1853
  * @returns Promise resolving to partial data record (empty object if not found)
1851
1854
  */
1852
- async readPartialData(signalId) {
1855
+ async readPartialData(signalId, _when) {
1853
1856
  if (await this._storage.hasValue(signalId)) {
1854
1857
  return await this._storage.readValue(signalId);
1855
1858
  }
@@ -1860,9 +1863,10 @@ class PersistPartialInstance {
1860
1863
  *
1861
1864
  * @param data - Partial data record to persist
1862
1865
  * @param signalId - Signal identifier
1866
+ * @param when - Logical timestamp (reserved for API consistency; not used)
1863
1867
  * @returns Promise that resolves when write is complete
1864
1868
  */
1865
- async writePartialData(data, signalId) {
1869
+ async writePartialData(data, signalId, _when) {
1866
1870
  await this._storage.writeValue(signalId, data);
1867
1871
  }
1868
1872
  }
@@ -1885,12 +1889,12 @@ class PersistPartialDummyInstance {
1885
1889
  * Always returns empty partial data record.
1886
1890
  * @returns Promise resolving to {}
1887
1891
  */
1888
- async readPartialData(_signalId) { return {}; }
1892
+ async readPartialData(_signalId, _when) { return {}; }
1889
1893
  /**
1890
1894
  * No-op write (discards partial data).
1891
1895
  * @returns Promise that resolves immediately
1892
1896
  */
1893
- async writePartialData(_data, _signalId) { }
1897
+ async writePartialData(_data, _signalId, _when) { }
1894
1898
  }
1895
1899
  /**
1896
1900
  * Utility class for managing partial profit/loss levels persistence.
@@ -1923,15 +1927,16 @@ class PersistPartialUtils {
1923
1927
  * @param strategyName - Strategy identifier
1924
1928
  * @param signalId - Signal identifier
1925
1929
  * @param exchangeName - Exchange identifier
1930
+ * @param when - Logical timestamp at which the read is happening (reserved for API consistency)
1926
1931
  * @returns Promise resolving to partial data record (empty object if none)
1927
1932
  */
1928
- this.readPartialData = async (symbol, strategyName, signalId, exchangeName) => {
1933
+ this.readPartialData = async (symbol, strategyName, signalId, exchangeName, when) => {
1929
1934
  LOGGER_SERVICE$7.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1930
1935
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1931
1936
  const isInitial = !this.getPartialStorage.has(key);
1932
1937
  const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
1933
1938
  await instance.waitForInit(isInitial);
1934
- return instance.readPartialData(signalId);
1939
+ return instance.readPartialData(signalId, when);
1935
1940
  };
1936
1941
  /**
1937
1942
  * Writes partial data for the given context and signalId.
@@ -1942,15 +1947,16 @@ class PersistPartialUtils {
1942
1947
  * @param strategyName - Strategy identifier
1943
1948
  * @param signalId - Signal identifier
1944
1949
  * @param exchangeName - Exchange identifier
1950
+ * @param when - Logical timestamp this write belongs to (reserved for API consistency)
1945
1951
  * @returns Promise that resolves when write is complete
1946
1952
  */
1947
- this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName) => {
1953
+ this.writePartialData = async (partialData, symbol, strategyName, signalId, exchangeName, when) => {
1948
1954
  LOGGER_SERVICE$7.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1949
1955
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1950
1956
  const isInitial = !this.getPartialStorage.has(key);
1951
1957
  const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
1952
1958
  await instance.waitForInit(isInitial);
1953
- return instance.writePartialData(partialData, signalId);
1959
+ return instance.writePartialData(partialData, signalId, when);
1954
1960
  };
1955
1961
  }
1956
1962
  /**
@@ -2049,7 +2055,7 @@ class PersistBreakevenInstance {
2049
2055
  * @param signalId - Signal identifier
2050
2056
  * @returns Promise resolving to breakeven data record (empty object if not found)
2051
2057
  */
2052
- async readBreakevenData(signalId) {
2058
+ async readBreakevenData(signalId, _when) {
2053
2059
  if (await this._storage.hasValue(signalId)) {
2054
2060
  return await this._storage.readValue(signalId);
2055
2061
  }
@@ -2060,9 +2066,10 @@ class PersistBreakevenInstance {
2060
2066
  *
2061
2067
  * @param data - Breakeven data record to persist
2062
2068
  * @param signalId - Signal identifier
2069
+ * @param when - Logical timestamp (reserved for API consistency; not used)
2063
2070
  * @returns Promise that resolves when write is complete
2064
2071
  */
2065
- async writeBreakevenData(data, signalId) {
2072
+ async writeBreakevenData(data, signalId, _when) {
2066
2073
  await this._storage.writeValue(signalId, data);
2067
2074
  }
2068
2075
  }
@@ -2085,12 +2092,12 @@ class PersistBreakevenDummyInstance {
2085
2092
  * Always returns empty breakeven data record.
2086
2093
  * @returns Promise resolving to {}
2087
2094
  */
2088
- async readBreakevenData(_signalId) { return {}; }
2095
+ async readBreakevenData(_signalId, _when) { return {}; }
2089
2096
  /**
2090
2097
  * No-op write (discards breakeven data).
2091
2098
  * @returns Promise that resolves immediately
2092
2099
  */
2093
- async writeBreakevenData(_data, _signalId) { }
2100
+ async writeBreakevenData(_data, _signalId, _when) { }
2094
2101
  }
2095
2102
  /**
2096
2103
  * Persistence utility class for breakeven state management.
@@ -2143,15 +2150,16 @@ class PersistBreakevenUtils {
2143
2150
  * @param strategyName - Strategy identifier
2144
2151
  * @param signalId - Signal identifier
2145
2152
  * @param exchangeName - Exchange identifier
2153
+ * @param when - Logical timestamp at which the read is happening (reserved for API consistency)
2146
2154
  * @returns Promise resolving to breakeven data record (empty object if none)
2147
2155
  */
2148
- this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName) => {
2156
+ this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName, when) => {
2149
2157
  LOGGER_SERVICE$7.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
2150
2158
  const key = `${symbol}:${strategyName}:${exchangeName}`;
2151
2159
  const isInitial = !this.getBreakevenStorage.has(key);
2152
2160
  const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
2153
2161
  await instance.waitForInit(isInitial);
2154
- return instance.readBreakevenData(signalId);
2162
+ return instance.readBreakevenData(signalId, when);
2155
2163
  };
2156
2164
  /**
2157
2165
  * Writes breakeven data for the given context and signalId.
@@ -2162,15 +2170,16 @@ class PersistBreakevenUtils {
2162
2170
  * @param strategyName - Strategy identifier
2163
2171
  * @param signalId - Signal identifier
2164
2172
  * @param exchangeName - Exchange identifier
2173
+ * @param when - Logical timestamp this write belongs to (reserved for API consistency)
2165
2174
  * @returns Promise that resolves when write is complete
2166
2175
  */
2167
- this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName) => {
2176
+ this.writeBreakevenData = async (breakevenData, symbol, strategyName, signalId, exchangeName, when) => {
2168
2177
  LOGGER_SERVICE$7.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
2169
2178
  const key = `${symbol}:${strategyName}:${exchangeName}`;
2170
2179
  const isInitial = !this.getBreakevenStorage.has(key);
2171
2180
  const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
2172
2181
  await instance.waitForInit(isInitial);
2173
- return instance.writeBreakevenData(breakevenData, signalId);
2182
+ return instance.writeBreakevenData(breakevenData, signalId, when);
2174
2183
  };
2175
2184
  }
2176
2185
  /**
@@ -14156,57 +14165,10 @@ const get = (object, path) => {
14156
14165
  return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
14157
14166
  };
14158
14167
 
14159
- const MS_PER_MINUTE$6 = 60000;
14160
- const INTERVAL_MINUTES$6 = {
14161
- "1m": 1,
14162
- "3m": 3,
14163
- "5m": 5,
14164
- "15m": 15,
14165
- "30m": 30,
14166
- "1h": 60,
14167
- "2h": 120,
14168
- "4h": 240,
14169
- "6h": 360,
14170
- "8h": 480,
14171
- "1d": 1440,
14172
- };
14173
- /**
14174
- * Aligns timestamp down to the nearest interval boundary.
14175
- * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
14176
- *
14177
- * Candle timestamp convention:
14178
- * - Candle timestamp = openTime (when candle opens)
14179
- * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
14180
- *
14181
- * Adapter contract:
14182
- * - Adapter must return candles with timestamp = openTime
14183
- * - First returned candle.timestamp must equal aligned since
14184
- * - Adapter must return exactly `limit` candles
14185
- *
14186
- * @param date - Date to align
14187
- * @param interval - Candle interval (e.g., "1m", "15m", "1h")
14188
- * @returns New Date aligned down to interval boundary
14189
- */
14190
- const alignToInterval = (date, interval) => {
14191
- const minutes = INTERVAL_MINUTES$6[interval];
14192
- if (minutes === undefined) {
14193
- throw new Error(`alignToInterval: unknown interval=${interval}`);
14194
- }
14195
- const intervalMs = minutes * MS_PER_MINUTE$6;
14196
- return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
14197
- };
14198
-
14199
14168
  /** Used to prevent race confition between concurent strategies */
14200
14169
  const RISK_LOCK = new Lock();
14201
14170
  /** Symbol indicating that positions need to be fetched from persistence */
14202
14171
  const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
14203
- /** Get timestamp from execution context or fallback to aligned current time */
14204
- const GET_CONTEXT_TIMESTAMP_FN = (self) => {
14205
- if (ExecutionContextService.hasContext()) {
14206
- return self.params.execution.context.when.getTime();
14207
- }
14208
- return alignToInterval(new Date(), "1m").getTime();
14209
- };
14210
14172
  /** Zero PNL constant for scheduled signals (which don't have priceOpen or PNL yet) */
14211
14173
  const ZERO_PNL = { pnlPercentage: 0, priceOpen: 0, priceClose: 0, pnlCost: 0, pnlEntries: 0 };
14212
14174
  /**
@@ -14332,7 +14294,7 @@ const CALL_ALLOWED_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, par
14332
14294
  *
14333
14295
  * In backtest mode, initializes with empty Map. In live mode, reads from persist storage.
14334
14296
  */
14335
- const WAIT_FOR_INIT_FN$3 = async (self) => {
14297
+ const WAIT_FOR_INIT_FN$3 = async (when, self) => {
14336
14298
  self.params.logger.debug("ClientRisk waitForInit", {
14337
14299
  backtest: self.params.backtest,
14338
14300
  });
@@ -14340,7 +14302,7 @@ const WAIT_FOR_INIT_FN$3 = async (self) => {
14340
14302
  self._activePositions = new Map();
14341
14303
  return;
14342
14304
  }
14343
- const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName, self.params.exchangeName);
14305
+ const persistedPositions = await PersistRiskAdapter.readPositionData(self.params.riskName, self.params.exchangeName, when);
14344
14306
  self._activePositions = new Map(persistedPositions);
14345
14307
  };
14346
14308
  /**
@@ -14369,7 +14331,7 @@ class ClientRisk {
14369
14331
  * Uses singleshot pattern to ensure initialization happens exactly once.
14370
14332
  * Skips persistence in backtest mode.
14371
14333
  */
14372
- this.waitForInit = functoolsKit.singleshot(async () => await WAIT_FOR_INIT_FN$3(this));
14334
+ this.waitForInit = functoolsKit.singleshot(async (when) => await WAIT_FOR_INIT_FN$3(when, this));
14373
14335
  /**
14374
14336
  * Checks if a signal should be allowed based on risk limits.
14375
14337
  *
@@ -14391,11 +14353,15 @@ class ClientRisk {
14391
14353
  });
14392
14354
  await RISK_LOCK.acquireLock();
14393
14355
  try {
14356
+ const timestamp = await this.params.time.getTimestamp(params.symbol, {
14357
+ strategyName: params.strategyName,
14358
+ exchangeName: params.exchangeName,
14359
+ frameName: params.frameName,
14360
+ }, this.params.backtest);
14394
14361
  if (this._activePositions === POSITION_NEED_FETCH) {
14395
- await this.waitForInit();
14362
+ await this.waitForInit(new Date(timestamp));
14396
14363
  }
14397
14364
  const riskMap = this._activePositions;
14398
- const timestamp = GET_CONTEXT_TIMESTAMP_FN(this);
14399
14365
  const payload = {
14400
14366
  ...params,
14401
14367
  currentSignal: TO_RISK_SIGNAL(params.currentSignal, params.currentPrice, timestamp),
@@ -14498,14 +14464,14 @@ class ClientRisk {
14498
14464
  * Persists current active positions to disk.
14499
14465
  * Skips in backtest mode.
14500
14466
  */
14501
- async _updatePositions() {
14467
+ async _updatePositions(when) {
14502
14468
  if (this.params.backtest) {
14503
14469
  return;
14504
14470
  }
14505
14471
  if (this._activePositions === POSITION_NEED_FETCH) {
14506
- await this.waitForInit();
14472
+ await this.waitForInit(when);
14507
14473
  }
14508
- await PersistRiskAdapter.writePositionData(Array.from(this._activePositions), this.params.riskName, this.params.exchangeName);
14474
+ await PersistRiskAdapter.writePositionData(Array.from(this._activePositions), this.params.riskName, this.params.exchangeName, when);
14509
14475
  }
14510
14476
  /**
14511
14477
  * Registers a new opened signal.
@@ -14520,8 +14486,9 @@ class ClientRisk {
14520
14486
  });
14521
14487
  await RISK_LOCK.acquireLock();
14522
14488
  try {
14489
+ const timestamp = await this.params.time.getTimestamp(symbol, context, this.params.backtest);
14523
14490
  if (this._activePositions === POSITION_NEED_FETCH) {
14524
- await this.waitForInit();
14491
+ await this.waitForInit(new Date(timestamp));
14525
14492
  }
14526
14493
  const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
14527
14494
  const riskMap = this._activePositions;
@@ -14537,7 +14504,7 @@ class ClientRisk {
14537
14504
  minuteEstimatedTime: positionData.minuteEstimatedTime,
14538
14505
  openTimestamp: positionData.openTimestamp,
14539
14506
  });
14540
- await this._updatePositions();
14507
+ await this._updatePositions(new Date(timestamp));
14541
14508
  }
14542
14509
  finally {
14543
14510
  await RISK_LOCK.releaseLock();
@@ -14555,13 +14522,14 @@ class ClientRisk {
14555
14522
  });
14556
14523
  await RISK_LOCK.acquireLock();
14557
14524
  try {
14525
+ const timestamp = await this.params.time.getTimestamp(symbol, context, this.params.backtest);
14558
14526
  if (this._activePositions === POSITION_NEED_FETCH) {
14559
- await this.waitForInit();
14527
+ await this.waitForInit(new Date(timestamp));
14560
14528
  }
14561
14529
  const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
14562
14530
  const riskMap = this._activePositions;
14563
14531
  riskMap.delete(key);
14564
- await this._updatePositions();
14532
+ await this._updatePositions(new Date(timestamp));
14565
14533
  }
14566
14534
  finally {
14567
14535
  await RISK_LOCK.releaseLock();
@@ -14661,7 +14629,7 @@ class RiskConnectionService {
14661
14629
  constructor() {
14662
14630
  this.loggerService = inject(TYPES.loggerService);
14663
14631
  this.riskSchemaService = inject(TYPES.riskSchemaService);
14664
- this.executionContextService = inject(TYPES.executionContextService);
14632
+ this.timeMetaService = inject(TYPES.timeMetaService);
14665
14633
  /**
14666
14634
  * Action core service injected from DI container.
14667
14635
  */
@@ -14683,7 +14651,7 @@ class RiskConnectionService {
14683
14651
  return new ClientRisk({
14684
14652
  ...schema,
14685
14653
  logger: this.loggerService,
14686
- execution: this.executionContextService,
14654
+ time: this.timeMetaService,
14687
14655
  backtest,
14688
14656
  exchangeName,
14689
14657
  onRejected: CREATE_COMMIT_REJECTION_FN(this, exchangeName, frameName),
@@ -19674,8 +19642,8 @@ class BacktestLogicPrivateService {
19674
19642
  }
19675
19643
 
19676
19644
  const EMITTER_CHECK_INTERVAL = 5000;
19677
- const MS_PER_MINUTE$5 = 60000;
19678
- const INTERVAL_MINUTES$5 = {
19645
+ const MS_PER_MINUTE$6 = 60000;
19646
+ const INTERVAL_MINUTES$6 = {
19679
19647
  "1m": 1,
19680
19648
  "3m": 3,
19681
19649
  "5m": 5,
@@ -19690,7 +19658,7 @@ const INTERVAL_MINUTES$5 = {
19690
19658
  };
19691
19659
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
19692
19660
  const tickSubject = new functoolsKit.Subject();
19693
- const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
19661
+ const intervalMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$6;
19694
19662
  {
19695
19663
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
19696
19664
  functoolsKit.Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -19717,6 +19685,46 @@ const waitForCandle = async (interval) => {
19717
19685
  return emitter.toPromise();
19718
19686
  };
19719
19687
 
19688
+ const MS_PER_MINUTE$5 = 60000;
19689
+ const INTERVAL_MINUTES$5 = {
19690
+ "1m": 1,
19691
+ "3m": 3,
19692
+ "5m": 5,
19693
+ "15m": 15,
19694
+ "30m": 30,
19695
+ "1h": 60,
19696
+ "2h": 120,
19697
+ "4h": 240,
19698
+ "6h": 360,
19699
+ "8h": 480,
19700
+ "1d": 1440,
19701
+ };
19702
+ /**
19703
+ * Aligns timestamp down to the nearest interval boundary.
19704
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
19705
+ *
19706
+ * Candle timestamp convention:
19707
+ * - Candle timestamp = openTime (when candle opens)
19708
+ * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
19709
+ *
19710
+ * Adapter contract:
19711
+ * - Adapter must return candles with timestamp = openTime
19712
+ * - First returned candle.timestamp must equal aligned since
19713
+ * - Adapter must return exactly `limit` candles
19714
+ *
19715
+ * @param date - Date to align
19716
+ * @param interval - Candle interval (e.g., "1m", "15m", "1h")
19717
+ * @returns New Date aligned down to interval boundary
19718
+ */
19719
+ const alignToInterval = (date, interval) => {
19720
+ const minutes = INTERVAL_MINUTES$5[interval];
19721
+ if (minutes === undefined) {
19722
+ throw new Error(`alignToInterval: unknown interval=${interval}`);
19723
+ }
19724
+ const intervalMs = minutes * MS_PER_MINUTE$5;
19725
+ return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
19726
+ };
19727
+
19720
19728
  /**
19721
19729
  * Private service for live trading orchestration using async generators.
19722
19730
  *
@@ -27215,7 +27223,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
27215
27223
  }
27216
27224
  }
27217
27225
  if (shouldPersist) {
27218
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
27226
+ await self._persistState(symbol, data.strategyName, data.exchangeName, data.frameName, self.params.signalId);
27219
27227
  }
27220
27228
  };
27221
27229
  /**
@@ -27265,7 +27273,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
27265
27273
  }
27266
27274
  }
27267
27275
  if (shouldPersist) {
27268
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
27276
+ await self._persistState(symbol, data.strategyName, data.exchangeName, data.frameName, self.params.signalId);
27269
27277
  }
27270
27278
  };
27271
27279
  /**
@@ -27282,7 +27290,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
27282
27290
  * @param backtest - True if backtest mode, false if live mode
27283
27291
  * @param self - ClientPartial instance reference
27284
27292
  */
27285
- const WAIT_FOR_INIT_FN$1 = async (symbol, strategyName, exchangeName, backtest, self) => {
27293
+ const WAIT_FOR_INIT_FN$1 = async (symbol, strategyName, exchangeName, frameName, backtest, self) => {
27286
27294
  self.params.logger.debug("ClientPartial waitForInit", {
27287
27295
  symbol,
27288
27296
  backtest,
@@ -27298,7 +27306,12 @@ const WAIT_FOR_INIT_FN$1 = async (symbol, strategyName, exchangeName, backtest,
27298
27306
  self.params.logger.debug("ClientPartial waitForInit: skipping persist read in backtest mode");
27299
27307
  return;
27300
27308
  }
27301
- const partialData = await PersistPartialAdapter.readPartialData(symbol, strategyName, self.params.signalId, exchangeName);
27309
+ const timestamp = await self.params.time.getTimestamp(symbol, {
27310
+ strategyName,
27311
+ exchangeName,
27312
+ frameName,
27313
+ }, self.params.backtest);
27314
+ const partialData = await PersistPartialAdapter.readPartialData(symbol, strategyName, self.params.signalId, exchangeName, new Date(timestamp));
27302
27315
  for (const [signalId, data] of Object.entries(partialData)) {
27303
27316
  const state = {
27304
27317
  profitLevels: new Set(data.profitLevels),
@@ -27406,7 +27419,7 @@ class ClientPartial {
27406
27419
  * // Now profit()/loss() can be called
27407
27420
  * ```
27408
27421
  */
27409
- this.waitForInit = functoolsKit.singleshot(async (symbol, strategyName, exchangeName, backtest) => await WAIT_FOR_INIT_FN$1(symbol, strategyName, exchangeName, backtest, this));
27422
+ this.waitForInit = functoolsKit.singleshot(async (symbol, strategyName, exchangeName, frameName, backtest) => await WAIT_FOR_INIT_FN$1(symbol, strategyName, exchangeName, frameName, backtest, this));
27410
27423
  }
27411
27424
  /**
27412
27425
  * Persists current partial state to disk.
@@ -27424,7 +27437,7 @@ class ClientPartial {
27424
27437
  * @param signalId - Signal identifier
27425
27438
  * @returns Promise that resolves when persistence is complete
27426
27439
  */
27427
- async _persistState(symbol, strategyName, exchangeName, signalId) {
27440
+ async _persistState(symbol, strategyName, exchangeName, frameName, signalId) {
27428
27441
  if (this.params.backtest) {
27429
27442
  return;
27430
27443
  }
@@ -27439,7 +27452,12 @@ class ClientPartial {
27439
27452
  lossLevels: Array.from(state.lossLevels),
27440
27453
  };
27441
27454
  }
27442
- await PersistPartialAdapter.writePartialData(partialData, symbol, strategyName, signalId, exchangeName);
27455
+ const timestamp = await this.params.time.getTimestamp(symbol, {
27456
+ strategyName,
27457
+ exchangeName,
27458
+ frameName,
27459
+ }, this.params.backtest);
27460
+ await PersistPartialAdapter.writePartialData(partialData, symbol, strategyName, signalId, exchangeName, new Date(timestamp));
27443
27461
  }
27444
27462
  /**
27445
27463
  * Processes profit state and emits events for newly reached profit levels.
@@ -27564,7 +27582,7 @@ class ClientPartial {
27564
27582
  throw new Error(`Signal ID mismatch: expected ${this.params.signalId}, got ${data.id}`);
27565
27583
  }
27566
27584
  this._states.delete(data.id);
27567
- await this._persistState(symbol, data.strategyName, data.exchangeName, this.params.signalId);
27585
+ await this._persistState(symbol, data.strategyName, data.exchangeName, data.frameName, this.params.signalId);
27568
27586
  }
27569
27587
  }
27570
27588
 
@@ -27689,6 +27707,7 @@ class PartialConnectionService {
27689
27707
  * Action core service injected from DI container.
27690
27708
  */
27691
27709
  this.actionCoreService = inject(TYPES.actionCoreService);
27710
+ this.timeMetaService = inject(TYPES.timeMetaService);
27692
27711
  /**
27693
27712
  * Memoized factory function for ClientPartial instances.
27694
27713
  *
@@ -27702,6 +27721,7 @@ class PartialConnectionService {
27702
27721
  return new ClientPartial({
27703
27722
  signalId,
27704
27723
  logger: this.loggerService,
27724
+ time: this.timeMetaService,
27705
27725
  backtest,
27706
27726
  onProfit: CREATE_COMMIT_PROFIT_FN(this),
27707
27727
  onLoss: CREATE_COMMIT_LOSS_FN(this),
@@ -27731,7 +27751,7 @@ class PartialConnectionService {
27731
27751
  when,
27732
27752
  });
27733
27753
  const partial = this.getPartial(data.id, backtest);
27734
- await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
27754
+ await partial.waitForInit(symbol, data.strategyName, data.exchangeName, data.frameName, backtest);
27735
27755
  return await partial.profit(symbol, data, currentPrice, revenuePercent, backtest, when);
27736
27756
  };
27737
27757
  /**
@@ -27758,7 +27778,7 @@ class PartialConnectionService {
27758
27778
  when,
27759
27779
  });
27760
27780
  const partial = this.getPartial(data.id, backtest);
27761
- await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
27781
+ await partial.waitForInit(symbol, data.strategyName, data.exchangeName, data.frameName, backtest);
27762
27782
  return await partial.loss(symbol, data, currentPrice, lossPercent, backtest, when);
27763
27783
  };
27764
27784
  /**
@@ -27786,7 +27806,7 @@ class PartialConnectionService {
27786
27806
  backtest,
27787
27807
  });
27788
27808
  const partial = this.getPartial(data.id, backtest);
27789
- await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
27809
+ await partial.waitForInit(symbol, data.strategyName, data.exchangeName, data.frameName, backtest);
27790
27810
  await partial.clear(symbol, data, priceClose, backtest);
27791
27811
  const key = CREATE_KEY_FN$l(data.id, backtest);
27792
27812
  this.getPartial.clear(key);
@@ -28512,7 +28532,7 @@ const HANDLE_BREAKEVEN_FN = async (symbol, data, currentPrice, backtest, when, s
28512
28532
  // Emit event
28513
28533
  await self.params.onBreakeven(symbol, data.strategyName, data.exchangeName, data.frameName, data, currentPrice, backtest, when.getTime());
28514
28534
  // Persist state
28515
- await self._persistState(symbol, data.strategyName, data.exchangeName, self.params.signalId);
28535
+ await self._persistState(symbol, data.strategyName, data.exchangeName, data.frameName, self.params.signalId);
28516
28536
  return true;
28517
28537
  };
28518
28538
  /**
@@ -28527,7 +28547,7 @@ const HANDLE_BREAKEVEN_FN = async (symbol, data, currentPrice, backtest, when, s
28527
28547
  * @param exchangeName - Exchange identifier
28528
28548
  * @param self - ClientBreakeven instance reference
28529
28549
  */
28530
- const WAIT_FOR_INIT_FN = async (symbol, strategyName, exchangeName, backtest, self) => {
28550
+ const WAIT_FOR_INIT_FN = async (symbol, strategyName, exchangeName, frameName, backtest, self) => {
28531
28551
  self.params.logger.debug("ClientBreakeven waitForInit", {
28532
28552
  symbol,
28533
28553
  strategyName,
@@ -28543,7 +28563,12 @@ const WAIT_FOR_INIT_FN = async (symbol, strategyName, exchangeName, backtest, se
28543
28563
  self.params.logger.debug("ClientBreakeven waitForInit: skipping persist read in backtest mode");
28544
28564
  return;
28545
28565
  }
28546
- const breakevenData = await PersistBreakevenAdapter.readBreakevenData(symbol, strategyName, self.params.signalId, exchangeName);
28566
+ const timestamp = await self.params.time.getTimestamp(symbol, {
28567
+ strategyName,
28568
+ exchangeName,
28569
+ frameName,
28570
+ }, self.params.backtest);
28571
+ const breakevenData = await PersistBreakevenAdapter.readBreakevenData(symbol, strategyName, self.params.signalId, exchangeName, new Date(timestamp));
28547
28572
  for (const [signalId, data] of Object.entries(breakevenData)) {
28548
28573
  const state = {
28549
28574
  reached: data.reached,
@@ -28648,7 +28673,7 @@ class ClientBreakeven {
28648
28673
  * // Now check() can be called
28649
28674
  * ```
28650
28675
  */
28651
- this.waitForInit = functoolsKit.singleshot(async (symbol, strategyName, exchangeName, backtest) => await WAIT_FOR_INIT_FN(symbol, strategyName, exchangeName, backtest, this));
28676
+ this.waitForInit = functoolsKit.singleshot(async (symbol, strategyName, exchangeName, frameName, backtest) => await WAIT_FOR_INIT_FN(symbol, strategyName, exchangeName, frameName, backtest, this));
28652
28677
  }
28653
28678
  /**
28654
28679
  * Persists current breakeven state to disk.
@@ -28665,7 +28690,7 @@ class ClientBreakeven {
28665
28690
  * @param signalId - Signal identifier
28666
28691
  * @returns Promise that resolves when persistence is complete
28667
28692
  */
28668
- async _persistState(symbol, strategyName, exchangeName, signalId) {
28693
+ async _persistState(symbol, strategyName, exchangeName, frameName, signalId) {
28669
28694
  if (this.params.backtest) {
28670
28695
  return;
28671
28696
  }
@@ -28679,7 +28704,12 @@ class ClientBreakeven {
28679
28704
  reached: state.reached,
28680
28705
  };
28681
28706
  }
28682
- await PersistBreakevenAdapter.writeBreakevenData(breakevenData, symbol, strategyName, signalId, exchangeName);
28707
+ const timestamp = await this.params.time.getTimestamp(symbol, {
28708
+ strategyName,
28709
+ exchangeName,
28710
+ frameName,
28711
+ }, this.params.backtest);
28712
+ await PersistBreakevenAdapter.writeBreakevenData(breakevenData, symbol, strategyName, signalId, exchangeName, new Date(timestamp));
28683
28713
  }
28684
28714
  /**
28685
28715
  * Checks if breakeven should be triggered and emits event if conditions met.
@@ -28770,7 +28800,7 @@ class ClientBreakeven {
28770
28800
  throw new Error(`Signal ID mismatch: expected ${this.params.signalId}, got ${data.id}`);
28771
28801
  }
28772
28802
  this._states.delete(data.id);
28773
- await this._persistState(symbol, data.strategyName, data.exchangeName, this.params.signalId);
28803
+ await this._persistState(symbol, data.strategyName, data.exchangeName, data.frameName, this.params.signalId);
28774
28804
  }
28775
28805
  }
28776
28806
 
@@ -28859,6 +28889,7 @@ class BreakevenConnectionService {
28859
28889
  * Action core service injected from DI container.
28860
28890
  */
28861
28891
  this.actionCoreService = inject(TYPES.actionCoreService);
28892
+ this.timeMetaService = inject(TYPES.timeMetaService);
28862
28893
  /**
28863
28894
  * Memoized factory function for ClientBreakeven instances.
28864
28895
  *
@@ -28872,6 +28903,7 @@ class BreakevenConnectionService {
28872
28903
  return new ClientBreakeven({
28873
28904
  signalId,
28874
28905
  logger: this.loggerService,
28906
+ time: this.timeMetaService,
28875
28907
  backtest,
28876
28908
  onBreakeven: CREATE_COMMIT_BREAKEVEN_FN(this),
28877
28909
  });
@@ -28898,7 +28930,7 @@ class BreakevenConnectionService {
28898
28930
  when,
28899
28931
  });
28900
28932
  const breakeven = this.getBreakeven(data.id, backtest);
28901
- await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
28933
+ await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, data.frameName, backtest);
28902
28934
  return await breakeven.check(symbol, data, currentPrice, backtest, when);
28903
28935
  };
28904
28936
  /**
@@ -28927,7 +28959,7 @@ class BreakevenConnectionService {
28927
28959
  backtest,
28928
28960
  });
28929
28961
  const breakeven = this.getBreakeven(data.id, backtest);
28930
- await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
28962
+ await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, data.frameName, backtest);
28931
28963
  await breakeven.clear(symbol, data, priceClose, backtest);
28932
28964
  const key = CREATE_KEY_FN$i(data.id, backtest);
28933
28965
  this.getBreakeven.clear(key);
@@ -36540,6 +36572,80 @@ function getActionSchema(actionName) {
36540
36572
  return backtest.actionSchemaService.get(actionName);
36541
36573
  }
36542
36574
 
36575
+ const WAIT_FOR_READY_METHOD_NAME = "init.waitForReady";
36576
+ const MAX_WAIT_SECONDS = 10;
36577
+ const SECOND_DELAY = 1000;
36578
+ /**
36579
+ * Blocks until the schema registries needed to start trading are populated.
36580
+ *
36581
+ * Polls `exchangeValidationService`, `frameValidationService` and
36582
+ * `strategyValidationService` once per second for up to `MAX_WAIT_SECONDS`
36583
+ * seconds. The loop exits as soon as the required registries are non-empty
36584
+ * for the given mode:
36585
+ *
36586
+ * - Backtest mode (`isBacktest = true`): exchange, frame and strategy schemas
36587
+ * must all be registered (frames define the historical window).
36588
+ * - Live mode (`isBacktest = false`): only exchange and strategy schemas are
36589
+ * required — frames are unused.
36590
+ *
36591
+ * Useful at startup when schemas are registered asynchronously (lazy imports,
36592
+ * remote config, plugin loading) and the caller wants to delay `Backtest`/
36593
+ * `Live` invocation until everything is ready. If the timeout elapses without
36594
+ * the registries filling in, the function returns silently — the caller is
36595
+ * expected to surface a clearer error from the subsequent `Backtest`/`Live`
36596
+ * call (e.g. "no strategy registered").
36597
+ *
36598
+ * @param isBacktest - Whether to additionally require a registered frame schema. Defaults to `true`.
36599
+ * @returns Promise that resolves when the registries are ready or the timeout elapses.
36600
+ *
36601
+ * @example
36602
+ * ```typescript
36603
+ * import { waitForReady, Backtest } from "backtest-kit";
36604
+ *
36605
+ * import "./schemas/exchange";
36606
+ * import "./schemas/strategy";
36607
+ * import "./schemas/frame";
36608
+ *
36609
+ * await waitForReady();
36610
+ * Backtest.background("BTCUSDT", { strategyName, exchangeName, frameName });
36611
+ * ```
36612
+ */
36613
+ async function waitForReady(isBacktest = true) {
36614
+ backtest.loggerService.info(WAIT_FOR_READY_METHOD_NAME, { isBacktest });
36615
+ for (let i = 0; i !== MAX_WAIT_SECONDS; i++) {
36616
+ const [exchangeList, frameList, strategyList] = await Promise.all([
36617
+ backtest.exchangeValidationService.list(),
36618
+ backtest.frameValidationService.list(),
36619
+ backtest.strategyValidationService.list(),
36620
+ ]);
36621
+ if (isBacktest && !frameList.length) {
36622
+ backtest.loggerService.debug(WAIT_FOR_READY_METHOD_NAME, {
36623
+ reason: "no frames registered",
36624
+ attempt: i + 1,
36625
+ });
36626
+ await functoolsKit.sleep(SECOND_DELAY);
36627
+ continue;
36628
+ }
36629
+ if (!exchangeList.length) {
36630
+ backtest.loggerService.debug(WAIT_FOR_READY_METHOD_NAME, {
36631
+ reason: "no exchanges registered",
36632
+ attempt: i + 1,
36633
+ });
36634
+ await functoolsKit.sleep(SECOND_DELAY);
36635
+ continue;
36636
+ }
36637
+ if (!strategyList.length) {
36638
+ backtest.loggerService.debug(WAIT_FOR_READY_METHOD_NAME, {
36639
+ reason: "no strategies registered",
36640
+ attempt: i + 1,
36641
+ });
36642
+ await functoolsKit.sleep(SECOND_DELAY);
36643
+ continue;
36644
+ }
36645
+ break;
36646
+ }
36647
+ }
36648
+
36543
36649
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
36544
36650
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
36545
36651
  const GET_CLOSE_PRICE_METHOD_NAME = "exchange.getClosePrice";
@@ -63825,5 +63931,6 @@ exports.validatePendingSignal = validatePendingSignal;
63825
63931
  exports.validateScheduledSignal = validateScheduledSignal;
63826
63932
  exports.validateSignal = validateSignal;
63827
63933
  exports.waitForCandle = waitForCandle;
63934
+ exports.waitForReady = waitForReady;
63828
63935
  exports.warmCandles = warmCandles;
63829
63936
  exports.writeMemory = writeMemory;