backtest-kit 8.5.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1825 -1825
  3. package/build/index.cjs +2319 -929
  4. package/build/index.mjs +2305 -930
  5. package/package.json +86 -86
  6. package/types.d.ts +1972 -402
package/build/index.cjs CHANGED
@@ -1221,43 +1221,91 @@ _a$3 = BASE_WAIT_FOR_INIT_SYMBOL;
1221
1221
  // @ts-ignore
1222
1222
  PersistBase = functoolsKit.makeExtendable(PersistBase);
1223
1223
  /**
1224
- * Dummy persist adapter that discards all writes.
1225
- * Used for disabling persistence.
1224
+ * Default file-based implementation of IPersistSignalInstance.
1225
+ *
1226
+ * Features:
1227
+ * - Wraps PersistBase for atomic JSON writes
1228
+ * - Uses symbol as entity ID within a per-context PersistBase
1229
+ * - Crash-safe via atomic writes
1230
+ *
1231
+ * @example
1232
+ * ```typescript
1233
+ * const instance = new PersistSignalInstance("BTCUSDT", "my-strategy", "binance");
1234
+ * await instance.waitForInit(true);
1235
+ * await instance.writeSignalData(signalRow);
1236
+ * const restored = await instance.readSignalData();
1237
+ * ```
1226
1238
  */
1227
- class PersistDummy {
1239
+ class PersistSignalInstance {
1228
1240
  /**
1229
- * No-op initialization function.
1230
- * @returns Promise that resolves immediately
1241
+ * Creates new signal persistence instance.
1242
+ *
1243
+ * @param symbol - Trading pair symbol
1244
+ * @param strategyName - Strategy identifier
1245
+ * @param exchangeName - Exchange identifier
1231
1246
  */
1232
- async waitForInit() {
1247
+ constructor(symbol, strategyName, exchangeName) {
1248
+ this.symbol = symbol;
1249
+ this.strategyName = strategyName;
1250
+ this.exchangeName = exchangeName;
1251
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/signal/`);
1233
1252
  }
1234
1253
  /**
1235
- * No-op read function.
1236
- * @returns Promise that resolves with empty object
1254
+ * Initializes the underlying PersistBase storage.
1255
+ * Delegates to PersistBase.waitForInit which uses singleshot.
1256
+ *
1257
+ * @param initial - Whether this is the first initialization
1258
+ * @returns Promise that resolves when initialization is complete
1237
1259
  */
1238
- async readValue() {
1239
- return {};
1260
+ async waitForInit(initial) {
1261
+ await this._storage.waitForInit(initial);
1240
1262
  }
1241
1263
  /**
1242
- * No-op has value check.
1243
- * @returns Promise that resolves to false
1264
+ * Reads the persisted signal using `symbol` as the entity key.
1265
+ *
1266
+ * @returns Promise resolving to the signal or null if not found
1244
1267
  */
1245
- async hasValue() {
1246
- return false;
1268
+ async readSignalData() {
1269
+ if (await this._storage.hasValue(this.symbol)) {
1270
+ return await this._storage.readValue(this.symbol);
1271
+ }
1272
+ return null;
1247
1273
  }
1248
1274
  /**
1249
- * No-op write function.
1250
- * @returns Promise that resolves immediately
1275
+ * Writes the signal (or null to clear) using `symbol` as the entity key.
1276
+ *
1277
+ * @param signalRow - Signal data to persist, or null to clear
1278
+ * @returns Promise that resolves when write is complete
1251
1279
  */
1252
- async writeValue() {
1280
+ async writeSignalData(signalRow) {
1281
+ await this._storage.writeValue(this.symbol, signalRow);
1253
1282
  }
1283
+ }
1284
+ /**
1285
+ * No-op IPersistSignalInstance implementation used by PersistSignalUtils.useDummy().
1286
+ * All reads return null, all writes are discarded.
1287
+ */
1288
+ class PersistSignalDummyInstance {
1254
1289
  /**
1255
- * No-op keys generator.
1256
- * @returns Empty async generator
1290
+ * No-op constructor.
1291
+ * Context arguments are accepted to satisfy TPersistSignalInstanceCtor.
1257
1292
  */
1258
- async *keys() {
1259
- // Empty generator - no keys
1260
- }
1293
+ constructor(_symbol, _strategyName, _exchangeName) { }
1294
+ /**
1295
+ * No-op initialization.
1296
+ * @returns Promise that resolves immediately
1297
+ */
1298
+ async waitForInit(_initial) { }
1299
+ /**
1300
+ * Always returns null (no persisted signal).
1301
+ * @returns Promise resolving to null
1302
+ */
1303
+ async readSignalData() { return null; }
1304
+ /**
1305
+ * No-op write (discards data).
1306
+ * @returns Promise that resolves immediately
1307
+ */
1308
+ async writeSignalData(_signalRow) { }
1261
1309
  }
1262
1310
  /**
1263
1311
  * Utility class for managing signal persistence.
@@ -1272,40 +1320,37 @@ class PersistDummy {
1272
1320
  */
1273
1321
  class PersistSignalUtils {
1274
1322
  constructor() {
1275
- this.PersistSignalFactory = PersistBase;
1276
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistSignalFactory, [
1277
- `${symbol}_${strategyName}_${exchangeName}`,
1278
- `./dump/data/signal/`,
1279
- ]));
1280
1323
  /**
1281
- * Reads persisted signal data for a symbol and strategy.
1282
- *
1283
- * Called by ClientStrategy.waitForInit() to restore state.
1284
- * Returns null if no signal exists.
1324
+ * Constructor used to create per-context signal instances.
1325
+ * Replaceable via usePersistSignalAdapter() / useJson() / useDummy().
1326
+ */
1327
+ this.PersistSignalInstanceCtor = PersistSignalInstance;
1328
+ /**
1329
+ * Memoized factory creating one IPersistSignalInstance per (symbol, strategy, exchange) triple.
1330
+ */
1331
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistSignalInstanceCtor, [symbol, strategyName, exchangeName]));
1332
+ /**
1333
+ * Reads persisted signal for the given context.
1334
+ * Lazily initializes the instance on first access.
1285
1335
  *
1286
1336
  * @param symbol - Trading pair symbol
1287
1337
  * @param strategyName - Strategy identifier
1288
1338
  * @param exchangeName - Exchange identifier
1289
- * @returns Promise resolving to signal or null
1339
+ * @returns Promise resolving to signal or null if none persisted
1290
1340
  */
1291
1341
  this.readSignalData = async (symbol, strategyName, exchangeName) => {
1292
1342
  LOGGER_SERVICE$7.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA);
1293
1343
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1294
1344
  const isInitial = !this.getStorage.has(key);
1295
- const stateStorage = this.getStorage(symbol, strategyName, exchangeName);
1296
- await stateStorage.waitForInit(isInitial);
1297
- if (await stateStorage.hasValue(symbol)) {
1298
- return await stateStorage.readValue(symbol);
1299
- }
1300
- return null;
1345
+ const instance = this.getStorage(symbol, strategyName, exchangeName);
1346
+ await instance.waitForInit(isInitial);
1347
+ return instance.readSignalData();
1301
1348
  };
1302
1349
  /**
1303
- * Writes signal data to disk with atomic file writes.
1350
+ * Writes signal data (or null to clear) for the given context.
1351
+ * Lazily initializes the instance on first access.
1304
1352
  *
1305
- * Called by ClientStrategy.setPendingSignal() to persist state.
1306
- * Uses atomic writes to prevent corruption on crashes.
1307
- *
1308
- * @param signalRow - Signal data (null to clear)
1353
+ * @param signalRow - Signal data to persist, or null to clear
1309
1354
  * @param symbol - Trading pair symbol
1310
1355
  * @param strategyName - Strategy identifier
1311
1356
  * @param exchangeName - Exchange identifier
@@ -1315,53 +1360,43 @@ class PersistSignalUtils {
1315
1360
  LOGGER_SERVICE$7.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA);
1316
1361
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1317
1362
  const isInitial = !this.getStorage.has(key);
1318
- const stateStorage = this.getStorage(symbol, strategyName, exchangeName);
1319
- await stateStorage.waitForInit(isInitial);
1320
- await stateStorage.writeValue(symbol, signalRow);
1363
+ const instance = this.getStorage(symbol, strategyName, exchangeName);
1364
+ await instance.waitForInit(isInitial);
1365
+ return instance.writeSignalData(signalRow);
1321
1366
  };
1322
1367
  }
1323
1368
  /**
1324
- * Registers a custom persistence adapter.
1369
+ * Registers a custom IPersistSignalInstance constructor.
1370
+ * Clears the memoization cache so subsequent calls use the new adapter.
1325
1371
  *
1326
- * @param Ctor - Custom PersistBase constructor
1327
- *
1328
- * @example
1329
- * ```typescript
1330
- * class RedisPersist extends PersistBase {
1331
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
1332
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1333
- * }
1334
- * PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
1335
- * ```
1372
+ * @param Ctor - Custom IPersistSignalInstance constructor
1336
1373
  */
1337
1374
  usePersistSignalAdapter(Ctor) {
1338
1375
  LOGGER_SERVICE$7.info(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER);
1339
- this.PersistSignalFactory = Ctor;
1376
+ this.PersistSignalInstanceCtor = Ctor;
1377
+ this.getStorage.clear();
1340
1378
  }
1341
1379
  /**
1342
- * Clears the memoized storage cache.
1343
- * Call this when process.cwd() changes between strategy iterations
1344
- * so new storage instances are created with the updated base path.
1380
+ * Clears the memoized instance cache.
1381
+ * Call when process.cwd() changes between strategy iterations.
1345
1382
  */
1346
1383
  clear() {
1347
1384
  LOGGER_SERVICE$7.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_CLEAR);
1348
1385
  this.getStorage.clear();
1349
1386
  }
1350
1387
  /**
1351
- * Switches to the default JSON persist adapter.
1352
- * All future persistence writes will use JSON storage.
1388
+ * Switches to the default file-based PersistSignalInstance.
1353
1389
  */
1354
1390
  useJson() {
1355
1391
  LOGGER_SERVICE$7.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
1356
- this.usePersistSignalAdapter(PersistBase);
1392
+ this.usePersistSignalAdapter(PersistSignalInstance);
1357
1393
  }
1358
1394
  /**
1359
- * Switches to a dummy persist adapter that discards all writes.
1360
- * All future persistence writes will be no-ops.
1395
+ * Switches to PersistSignalDummyInstance (all operations are no-ops).
1361
1396
  */
1362
1397
  useDummy() {
1363
1398
  LOGGER_SERVICE$7.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
1364
- this.usePersistSignalAdapter(PersistDummy);
1399
+ this.usePersistSignalAdapter(PersistSignalDummyInstance);
1365
1400
  }
1366
1401
  }
1367
1402
  /**
@@ -1381,6 +1416,92 @@ class PersistSignalUtils {
1381
1416
  * ```
1382
1417
  */
1383
1418
  const PersistSignalAdapter = new PersistSignalUtils();
1419
+ /**
1420
+ * Default file-based implementation of IPersistRiskInstance.
1421
+ *
1422
+ * Features:
1423
+ * - Wraps PersistBase for atomic JSON writes
1424
+ * - Uses fixed entity ID "positions" within a per-context PersistBase
1425
+ * - Crash-safe via atomic writes
1426
+ *
1427
+ * @example
1428
+ * ```typescript
1429
+ * const instance = new PersistRiskInstance("my-risk", "binance");
1430
+ * await instance.waitForInit(true);
1431
+ * await instance.writePositionData([["strategy:BTCUSDT", positionData]]);
1432
+ * const positions = await instance.readPositionData();
1433
+ * ```
1434
+ */
1435
+ class PersistRiskInstance {
1436
+ /**
1437
+ * Creates new risk positions persistence instance.
1438
+ *
1439
+ * @param riskName - Risk profile identifier
1440
+ * @param exchangeName - Exchange identifier
1441
+ */
1442
+ constructor(riskName, exchangeName) {
1443
+ this.riskName = riskName;
1444
+ this.exchangeName = exchangeName;
1445
+ this._storage = new PersistBase(`${riskName}_${exchangeName}`, `./dump/data/risk/`);
1446
+ }
1447
+ /**
1448
+ * Initializes the underlying PersistBase storage.
1449
+ *
1450
+ * @param initial - Whether this is the first initialization
1451
+ * @returns Promise that resolves when initialization is complete
1452
+ */
1453
+ async waitForInit(initial) {
1454
+ await this._storage.waitForInit(initial);
1455
+ }
1456
+ /**
1457
+ * Reads the persisted positions array using the fixed STORAGE_KEY.
1458
+ *
1459
+ * @returns Promise resolving to positions (empty array if none persisted)
1460
+ */
1461
+ async readPositionData() {
1462
+ if (await this._storage.hasValue(PersistRiskInstance.STORAGE_KEY)) {
1463
+ return await this._storage.readValue(PersistRiskInstance.STORAGE_KEY);
1464
+ }
1465
+ return [];
1466
+ }
1467
+ /**
1468
+ * Writes the positions array using the fixed STORAGE_KEY.
1469
+ *
1470
+ * @param riskRow - Position entries to persist
1471
+ * @returns Promise that resolves when write is complete
1472
+ */
1473
+ async writePositionData(riskRow) {
1474
+ await this._storage.writeValue(PersistRiskInstance.STORAGE_KEY, riskRow);
1475
+ }
1476
+ }
1477
+ /** Fixed entity key for storing the positions array */
1478
+ PersistRiskInstance.STORAGE_KEY = "positions";
1479
+ /**
1480
+ * No-op IPersistRiskInstance implementation used by PersistRiskUtils.useDummy().
1481
+ * All reads return empty array, all writes are discarded.
1482
+ */
1483
+ class PersistRiskDummyInstance {
1484
+ /**
1485
+ * No-op constructor.
1486
+ * Context arguments are accepted to satisfy TPersistRiskInstanceCtor.
1487
+ */
1488
+ constructor(_riskName, _exchangeName) { }
1489
+ /**
1490
+ * No-op initialization.
1491
+ * @returns Promise that resolves immediately
1492
+ */
1493
+ async waitForInit(_initial) { }
1494
+ /**
1495
+ * Always returns empty positions array.
1496
+ * @returns Promise resolving to []
1497
+ */
1498
+ async readPositionData() { return []; }
1499
+ /**
1500
+ * No-op write (discards positions).
1501
+ * @returns Promise that resolves immediately
1502
+ */
1503
+ async writePositionData(_riskRow) { }
1504
+ }
1384
1505
  /**
1385
1506
  * Utility class for managing risk active positions persistence.
1386
1507
  *
@@ -1394,40 +1515,36 @@ const PersistSignalAdapter = new PersistSignalUtils();
1394
1515
  */
1395
1516
  class PersistRiskUtils {
1396
1517
  constructor() {
1397
- this.PersistRiskFactory = PersistBase;
1398
- this.getRiskStorage = functoolsKit.memoize(([riskName, exchangeName]) => `${riskName}:${exchangeName}`, (riskName, exchangeName) => Reflect.construct(this.PersistRiskFactory, [
1399
- `${riskName}_${exchangeName}`,
1400
- `./dump/data/risk/`,
1401
- ]));
1402
1518
  /**
1403
- * Reads persisted active positions for a risk profile.
1404
- *
1405
- * Called by ClientRisk.waitForInit() to restore state.
1406
- * Returns empty Map if no positions exist.
1519
+ * Constructor used to create per-context risk instances.
1520
+ * Replaceable via usePersistRiskAdapter() / useJson() / useDummy().
1521
+ */
1522
+ this.PersistRiskInstanceCtor = PersistRiskInstance;
1523
+ /**
1524
+ * Memoized factory creating one IPersistRiskInstance per (riskName, exchange) pair.
1525
+ */
1526
+ this.getRiskStorage = functoolsKit.memoize(([riskName, exchangeName]) => `${riskName}:${exchangeName}`, (riskName, exchangeName) => Reflect.construct(this.PersistRiskInstanceCtor, [riskName, exchangeName]));
1527
+ /**
1528
+ * Reads persisted active positions for the given risk context.
1529
+ * Lazily initializes the instance on first access.
1407
1530
  *
1408
1531
  * @param riskName - Risk profile identifier
1409
1532
  * @param exchangeName - Exchange identifier
1410
- * @returns Promise resolving to Map of active positions
1533
+ * @returns Promise resolving to position entries (empty array if none)
1411
1534
  */
1412
1535
  this.readPositionData = async (riskName, exchangeName) => {
1413
1536
  LOGGER_SERVICE$7.info(PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA);
1414
1537
  const key = `${riskName}:${exchangeName}`;
1415
1538
  const isInitial = !this.getRiskStorage.has(key);
1416
- const stateStorage = this.getRiskStorage(riskName, exchangeName);
1417
- await stateStorage.waitForInit(isInitial);
1418
- const RISK_STORAGE_KEY = "positions";
1419
- if (await stateStorage.hasValue(RISK_STORAGE_KEY)) {
1420
- return await stateStorage.readValue(RISK_STORAGE_KEY);
1421
- }
1422
- return [];
1539
+ const instance = this.getRiskStorage(riskName, exchangeName);
1540
+ await instance.waitForInit(isInitial);
1541
+ return instance.readPositionData();
1423
1542
  };
1424
1543
  /**
1425
- * Writes active positions to disk with atomic file writes.
1544
+ * Writes active positions for the given risk context.
1545
+ * Lazily initializes the instance on first access.
1426
1546
  *
1427
- * Called by ClientRisk after addSignal/removeSignal to persist state.
1428
- * Uses atomic writes to prevent corruption on crashes.
1429
- *
1430
- * @param positions - Map of active positions
1547
+ * @param riskRow - Position entries to persist
1431
1548
  * @param riskName - Risk profile identifier
1432
1549
  * @param exchangeName - Exchange identifier
1433
1550
  * @returns Promise that resolves when write is complete
@@ -1436,54 +1553,43 @@ class PersistRiskUtils {
1436
1553
  LOGGER_SERVICE$7.info(PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA);
1437
1554
  const key = `${riskName}:${exchangeName}`;
1438
1555
  const isInitial = !this.getRiskStorage.has(key);
1439
- const stateStorage = this.getRiskStorage(riskName, exchangeName);
1440
- await stateStorage.waitForInit(isInitial);
1441
- const RISK_STORAGE_KEY = "positions";
1442
- await stateStorage.writeValue(RISK_STORAGE_KEY, riskRow);
1556
+ const instance = this.getRiskStorage(riskName, exchangeName);
1557
+ await instance.waitForInit(isInitial);
1558
+ return instance.writePositionData(riskRow);
1443
1559
  };
1444
1560
  }
1445
1561
  /**
1446
- * Registers a custom persistence adapter.
1562
+ * Registers a custom IPersistRiskInstance constructor.
1563
+ * Clears the memoization cache so subsequent calls use the new adapter.
1447
1564
  *
1448
- * @param Ctor - Custom PersistBase constructor
1449
- *
1450
- * @example
1451
- * ```typescript
1452
- * class RedisPersist extends PersistBase {
1453
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
1454
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1455
- * }
1456
- * PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
1457
- * ```
1565
+ * @param Ctor - Custom IPersistRiskInstance constructor
1458
1566
  */
1459
1567
  usePersistRiskAdapter(Ctor) {
1460
1568
  LOGGER_SERVICE$7.info(PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER);
1461
- this.PersistRiskFactory = Ctor;
1569
+ this.PersistRiskInstanceCtor = Ctor;
1570
+ this.getRiskStorage.clear();
1462
1571
  }
1463
1572
  /**
1464
- * Clears the memoized storage cache.
1465
- * Call this when process.cwd() changes between strategy iterations
1466
- * so new storage instances are created with the updated base path.
1573
+ * Clears the memoized instance cache.
1574
+ * Call when process.cwd() changes between strategy iterations.
1467
1575
  */
1468
1576
  clear() {
1469
1577
  LOGGER_SERVICE$7.log(PERSIST_RISK_UTILS_METHOD_NAME_CLEAR);
1470
1578
  this.getRiskStorage.clear();
1471
1579
  }
1472
1580
  /**
1473
- * Switches to the default JSON persist adapter.
1474
- * All future persistence writes will use JSON storage.
1581
+ * Switches to the default file-based PersistRiskInstance.
1475
1582
  */
1476
1583
  useJson() {
1477
1584
  LOGGER_SERVICE$7.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_JSON);
1478
- this.usePersistRiskAdapter(PersistBase);
1585
+ this.usePersistRiskAdapter(PersistRiskInstance);
1479
1586
  }
1480
1587
  /**
1481
- * Switches to a dummy persist adapter that discards all writes.
1482
- * All future persistence writes will be no-ops.
1588
+ * Switches to PersistRiskDummyInstance (all operations are no-ops).
1483
1589
  */
1484
1590
  useDummy() {
1485
1591
  LOGGER_SERVICE$7.log(PERSIST_RISK_UTILS_METHOD_NAME_USE_DUMMY);
1486
- this.usePersistRiskAdapter(PersistDummy);
1592
+ this.usePersistRiskAdapter(PersistRiskDummyInstance);
1487
1593
  }
1488
1594
  }
1489
1595
  /**
@@ -1503,6 +1609,92 @@ class PersistRiskUtils {
1503
1609
  * ```
1504
1610
  */
1505
1611
  const PersistRiskAdapter = new PersistRiskUtils();
1612
+ /**
1613
+ * Default file-based implementation of IPersistScheduleInstance.
1614
+ *
1615
+ * Features:
1616
+ * - Wraps PersistBase for atomic JSON writes
1617
+ * - Uses symbol as entity ID within a per-context PersistBase
1618
+ * - Crash-safe via atomic writes
1619
+ *
1620
+ * @example
1621
+ * ```typescript
1622
+ * const instance = new PersistScheduleInstance("BTCUSDT", "my-strategy", "binance");
1623
+ * await instance.waitForInit(true);
1624
+ * await instance.writeScheduleData(scheduledRow);
1625
+ * const restored = await instance.readScheduleData();
1626
+ * ```
1627
+ */
1628
+ class PersistScheduleInstance {
1629
+ /**
1630
+ * Creates new scheduled signal persistence instance.
1631
+ *
1632
+ * @param symbol - Trading pair symbol
1633
+ * @param strategyName - Strategy identifier
1634
+ * @param exchangeName - Exchange identifier
1635
+ */
1636
+ constructor(symbol, strategyName, exchangeName) {
1637
+ this.symbol = symbol;
1638
+ this.strategyName = strategyName;
1639
+ this.exchangeName = exchangeName;
1640
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/schedule/`);
1641
+ }
1642
+ /**
1643
+ * Initializes the underlying PersistBase storage.
1644
+ *
1645
+ * @param initial - Whether this is the first initialization
1646
+ * @returns Promise that resolves when initialization is complete
1647
+ */
1648
+ async waitForInit(initial) {
1649
+ await this._storage.waitForInit(initial);
1650
+ }
1651
+ /**
1652
+ * Reads the persisted scheduled signal using `symbol` as the entity key.
1653
+ *
1654
+ * @returns Promise resolving to scheduled signal or null if not found
1655
+ */
1656
+ async readScheduleData() {
1657
+ if (await this._storage.hasValue(this.symbol)) {
1658
+ return await this._storage.readValue(this.symbol);
1659
+ }
1660
+ return null;
1661
+ }
1662
+ /**
1663
+ * Writes the scheduled signal (or null to clear) using `symbol` as the entity key.
1664
+ *
1665
+ * @param row - Scheduled signal data to persist, or null to clear
1666
+ * @returns Promise that resolves when write is complete
1667
+ */
1668
+ async writeScheduleData(row) {
1669
+ await this._storage.writeValue(this.symbol, row);
1670
+ }
1671
+ }
1672
+ /**
1673
+ * No-op IPersistScheduleInstance implementation used by PersistScheduleUtils.useDummy().
1674
+ * All reads return null, all writes are discarded.
1675
+ */
1676
+ class PersistScheduleDummyInstance {
1677
+ /**
1678
+ * No-op constructor.
1679
+ * Context arguments are accepted to satisfy TPersistScheduleInstanceCtor.
1680
+ */
1681
+ constructor(_symbol, _strategyName, _exchangeName) { }
1682
+ /**
1683
+ * No-op initialization.
1684
+ * @returns Promise that resolves immediately
1685
+ */
1686
+ async waitForInit(_initial) { }
1687
+ /**
1688
+ * Always returns null (no persisted scheduled signal).
1689
+ * @returns Promise resolving to null
1690
+ */
1691
+ async readScheduleData() { return null; }
1692
+ /**
1693
+ * No-op write (discards scheduled signal).
1694
+ * @returns Promise that resolves immediately
1695
+ */
1696
+ async writeScheduleData(_row) { }
1697
+ }
1506
1698
  /**
1507
1699
  * Utility class for managing scheduled signal persistence.
1508
1700
  *
@@ -1516,40 +1708,37 @@ const PersistRiskAdapter = new PersistRiskUtils();
1516
1708
  */
1517
1709
  class PersistScheduleUtils {
1518
1710
  constructor() {
1519
- this.PersistScheduleFactory = PersistBase;
1520
- this.getScheduleStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistScheduleFactory, [
1521
- `${symbol}_${strategyName}_${exchangeName}`,
1522
- `./dump/data/schedule/`,
1523
- ]));
1524
1711
  /**
1525
- * Reads persisted scheduled signal data for a symbol and strategy.
1526
- *
1527
- * Called by ClientStrategy.waitForInit() to restore scheduled signal state.
1528
- * Returns null if no scheduled signal exists.
1712
+ * Constructor used to create per-context scheduled signal instances.
1713
+ * Replaceable via usePersistScheduleAdapter() / useJson() / useDummy().
1714
+ */
1715
+ this.PersistScheduleInstanceCtor = PersistScheduleInstance;
1716
+ /**
1717
+ * Memoized factory creating one IPersistScheduleInstance per (symbol, strategy, exchange) triple.
1718
+ */
1719
+ this.getScheduleStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistScheduleInstanceCtor, [symbol, strategyName, exchangeName]));
1720
+ /**
1721
+ * Reads persisted scheduled signal for the given context.
1722
+ * Lazily initializes the instance on first access.
1529
1723
  *
1530
1724
  * @param symbol - Trading pair symbol
1531
1725
  * @param strategyName - Strategy identifier
1532
1726
  * @param exchangeName - Exchange identifier
1533
- * @returns Promise resolving to scheduled signal or null
1727
+ * @returns Promise resolving to scheduled signal or null if none persisted
1534
1728
  */
1535
1729
  this.readScheduleData = async (symbol, strategyName, exchangeName) => {
1536
1730
  LOGGER_SERVICE$7.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_READ_DATA);
1537
1731
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1538
1732
  const isInitial = !this.getScheduleStorage.has(key);
1539
- const stateStorage = this.getScheduleStorage(symbol, strategyName, exchangeName);
1540
- await stateStorage.waitForInit(isInitial);
1541
- if (await stateStorage.hasValue(symbol)) {
1542
- return await stateStorage.readValue(symbol);
1543
- }
1544
- return null;
1733
+ const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
1734
+ await instance.waitForInit(isInitial);
1735
+ return instance.readScheduleData();
1545
1736
  };
1546
1737
  /**
1547
- * Writes scheduled signal data to disk with atomic file writes.
1548
- *
1549
- * Called by ClientStrategy.setScheduledSignal() to persist state.
1550
- * Uses atomic writes to prevent corruption on crashes.
1738
+ * Writes scheduled signal (or null to clear) for the given context.
1739
+ * Lazily initializes the instance on first access.
1551
1740
  *
1552
- * @param scheduledSignalRow - Scheduled signal data (null to clear)
1741
+ * @param scheduledSignalRow - Scheduled signal data to persist, or null to clear
1553
1742
  * @param symbol - Trading pair symbol
1554
1743
  * @param strategyName - Strategy identifier
1555
1744
  * @param exchangeName - Exchange identifier
@@ -1559,53 +1748,43 @@ class PersistScheduleUtils {
1559
1748
  LOGGER_SERVICE$7.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA);
1560
1749
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1561
1750
  const isInitial = !this.getScheduleStorage.has(key);
1562
- const stateStorage = this.getScheduleStorage(symbol, strategyName, exchangeName);
1563
- await stateStorage.waitForInit(isInitial);
1564
- await stateStorage.writeValue(symbol, scheduledSignalRow);
1751
+ const instance = this.getScheduleStorage(symbol, strategyName, exchangeName);
1752
+ await instance.waitForInit(isInitial);
1753
+ return instance.writeScheduleData(scheduledSignalRow);
1565
1754
  };
1566
1755
  }
1567
1756
  /**
1568
- * Registers a custom persistence adapter.
1569
- *
1570
- * @param Ctor - Custom PersistBase constructor
1757
+ * Registers a custom IPersistScheduleInstance constructor.
1758
+ * Clears the memoization cache so subsequent calls use the new adapter.
1571
1759
  *
1572
- * @example
1573
- * ```typescript
1574
- * class RedisPersist extends PersistBase {
1575
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
1576
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1577
- * }
1578
- * PersistScheduleAdapter.usePersistScheduleAdapter(RedisPersist);
1579
- * ```
1760
+ * @param Ctor - Custom IPersistScheduleInstance constructor
1580
1761
  */
1581
1762
  usePersistScheduleAdapter(Ctor) {
1582
1763
  LOGGER_SERVICE$7.info(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_PERSIST_SCHEDULE_ADAPTER);
1583
- this.PersistScheduleFactory = Ctor;
1764
+ this.PersistScheduleInstanceCtor = Ctor;
1765
+ this.getScheduleStorage.clear();
1584
1766
  }
1585
1767
  /**
1586
- * Clears the memoized storage cache.
1587
- * Call this when process.cwd() changes between strategy iterations
1588
- * so new storage instances are created with the updated base path.
1768
+ * Clears the memoized instance cache.
1769
+ * Call when process.cwd() changes between strategy iterations.
1589
1770
  */
1590
1771
  clear() {
1591
1772
  LOGGER_SERVICE$7.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR);
1592
1773
  this.getScheduleStorage.clear();
1593
1774
  }
1594
1775
  /**
1595
- * Switches to the default JSON persist adapter.
1596
- * All future persistence writes will use JSON storage.
1776
+ * Switches to the default file-based PersistScheduleInstance.
1597
1777
  */
1598
1778
  useJson() {
1599
1779
  LOGGER_SERVICE$7.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON);
1600
- this.usePersistScheduleAdapter(PersistBase);
1780
+ this.usePersistScheduleAdapter(PersistScheduleInstance);
1601
1781
  }
1602
1782
  /**
1603
- * Switches to a dummy persist adapter that discards all writes.
1604
- * All future persistence writes will be no-ops.
1783
+ * Switches to PersistScheduleDummyInstance (all operations are no-ops).
1605
1784
  */
1606
1785
  useDummy() {
1607
1786
  LOGGER_SERVICE$7.log(PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY);
1608
- this.usePersistScheduleAdapter(PersistDummy);
1787
+ this.usePersistScheduleAdapter(PersistScheduleDummyInstance);
1609
1788
  }
1610
1789
  }
1611
1790
  /**
@@ -1625,6 +1804,94 @@ class PersistScheduleUtils {
1625
1804
  * ```
1626
1805
  */
1627
1806
  const PersistScheduleAdapter = new PersistScheduleUtils();
1807
+ /**
1808
+ * Default file-based implementation of IPersistPartialInstance.
1809
+ *
1810
+ * Features:
1811
+ * - Wraps PersistBase for atomic JSON writes
1812
+ * - Uses signalId as entity ID within a per-context PersistBase
1813
+ * - Crash-safe via atomic writes
1814
+ *
1815
+ * @example
1816
+ * ```typescript
1817
+ * const instance = new PersistPartialInstance("BTCUSDT", "my-strategy", "binance");
1818
+ * await instance.waitForInit(true);
1819
+ * await instance.writePartialData(partialData, "signal-id-1");
1820
+ * const restored = await instance.readPartialData("signal-id-1");
1821
+ * ```
1822
+ */
1823
+ class PersistPartialInstance {
1824
+ /**
1825
+ * Creates new partial data persistence instance.
1826
+ *
1827
+ * @param symbol - Trading pair symbol
1828
+ * @param strategyName - Strategy identifier
1829
+ * @param exchangeName - Exchange identifier
1830
+ */
1831
+ constructor(symbol, strategyName, exchangeName) {
1832
+ this.symbol = symbol;
1833
+ this.strategyName = strategyName;
1834
+ this.exchangeName = exchangeName;
1835
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/partial/`);
1836
+ }
1837
+ /**
1838
+ * Initializes the underlying PersistBase storage.
1839
+ *
1840
+ * @param initial - Whether this is the first initialization
1841
+ * @returns Promise that resolves when initialization is complete
1842
+ */
1843
+ async waitForInit(initial) {
1844
+ await this._storage.waitForInit(initial);
1845
+ }
1846
+ /**
1847
+ * Reads the partial data for the given signal using `signalId` as the entity key.
1848
+ *
1849
+ * @param signalId - Signal identifier
1850
+ * @returns Promise resolving to partial data record (empty object if not found)
1851
+ */
1852
+ async readPartialData(signalId) {
1853
+ if (await this._storage.hasValue(signalId)) {
1854
+ return await this._storage.readValue(signalId);
1855
+ }
1856
+ return {};
1857
+ }
1858
+ /**
1859
+ * Writes the partial data for the given signal using `signalId` as the entity key.
1860
+ *
1861
+ * @param data - Partial data record to persist
1862
+ * @param signalId - Signal identifier
1863
+ * @returns Promise that resolves when write is complete
1864
+ */
1865
+ async writePartialData(data, signalId) {
1866
+ await this._storage.writeValue(signalId, data);
1867
+ }
1868
+ }
1869
+ /**
1870
+ * No-op IPersistPartialInstance implementation used by PersistPartialUtils.useDummy().
1871
+ * All reads return empty object, all writes are discarded.
1872
+ */
1873
+ class PersistPartialDummyInstance {
1874
+ /**
1875
+ * No-op constructor.
1876
+ * Context arguments are accepted to satisfy TPersistPartialInstanceCtor.
1877
+ */
1878
+ constructor(_symbol, _strategyName, _exchangeName) { }
1879
+ /**
1880
+ * No-op initialization.
1881
+ * @returns Promise that resolves immediately
1882
+ */
1883
+ async waitForInit(_initial) { }
1884
+ /**
1885
+ * Always returns empty partial data record.
1886
+ * @returns Promise resolving to {}
1887
+ */
1888
+ async readPartialData(_signalId) { return {}; }
1889
+ /**
1890
+ * No-op write (discards partial data).
1891
+ * @returns Promise that resolves immediately
1892
+ */
1893
+ async writePartialData(_data, _signalId) { }
1894
+ }
1628
1895
  /**
1629
1896
  * Utility class for managing partial profit/loss levels persistence.
1630
1897
  *
@@ -1638,41 +1905,39 @@ const PersistScheduleAdapter = new PersistScheduleUtils();
1638
1905
  */
1639
1906
  class PersistPartialUtils {
1640
1907
  constructor() {
1641
- this.PersistPartialFactory = PersistBase;
1642
- this.getPartialStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistPartialFactory, [
1643
- `${symbol}_${strategyName}_${exchangeName}`,
1644
- `./dump/data/partial/`,
1645
- ]));
1646
1908
  /**
1647
- * Reads persisted partial data for a symbol and strategy.
1648
- *
1649
- * Called by ClientPartial.waitForInit() to restore state.
1650
- * Returns empty object if no partial data exists.
1909
+ * Constructor used to create per-context partial data instances.
1910
+ * Replaceable via usePersistPartialAdapter() / useJson() / useDummy().
1911
+ */
1912
+ this.PersistPartialInstanceCtor = PersistPartialInstance;
1913
+ /**
1914
+ * Memoized factory creating one IPersistPartialInstance per (symbol, strategy, exchange) triple.
1915
+ * Each signal's partial data is stored under its own signalId within the instance.
1916
+ */
1917
+ this.getPartialStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistPartialInstanceCtor, [symbol, strategyName, exchangeName]));
1918
+ /**
1919
+ * Reads partial data for the given context and signalId.
1920
+ * Lazily initializes the instance on first access.
1651
1921
  *
1652
1922
  * @param symbol - Trading pair symbol
1653
1923
  * @param strategyName - Strategy identifier
1654
1924
  * @param signalId - Signal identifier
1655
1925
  * @param exchangeName - Exchange identifier
1656
- * @returns Promise resolving to partial data record
1926
+ * @returns Promise resolving to partial data record (empty object if none)
1657
1927
  */
1658
1928
  this.readPartialData = async (symbol, strategyName, signalId, exchangeName) => {
1659
1929
  LOGGER_SERVICE$7.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA);
1660
1930
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1661
1931
  const isInitial = !this.getPartialStorage.has(key);
1662
- const stateStorage = this.getPartialStorage(symbol, strategyName, exchangeName);
1663
- await stateStorage.waitForInit(isInitial);
1664
- if (await stateStorage.hasValue(signalId)) {
1665
- return await stateStorage.readValue(signalId);
1666
- }
1667
- return {};
1932
+ const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
1933
+ await instance.waitForInit(isInitial);
1934
+ return instance.readPartialData(signalId);
1668
1935
  };
1669
1936
  /**
1670
- * Writes partial data to disk with atomic file writes.
1937
+ * Writes partial data for the given context and signalId.
1938
+ * Lazily initializes the instance on first access.
1671
1939
  *
1672
- * Called by ClientPartial after profit/loss level changes to persist state.
1673
- * Uses atomic writes to prevent corruption on crashes.
1674
- *
1675
- * @param partialData - Record of signal IDs to partial data
1940
+ * @param partialData - Partial data record to persist
1676
1941
  * @param symbol - Trading pair symbol
1677
1942
  * @param strategyName - Strategy identifier
1678
1943
  * @param signalId - Signal identifier
@@ -1683,53 +1948,43 @@ class PersistPartialUtils {
1683
1948
  LOGGER_SERVICE$7.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA);
1684
1949
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1685
1950
  const isInitial = !this.getPartialStorage.has(key);
1686
- const stateStorage = this.getPartialStorage(symbol, strategyName, exchangeName);
1687
- await stateStorage.waitForInit(isInitial);
1688
- await stateStorage.writeValue(signalId, partialData);
1951
+ const instance = this.getPartialStorage(symbol, strategyName, exchangeName);
1952
+ await instance.waitForInit(isInitial);
1953
+ return instance.writePartialData(partialData, signalId);
1689
1954
  };
1690
1955
  }
1691
1956
  /**
1692
- * Registers a custom persistence adapter.
1957
+ * Registers a custom IPersistPartialInstance constructor.
1958
+ * Clears the memoization cache so subsequent calls use the new adapter.
1693
1959
  *
1694
- * @param Ctor - Custom PersistBase constructor
1695
- *
1696
- * @example
1697
- * ```typescript
1698
- * class RedisPersist extends PersistBase {
1699
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
1700
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1701
- * }
1702
- * PersistPartialAdapter.usePersistPartialAdapter(RedisPersist);
1703
- * ```
1960
+ * @param Ctor - Custom IPersistPartialInstance constructor
1704
1961
  */
1705
1962
  usePersistPartialAdapter(Ctor) {
1706
1963
  LOGGER_SERVICE$7.info(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER);
1707
- this.PersistPartialFactory = Ctor;
1964
+ this.PersistPartialInstanceCtor = Ctor;
1965
+ this.getPartialStorage.clear();
1708
1966
  }
1709
1967
  /**
1710
- * Clears the memoized storage cache.
1711
- * Call this when process.cwd() changes between strategy iterations
1712
- * so new storage instances are created with the updated base path.
1968
+ * Clears the memoized instance cache.
1969
+ * Call when process.cwd() changes between strategy iterations.
1713
1970
  */
1714
1971
  clear() {
1715
1972
  LOGGER_SERVICE$7.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_CLEAR);
1716
1973
  this.getPartialStorage.clear();
1717
1974
  }
1718
1975
  /**
1719
- * Switches to the default JSON persist adapter.
1720
- * All future persistence writes will use JSON storage.
1976
+ * Switches to the default file-based PersistPartialInstance.
1721
1977
  */
1722
1978
  useJson() {
1723
1979
  LOGGER_SERVICE$7.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_JSON);
1724
- this.usePersistPartialAdapter(PersistBase);
1980
+ this.usePersistPartialAdapter(PersistPartialInstance);
1725
1981
  }
1726
1982
  /**
1727
- * Switches to a dummy persist adapter that discards all writes.
1728
- * All future persistence writes will be no-ops.
1983
+ * Switches to PersistPartialDummyInstance (all operations are no-ops).
1729
1984
  */
1730
1985
  useDummy() {
1731
1986
  LOGGER_SERVICE$7.log(PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_DUMMY);
1732
- this.usePersistPartialAdapter(PersistDummy);
1987
+ this.usePersistPartialAdapter(PersistPartialDummyInstance);
1733
1988
  }
1734
1989
  }
1735
1990
  /**
@@ -1749,11 +2004,99 @@ class PersistPartialUtils {
1749
2004
  * ```
1750
2005
  */
1751
2006
  const PersistPartialAdapter = new PersistPartialUtils();
2007
+ /**
2008
+ * Default file-based implementation of IPersistBreakevenInstance.
2009
+ *
2010
+ * Features:
2011
+ * - Wraps PersistBase for atomic JSON writes
2012
+ * - Uses signalId as entity ID within a per-context PersistBase
2013
+ * - Crash-safe via atomic writes
2014
+ *
2015
+ * @example
2016
+ * ```typescript
2017
+ * const instance = new PersistBreakevenInstance("BTCUSDT", "my-strategy", "binance");
2018
+ * await instance.waitForInit(true);
2019
+ * await instance.writeBreakevenData(breakevenData, "signal-id-1");
2020
+ * const restored = await instance.readBreakevenData("signal-id-1");
2021
+ * ```
2022
+ */
2023
+ class PersistBreakevenInstance {
2024
+ /**
2025
+ * Creates new breakeven persistence instance.
2026
+ *
2027
+ * @param symbol - Trading pair symbol
2028
+ * @param strategyName - Strategy identifier
2029
+ * @param exchangeName - Exchange identifier
2030
+ */
2031
+ constructor(symbol, strategyName, exchangeName) {
2032
+ this.symbol = symbol;
2033
+ this.strategyName = strategyName;
2034
+ this.exchangeName = exchangeName;
2035
+ this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/breakeven/`);
2036
+ }
2037
+ /**
2038
+ * Initializes the underlying PersistBase storage.
2039
+ *
2040
+ * @param initial - Whether this is the first initialization
2041
+ * @returns Promise that resolves when initialization is complete
2042
+ */
2043
+ async waitForInit(initial) {
2044
+ await this._storage.waitForInit(initial);
2045
+ }
2046
+ /**
2047
+ * Reads the breakeven data for the given signal using `signalId` as the entity key.
2048
+ *
2049
+ * @param signalId - Signal identifier
2050
+ * @returns Promise resolving to breakeven data record (empty object if not found)
2051
+ */
2052
+ async readBreakevenData(signalId) {
2053
+ if (await this._storage.hasValue(signalId)) {
2054
+ return await this._storage.readValue(signalId);
2055
+ }
2056
+ return {};
2057
+ }
2058
+ /**
2059
+ * Writes the breakeven data for the given signal using `signalId` as the entity key.
2060
+ *
2061
+ * @param data - Breakeven data record to persist
2062
+ * @param signalId - Signal identifier
2063
+ * @returns Promise that resolves when write is complete
2064
+ */
2065
+ async writeBreakevenData(data, signalId) {
2066
+ await this._storage.writeValue(signalId, data);
2067
+ }
2068
+ }
2069
+ /**
2070
+ * No-op IPersistBreakevenInstance implementation used by PersistBreakevenUtils.useDummy().
2071
+ * All reads return empty object, all writes are discarded.
2072
+ */
2073
+ class PersistBreakevenDummyInstance {
2074
+ /**
2075
+ * No-op constructor.
2076
+ * Context arguments are accepted to satisfy TPersistBreakevenInstanceCtor.
2077
+ */
2078
+ constructor(_symbol, _strategyName, _exchangeName) { }
2079
+ /**
2080
+ * No-op initialization.
2081
+ * @returns Promise that resolves immediately
2082
+ */
2083
+ async waitForInit(_initial) { }
2084
+ /**
2085
+ * Always returns empty breakeven data record.
2086
+ * @returns Promise resolving to {}
2087
+ */
2088
+ async readBreakevenData(_signalId) { return {}; }
2089
+ /**
2090
+ * No-op write (discards breakeven data).
2091
+ * @returns Promise that resolves immediately
2092
+ */
2093
+ async writeBreakevenData(_data, _signalId) { }
2094
+ }
1752
2095
  /**
1753
2096
  * Persistence utility class for breakeven state management.
1754
2097
  *
1755
2098
  * Handles reading and writing breakeven state to disk.
1756
- * Uses memoized PersistBase instances per symbol-strategy pair.
2099
+ * Uses memoized PersistBreakevenInstance instances per symbol-strategy pair.
1757
2100
  *
1758
2101
  * Features:
1759
2102
  * - Atomic file writes via PersistBase.writeValue()
@@ -1783,53 +2126,36 @@ const PersistPartialAdapter = new PersistPartialUtils();
1783
2126
  class PersistBreakevenUtils {
1784
2127
  constructor() {
1785
2128
  /**
1786
- * Factory for creating PersistBase instances.
1787
- * Can be replaced via usePersistBreakevenAdapter().
2129
+ * Constructor used to create per-context breakeven instances.
2130
+ * Replaceable via usePersistBreakevenAdapter() / useJson() / useDummy().
1788
2131
  */
1789
- this.PersistBreakevenFactory = PersistBase;
2132
+ this.PersistBreakevenInstanceCtor = PersistBreakevenInstance;
1790
2133
  /**
1791
- * Memoized storage factory for breakeven data.
1792
- * Creates one PersistBase instance per symbol-strategy-exchange combination.
1793
- * Key format: "symbol:strategyName:exchangeName"
1794
- *
1795
- * @param symbol - Trading pair symbol
1796
- * @param strategyName - Strategy identifier
1797
- * @param exchangeName - Exchange identifier
1798
- * @returns PersistBase instance for this symbol-strategy-exchange combination
2134
+ * Memoized factory creating one IPersistBreakevenInstance per (symbol, strategy, exchange) triple.
2135
+ * Each signal's breakeven data is stored under its own signalId within the instance.
1799
2136
  */
1800
- this.getBreakevenStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistBreakevenFactory, [
1801
- `${symbol}_${strategyName}_${exchangeName}`,
1802
- `./dump/data/breakeven/`,
1803
- ]));
2137
+ this.getBreakevenStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistBreakevenInstanceCtor, [symbol, strategyName, exchangeName]));
1804
2138
  /**
1805
- * Reads persisted breakeven data for a symbol and strategy.
1806
- *
1807
- * Called by ClientBreakeven.waitForInit() to restore state.
1808
- * Returns empty object if no breakeven data exists.
2139
+ * Reads breakeven data for the given context and signalId.
2140
+ * Lazily initializes the instance on first access.
1809
2141
  *
1810
2142
  * @param symbol - Trading pair symbol
1811
2143
  * @param strategyName - Strategy identifier
1812
2144
  * @param signalId - Signal identifier
1813
2145
  * @param exchangeName - Exchange identifier
1814
- * @returns Promise resolving to breakeven data record
2146
+ * @returns Promise resolving to breakeven data record (empty object if none)
1815
2147
  */
1816
2148
  this.readBreakevenData = async (symbol, strategyName, signalId, exchangeName) => {
1817
2149
  LOGGER_SERVICE$7.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_READ_DATA);
1818
2150
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1819
2151
  const isInitial = !this.getBreakevenStorage.has(key);
1820
- const stateStorage = this.getBreakevenStorage(symbol, strategyName, exchangeName);
1821
- await stateStorage.waitForInit(isInitial);
1822
- if (await stateStorage.hasValue(signalId)) {
1823
- return await stateStorage.readValue(signalId);
1824
- }
1825
- return {};
2152
+ const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
2153
+ await instance.waitForInit(isInitial);
2154
+ return instance.readBreakevenData(signalId);
1826
2155
  };
1827
2156
  /**
1828
- * Writes breakeven data to disk.
1829
- *
1830
- * Called by ClientBreakeven._persistState() after state changes.
1831
- * Creates directory and file if they don't exist.
1832
- * Uses atomic writes to prevent data corruption.
2157
+ * Writes breakeven data for the given context and signalId.
2158
+ * Lazily initializes the instance on first access.
1833
2159
  *
1834
2160
  * @param breakevenData - Breakeven data record to persist
1835
2161
  * @param symbol - Trading pair symbol
@@ -1842,53 +2168,43 @@ class PersistBreakevenUtils {
1842
2168
  LOGGER_SERVICE$7.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_WRITE_DATA);
1843
2169
  const key = `${symbol}:${strategyName}:${exchangeName}`;
1844
2170
  const isInitial = !this.getBreakevenStorage.has(key);
1845
- const stateStorage = this.getBreakevenStorage(symbol, strategyName, exchangeName);
1846
- await stateStorage.waitForInit(isInitial);
1847
- await stateStorage.writeValue(signalId, breakevenData);
2171
+ const instance = this.getBreakevenStorage(symbol, strategyName, exchangeName);
2172
+ await instance.waitForInit(isInitial);
2173
+ return instance.writeBreakevenData(breakevenData, signalId);
1848
2174
  };
1849
2175
  }
1850
2176
  /**
1851
- * Registers a custom persistence adapter.
1852
- *
1853
- * @param Ctor - Custom PersistBase constructor
2177
+ * Registers a custom IPersistBreakevenInstance constructor.
2178
+ * Clears the memoization cache so subsequent calls use the new adapter.
1854
2179
  *
1855
- * @example
1856
- * ```typescript
1857
- * class RedisPersist extends PersistBase {
1858
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
1859
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
1860
- * }
1861
- * PersistBreakevenAdapter.usePersistBreakevenAdapter(RedisPersist);
1862
- * ```
2180
+ * @param Ctor - Custom IPersistBreakevenInstance constructor
1863
2181
  */
1864
2182
  usePersistBreakevenAdapter(Ctor) {
1865
2183
  LOGGER_SERVICE$7.info(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_PERSIST_BREAKEVEN_ADAPTER);
1866
- this.PersistBreakevenFactory = Ctor;
2184
+ this.PersistBreakevenInstanceCtor = Ctor;
2185
+ this.getBreakevenStorage.clear();
1867
2186
  }
1868
2187
  /**
1869
- * Clears the memoized storage cache.
1870
- * Call this when process.cwd() changes between strategy iterations
1871
- * so new storage instances are created with the updated base path.
2188
+ * Clears the memoized instance cache.
2189
+ * Call when process.cwd() changes between strategy iterations.
1872
2190
  */
1873
2191
  clear() {
1874
2192
  LOGGER_SERVICE$7.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_CLEAR);
1875
2193
  this.getBreakevenStorage.clear();
1876
2194
  }
1877
2195
  /**
1878
- * Switches to the default JSON persist adapter.
1879
- * All future persistence writes will use JSON storage.
2196
+ * Switches to the default file-based PersistBreakevenInstance.
1880
2197
  */
1881
2198
  useJson() {
1882
2199
  LOGGER_SERVICE$7.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_JSON);
1883
- this.usePersistBreakevenAdapter(PersistBase);
2200
+ this.usePersistBreakevenAdapter(PersistBreakevenInstance);
1884
2201
  }
1885
2202
  /**
1886
- * Switches to a dummy persist adapter that discards all writes.
1887
- * All future persistence writes will be no-ops.
2203
+ * Switches to PersistBreakevenDummyInstance (all operations are no-ops).
1888
2204
  */
1889
2205
  useDummy() {
1890
2206
  LOGGER_SERVICE$7.log(PERSIST_BREAKEVEN_UTILS_METHOD_NAME_USE_DUMMY);
1891
- this.usePersistBreakevenAdapter(PersistDummy);
2207
+ this.usePersistBreakevenAdapter(PersistBreakevenDummyInstance);
1892
2208
  }
1893
2209
  }
1894
2210
  /**
@@ -1908,6 +2224,140 @@ class PersistBreakevenUtils {
1908
2224
  * ```
1909
2225
  */
1910
2226
  const PersistBreakevenAdapter = new PersistBreakevenUtils();
2227
+ /**
2228
+ * Default file-based implementation of IPersistCandleInstance.
2229
+ *
2230
+ * Features:
2231
+ * - Each candle stored as a separate JSON file keyed by its timestamp
2232
+ * - Read returns null on any missing timestamp (cache miss → refetch)
2233
+ * - Write skips incomplete candles (closeTime > now) and existing keys
2234
+ * - Invalid cached candles emit warnings via errorEmitter and treated as miss
2235
+ *
2236
+ * @example
2237
+ * ```typescript
2238
+ * const instance = new PersistCandleInstance("BTCUSDT", "1m", "binance");
2239
+ * await instance.waitForInit(true);
2240
+ * await instance.writeCandlesData(candles);
2241
+ * const cached = await instance.readCandlesData(100, since, until);
2242
+ * ```
2243
+ */
2244
+ class PersistCandleInstance {
2245
+ /**
2246
+ * Creates new candle cache persistence instance.
2247
+ *
2248
+ * @param symbol - Trading pair symbol
2249
+ * @param interval - Candle interval (1m, 5m, 1h, etc.)
2250
+ * @param exchangeName - Exchange identifier
2251
+ */
2252
+ constructor(symbol, interval, exchangeName) {
2253
+ this.symbol = symbol;
2254
+ this.interval = interval;
2255
+ this.exchangeName = exchangeName;
2256
+ this._storage = new PersistBase(`${exchangeName}/${symbol}/${interval}`, `./dump/data/candle/`);
2257
+ }
2258
+ /**
2259
+ * Initializes the underlying PersistBase storage.
2260
+ *
2261
+ * @param initial - Whether this is the first initialization
2262
+ * @returns Promise that resolves when initialization is complete
2263
+ */
2264
+ async waitForInit(initial) {
2265
+ await this._storage.waitForInit(initial);
2266
+ }
2267
+ /**
2268
+ * Reads cached candles for the requested window.
2269
+ * Computes expected timestamps (sinceTimestamp + i * stepMs) and reads each
2270
+ * by timestamp key. Returns null on ANY missing timestamp (cache miss).
2271
+ * Invalid cached candles emit a warning via errorEmitter and are treated as miss.
2272
+ *
2273
+ * @param limit - Number of candles requested
2274
+ * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
2275
+ * @param _untilTimestamp - Reserved for API compatibility, unused
2276
+ * @returns Promise resolving to candles in order, or null on cache miss
2277
+ */
2278
+ async readCandlesData(limit, sinceTimestamp, _untilTimestamp) {
2279
+ const stepMs = INTERVAL_MINUTES$9[this.interval] * MS_PER_MINUTE$7;
2280
+ const cachedCandles = [];
2281
+ for (let i = 0; i < limit; i++) {
2282
+ const expectedTimestamp = sinceTimestamp + i * stepMs;
2283
+ const timestampKey = String(expectedTimestamp);
2284
+ if (await functoolsKit.not(this._storage.hasValue(timestampKey))) {
2285
+ return null;
2286
+ }
2287
+ try {
2288
+ const candle = await this._storage.readValue(timestampKey);
2289
+ cachedCandles.push(candle);
2290
+ }
2291
+ catch (error) {
2292
+ const message = `PersistCandleInstance.readCandlesData found invalid candle symbol=${this.symbol} interval=${this.interval} timestamp=${expectedTimestamp}`;
2293
+ const payload = {
2294
+ error: functoolsKit.errorData(error),
2295
+ message: functoolsKit.getErrorMessage(error),
2296
+ };
2297
+ LOGGER_SERVICE$7.warn(message, payload);
2298
+ console.warn(message, payload);
2299
+ errorEmitter.next(error);
2300
+ return null;
2301
+ }
2302
+ }
2303
+ return cachedCandles;
2304
+ }
2305
+ /**
2306
+ * Writes candles to cache.
2307
+ * Skips incomplete candles (closeTime > now) and existing keys to keep
2308
+ * the cache append-only for fully closed candles.
2309
+ *
2310
+ * @param candles - Array of candle data to cache
2311
+ * @returns Promise that resolves when all writes are complete
2312
+ */
2313
+ async writeCandlesData(candles) {
2314
+ const stepMs = INTERVAL_MINUTES$9[this.interval] * MS_PER_MINUTE$7;
2315
+ const now = Date.now();
2316
+ for (const candle of candles) {
2317
+ const candleCloseTime = candle.timestamp + stepMs;
2318
+ if (candleCloseTime > now) {
2319
+ LOGGER_SERVICE$7.debug("PersistCandleInstance.writeCandlesData: skipping incomplete candle", {
2320
+ symbol: this.symbol,
2321
+ interval: this.interval,
2322
+ exchangeName: this.exchangeName,
2323
+ timestamp: candle.timestamp,
2324
+ closeTime: candleCloseTime,
2325
+ now,
2326
+ });
2327
+ continue;
2328
+ }
2329
+ if (await functoolsKit.not(this._storage.hasValue(String(candle.timestamp)))) {
2330
+ await this._storage.writeValue(String(candle.timestamp), candle);
2331
+ }
2332
+ }
2333
+ }
2334
+ }
2335
+ /**
2336
+ * No-op IPersistCandleInstance implementation used by PersistCandleUtils.useDummy().
2337
+ * Always returns null on read (forces refetch), discards writes.
2338
+ */
2339
+ class PersistCandleDummyInstance {
2340
+ /**
2341
+ * No-op constructor.
2342
+ * Context arguments are accepted to satisfy TPersistCandleInstanceCtor.
2343
+ */
2344
+ constructor(_symbol, _interval, _exchangeName) { }
2345
+ /**
2346
+ * No-op initialization.
2347
+ * @returns Promise that resolves immediately
2348
+ */
2349
+ async waitForInit(_initial) { }
2350
+ /**
2351
+ * Always returns null (forces refetch via cache miss).
2352
+ * @returns Promise resolving to null
2353
+ */
2354
+ async readCandlesData(_limit, _since, _until) { return null; }
2355
+ /**
2356
+ * No-op write (discards candles).
2357
+ * @returns Promise that resolves immediately
2358
+ */
2359
+ async writeCandlesData(_candles) { }
2360
+ }
1911
2361
  /**
1912
2362
  * Utility class for managing candles cache persistence.
1913
2363
  *
@@ -1921,30 +2371,28 @@ const PersistBreakevenAdapter = new PersistBreakevenUtils();
1921
2371
  */
1922
2372
  class PersistCandleUtils {
1923
2373
  constructor() {
1924
- this.PersistCandlesFactory = PersistBase;
1925
- this.getCandlesStorage = functoolsKit.memoize(([symbol, interval, exchangeName]) => `${symbol}:${interval}:${exchangeName}`, (symbol, interval, exchangeName) => Reflect.construct(this.PersistCandlesFactory, [
1926
- `${exchangeName}/${symbol}/${interval}`,
1927
- `./dump/data/candle/`,
1928
- ]));
1929
2374
  /**
1930
- * Reads cached candles for a specific exchange, symbol, and interval.
1931
- * Returns candles only if cache contains ALL requested candles.
1932
- *
1933
- * Algorithm (matches ClientExchange.ts logic):
1934
- * 1. Calculate expected timestamps: sinceTimestamp, sinceTimestamp + stepMs, ..., sinceTimestamp + (limit-1) * stepMs
1935
- * 2. Try to read each expected candle by timestamp key
1936
- * 3. If ANY candle is missing, return null (cache miss)
1937
- * 4. If all candles found, return them in order
2375
+ * Constructor used to create per-context candle cache instances.
2376
+ * Replaceable via usePersistCandleAdapter() / useJson() / useDummy().
2377
+ */
2378
+ this.PersistCandleInstanceCtor = PersistCandleInstance;
2379
+ /**
2380
+ * Memoized factory creating one IPersistCandleInstance per (symbol, interval, exchange) triple.
2381
+ */
2382
+ this.getCandlesStorage = functoolsKit.memoize(([symbol, interval, exchangeName]) => `${symbol}:${interval}:${exchangeName}`, (symbol, interval, exchangeName) => Reflect.construct(this.PersistCandleInstanceCtor, [symbol, interval, exchangeName]));
2383
+ /**
2384
+ * Reads cached candles for the given context and time window.
2385
+ * Lazily initializes the instance on first access.
1938
2386
  *
1939
2387
  * @param symbol - Trading pair symbol
1940
2388
  * @param interval - Candle interval
1941
2389
  * @param exchangeName - Exchange identifier
1942
2390
  * @param limit - Number of candles requested
1943
2391
  * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
1944
- * @param _untilTimestamp - Unused, kept for API compatibility
1945
- * @returns Promise resolving to array of candles or null if cache is incomplete
2392
+ * @param untilTimestamp - Reserved for API compatibility
2393
+ * @returns Promise resolving to candles in order, or null on cache miss
1946
2394
  */
1947
- this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, _untilTimestamp) => {
2395
+ this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
1948
2396
  LOGGER_SERVICE$7.info("PersistCandleUtils.readCandlesData", {
1949
2397
  symbol,
1950
2398
  interval,
@@ -1954,47 +2402,15 @@ class PersistCandleUtils {
1954
2402
  });
1955
2403
  const key = `${symbol}:${interval}:${exchangeName}`;
1956
2404
  const isInitial = !this.getCandlesStorage.has(key);
1957
- const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1958
- await stateStorage.waitForInit(isInitial);
1959
- const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
1960
- // Calculate expected timestamps and fetch each candle directly
1961
- const cachedCandles = [];
1962
- for (let i = 0; i < limit; i++) {
1963
- const expectedTimestamp = sinceTimestamp + i * stepMs;
1964
- const timestampKey = String(expectedTimestamp);
1965
- if (await functoolsKit.not(stateStorage.hasValue(timestampKey))) {
1966
- // Cache miss - candle not found
1967
- return null;
1968
- }
1969
- try {
1970
- const candle = await stateStorage.readValue(timestampKey);
1971
- cachedCandles.push(candle);
1972
- }
1973
- catch (error) {
1974
- // Invalid candle in cache - treat as cache miss
1975
- const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${expectedTimestamp}`;
1976
- const payload = {
1977
- error: functoolsKit.errorData(error),
1978
- message: functoolsKit.getErrorMessage(error),
1979
- };
1980
- LOGGER_SERVICE$7.warn(message, payload);
1981
- console.warn(message, payload);
1982
- errorEmitter.next(error);
1983
- return null;
1984
- }
1985
- }
1986
- return cachedCandles;
2405
+ const instance = this.getCandlesStorage(symbol, interval, exchangeName);
2406
+ await instance.waitForInit(isInitial);
2407
+ return instance.readCandlesData(limit, sinceTimestamp, untilTimestamp);
1987
2408
  };
1988
2409
  /**
1989
- * Writes candles to cache with atomic file writes.
1990
- * Each candle is stored as a separate JSON file named by its timestamp.
2410
+ * Writes candles to cache for the given context.
2411
+ * Lazily initializes the instance on first access.
1991
2412
  *
1992
- * The candles passed to this function should be validated candles from the adapter:
1993
- * - First candle.timestamp equals aligned sinceTimestamp (openTime)
1994
- * - Exact number of candles as requested
1995
- * - All candles are fully closed (timestamp + stepMs < untilTimestamp)
1996
- *
1997
- * @param candles - Array of candle data to cache (validated by the caller)
2413
+ * @param candles - Array of candle data to cache
1998
2414
  * @param symbol - Trading pair symbol
1999
2415
  * @param interval - Candle interval
2000
2416
  * @param exchangeName - Exchange identifier
@@ -2009,66 +2425,43 @@ class PersistCandleUtils {
2009
2425
  });
2010
2426
  const key = `${symbol}:${interval}:${exchangeName}`;
2011
2427
  const isInitial = !this.getCandlesStorage.has(key);
2012
- const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
2013
- await stateStorage.waitForInit(isInitial);
2014
- // Calculate step in milliseconds to determine candle close time
2015
- const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
2016
- const now = Date.now();
2017
- // Write each candle as a separate file, skipping incomplete candles
2018
- for (const candle of candles) {
2019
- // Skip incomplete candles: candle is complete when closeTime <= now
2020
- // closeTime = timestamp + stepMs
2021
- const candleCloseTime = candle.timestamp + stepMs;
2022
- if (candleCloseTime > now) {
2023
- LOGGER_SERVICE$7.debug("PersistCandleUtils.writeCandlesData: skipping incomplete candle", {
2024
- symbol,
2025
- interval,
2026
- exchangeName,
2027
- timestamp: candle.timestamp,
2028
- closeTime: candleCloseTime,
2029
- now,
2030
- });
2031
- continue;
2032
- }
2033
- if (await functoolsKit.not(stateStorage.hasValue(String(candle.timestamp)))) {
2034
- await stateStorage.writeValue(String(candle.timestamp), candle);
2035
- }
2036
- }
2428
+ const instance = this.getCandlesStorage(symbol, interval, exchangeName);
2429
+ await instance.waitForInit(isInitial);
2430
+ return instance.writeCandlesData(candles);
2037
2431
  };
2038
2432
  }
2039
2433
  /**
2040
- * Registers a custom persistence adapter.
2434
+ * Registers a custom IPersistCandleInstance constructor.
2435
+ * Clears the memoization cache so subsequent calls use the new adapter.
2041
2436
  *
2042
- * @param Ctor - Custom PersistBase constructor
2437
+ * @param Ctor - Custom IPersistCandleInstance constructor
2043
2438
  */
2044
2439
  usePersistCandleAdapter(Ctor) {
2045
2440
  LOGGER_SERVICE$7.info("PersistCandleUtils.usePersistCandleAdapter");
2046
- this.PersistCandlesFactory = Ctor;
2441
+ this.PersistCandleInstanceCtor = Ctor;
2442
+ this.getCandlesStorage.clear();
2047
2443
  }
2048
2444
  /**
2049
- * Clears the memoized storage cache.
2050
- * Call this when process.cwd() changes between strategy iterations
2051
- * so new storage instances are created with the updated base path.
2445
+ * Clears the memoized instance cache.
2446
+ * Call when process.cwd() changes between strategy iterations.
2052
2447
  */
2053
2448
  clear() {
2054
2449
  LOGGER_SERVICE$7.log(PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR);
2055
2450
  this.getCandlesStorage.clear();
2056
2451
  }
2057
2452
  /**
2058
- * Switches to the default JSON persist adapter.
2059
- * All future persistence writes will use JSON storage.
2453
+ * Switches to the default file-based PersistCandleInstance.
2060
2454
  */
2061
2455
  useJson() {
2062
2456
  LOGGER_SERVICE$7.log("PersistCandleUtils.useJson");
2063
- this.usePersistCandleAdapter(PersistBase);
2457
+ this.usePersistCandleAdapter(PersistCandleInstance);
2064
2458
  }
2065
2459
  /**
2066
- * Switches to a dummy persist adapter that discards all writes.
2067
- * All future persistence writes will be no-ops.
2460
+ * Switches to PersistCandleDummyInstance (always returns null on read, discards writes).
2068
2461
  */
2069
2462
  useDummy() {
2070
2463
  LOGGER_SERVICE$7.log("PersistCandleUtils.useDummy");
2071
- this.usePersistCandleAdapter(PersistDummy);
2464
+ this.usePersistCandleAdapter(PersistCandleDummyInstance);
2072
2465
  }
2073
2466
  }
2074
2467
  /**
@@ -2087,6 +2480,92 @@ class PersistCandleUtils {
2087
2480
  * ```
2088
2481
  */
2089
2482
  const PersistCandleAdapter = new PersistCandleUtils();
2483
+ /**
2484
+ * Default file-based implementation of IPersistStorageInstance.
2485
+ *
2486
+ * Features:
2487
+ * - Each signal stored as separate JSON file keyed by signal.id
2488
+ * - Read iterates all keys via PersistBase.keys()
2489
+ * - Crash-safe via atomic writes
2490
+ *
2491
+ * @example
2492
+ * ```typescript
2493
+ * const instance = new PersistStorageInstance(false);
2494
+ * await instance.waitForInit(true);
2495
+ * await instance.writeStorageData(signals);
2496
+ * const all = await instance.readStorageData();
2497
+ * ```
2498
+ */
2499
+ class PersistStorageInstance {
2500
+ /**
2501
+ * Creates new signal storage persistence instance.
2502
+ *
2503
+ * @param backtest - True for backtest mode storage, false for live mode
2504
+ */
2505
+ constructor(backtest) {
2506
+ this.backtest = backtest;
2507
+ this._storage = new PersistBase(backtest ? `backtest` : `live`, `./dump/data/storage/`);
2508
+ }
2509
+ /**
2510
+ * Initializes the underlying PersistBase storage.
2511
+ *
2512
+ * @param initial - Whether this is the first initialization
2513
+ * @returns Promise that resolves when initialization is complete
2514
+ */
2515
+ async waitForInit(initial) {
2516
+ await this._storage.waitForInit(initial);
2517
+ }
2518
+ /**
2519
+ * Reads all persisted signals by iterating storage keys.
2520
+ *
2521
+ * @returns Promise resolving to array of signal entries
2522
+ */
2523
+ async readStorageData() {
2524
+ const signals = [];
2525
+ for await (const signalId of this._storage.keys()) {
2526
+ const signal = await this._storage.readValue(signalId);
2527
+ signals.push(signal);
2528
+ }
2529
+ return signals;
2530
+ }
2531
+ /**
2532
+ * Writes each signal as a separate entity keyed by `signal.id`.
2533
+ *
2534
+ * @param signals - Signal entries to persist
2535
+ * @returns Promise that resolves when all writes are complete
2536
+ */
2537
+ async writeStorageData(signals) {
2538
+ for (const signal of signals) {
2539
+ await this._storage.writeValue(signal.id, signal);
2540
+ }
2541
+ }
2542
+ }
2543
+ /**
2544
+ * No-op IPersistStorageInstance implementation used by PersistStorageUtils.useDummy().
2545
+ * All reads return empty array, all writes are discarded.
2546
+ */
2547
+ class PersistStorageDummyInstance {
2548
+ /**
2549
+ * No-op constructor.
2550
+ * Context arguments are accepted to satisfy TPersistStorageInstanceCtor.
2551
+ */
2552
+ constructor(_backtest) { }
2553
+ /**
2554
+ * No-op initialization.
2555
+ * @returns Promise that resolves immediately
2556
+ */
2557
+ async waitForInit(_initial) { }
2558
+ /**
2559
+ * Always returns empty signals array.
2560
+ * @returns Promise resolving to []
2561
+ */
2562
+ async readStorageData() { return []; }
2563
+ /**
2564
+ * No-op write (discards signals).
2565
+ * @returns Promise that resolves immediately
2566
+ */
2567
+ async writeStorageData(_signals) { }
2568
+ }
2090
2569
  /**
2091
2570
  * Utility class for managing signal storage persistence.
2092
2571
  *
@@ -2101,89 +2580,80 @@ const PersistCandleAdapter = new PersistCandleUtils();
2101
2580
  */
2102
2581
  class PersistStorageUtils {
2103
2582
  constructor() {
2104
- this.PersistStorageFactory = PersistBase;
2105
- this.getStorage = functoolsKit.memoize(([backtest]) => backtest ? `backtest` : `live`, (backtest) => Reflect.construct(this.PersistStorageFactory, [
2106
- backtest ? `backtest` : `live`,
2107
- `./dump/data/storage/`,
2108
- ]));
2109
2583
  /**
2110
- * Reads persisted signals data.
2111
- *
2112
- * Called by StorageLiveUtils/StorageBacktestUtils.waitForInit() to restore state.
2113
- * Uses keys() from PersistBase to iterate over all stored signals.
2114
- * Returns empty array if no signals exist.
2584
+ * Constructor used to create per-mode signal storage instances.
2585
+ * Replaceable via usePersistStorageAdapter() / useJson() / useDummy().
2586
+ */
2587
+ this.PersistStorageInstanceCtor = PersistStorageInstance;
2588
+ /**
2589
+ * Memoized factory creating one IPersistStorageInstance per mode (backtest/live).
2590
+ * Key: "backtest" or "live".
2591
+ */
2592
+ this.getStorage = functoolsKit.memoize(([backtest]) => backtest ? `backtest` : `live`, (backtest) => Reflect.construct(this.PersistStorageInstanceCtor, [backtest]));
2593
+ /**
2594
+ * Reads all persisted signals for the given mode.
2595
+ * Lazily initializes the instance on first access.
2115
2596
  *
2116
- * @param backtest - If true, reads from backtest storage; otherwise from live storage
2597
+ * @param backtest - True for backtest mode storage, false for live mode
2117
2598
  * @returns Promise resolving to array of signal entries
2118
2599
  */
2119
2600
  this.readStorageData = async (backtest) => {
2120
2601
  LOGGER_SERVICE$7.info(PERSIST_STORAGE_UTILS_METHOD_NAME_READ_DATA);
2121
2602
  const key = backtest ? `backtest` : `live`;
2122
2603
  const isInitial = !this.getStorage.has(key);
2123
- const stateStorage = this.getStorage(backtest);
2124
- await stateStorage.waitForInit(isInitial);
2125
- const signals = [];
2126
- for await (const signalId of stateStorage.keys()) {
2127
- const signal = await stateStorage.readValue(signalId);
2128
- signals.push(signal);
2129
- }
2130
- return signals;
2604
+ const instance = this.getStorage(backtest);
2605
+ await instance.waitForInit(isInitial);
2606
+ return instance.readStorageData();
2131
2607
  };
2132
2608
  /**
2133
- * Writes signal data to disk with atomic file writes.
2134
- *
2135
- * Called by StorageLiveUtils/StorageBacktestUtils after signal changes to persist state.
2136
- * Uses signal.id as the storage key for individual file storage.
2137
- * Uses atomic writes to prevent corruption on crashes.
2609
+ * Writes signals for the given mode.
2610
+ * Lazily initializes the instance on first access.
2138
2611
  *
2139
2612
  * @param signalData - Signal entries to persist
2140
- * @param backtest - If true, writes to backtest storage; otherwise to live storage
2613
+ * @param backtest - True for backtest mode storage, false for live mode
2141
2614
  * @returns Promise that resolves when write is complete
2142
2615
  */
2143
2616
  this.writeStorageData = async (signalData, backtest) => {
2144
2617
  LOGGER_SERVICE$7.info(PERSIST_STORAGE_UTILS_METHOD_NAME_WRITE_DATA);
2145
2618
  const key = backtest ? `backtest` : `live`;
2146
2619
  const isInitial = !this.getStorage.has(key);
2147
- const stateStorage = this.getStorage(backtest);
2148
- await stateStorage.waitForInit(isInitial);
2149
- for (const signal of signalData) {
2150
- await stateStorage.writeValue(signal.id, signal);
2151
- }
2620
+ const instance = this.getStorage(backtest);
2621
+ await instance.waitForInit(isInitial);
2622
+ return instance.writeStorageData(signalData);
2152
2623
  };
2153
2624
  }
2154
2625
  /**
2155
- * Registers a custom persistence adapter.
2626
+ * Registers a custom IPersistStorageInstance constructor.
2627
+ * Clears the memoization cache so subsequent calls use the new adapter.
2156
2628
  *
2157
- * @param Ctor - Custom PersistBase constructor
2629
+ * @param Ctor - Custom IPersistStorageInstance constructor
2158
2630
  */
2159
2631
  usePersistStorageAdapter(Ctor) {
2160
2632
  LOGGER_SERVICE$7.info(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_PERSIST_STORAGE_ADAPTER);
2161
- this.PersistStorageFactory = Ctor;
2633
+ this.PersistStorageInstanceCtor = Ctor;
2634
+ this.getStorage.clear();
2162
2635
  }
2163
2636
  /**
2164
- * Clears the memoized storage cache.
2165
- * Call this when process.cwd() changes between strategy iterations
2166
- * so new storage instances are created with the updated base path.
2637
+ * Clears the memoized instance cache.
2638
+ * Call when process.cwd() changes between strategy iterations.
2167
2639
  */
2168
2640
  clear() {
2169
2641
  LOGGER_SERVICE$7.log(PERSIST_STORAGE_UTILS_METHOD_NAME_CLEAR);
2170
2642
  this.getStorage.clear();
2171
2643
  }
2172
2644
  /**
2173
- * Switches to the default JSON persist adapter.
2174
- * All future persistence writes will use JSON storage.
2645
+ * Switches to the default file-based PersistStorageInstance.
2175
2646
  */
2176
2647
  useJson() {
2177
2648
  LOGGER_SERVICE$7.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_JSON);
2178
- this.usePersistStorageAdapter(PersistBase);
2649
+ this.usePersistStorageAdapter(PersistStorageInstance);
2179
2650
  }
2180
2651
  /**
2181
- * Switches to a dummy persist adapter that discards all writes.
2182
- * All future persistence writes will be no-ops.
2652
+ * Switches to PersistStorageDummyInstance (all operations are no-ops).
2183
2653
  */
2184
2654
  useDummy() {
2185
2655
  LOGGER_SERVICE$7.log(PERSIST_STORAGE_UTILS_METHOD_NAME_USE_DUMMY);
2186
- this.usePersistStorageAdapter(PersistDummy);
2656
+ this.usePersistStorageAdapter(PersistStorageDummyInstance);
2187
2657
  }
2188
2658
  }
2189
2659
  /**
@@ -2191,6 +2661,92 @@ class PersistStorageUtils {
2191
2661
  * Used by SignalLiveUtils for signal storage persistence.
2192
2662
  */
2193
2663
  const PersistStorageAdapter = new PersistStorageUtils();
2664
+ /**
2665
+ * Default file-based implementation of IPersistNotificationInstance.
2666
+ *
2667
+ * Features:
2668
+ * - Each notification stored as separate JSON file keyed by id
2669
+ * - Read iterates all keys via PersistBase.keys()
2670
+ * - Crash-safe via atomic writes
2671
+ *
2672
+ * @example
2673
+ * ```typescript
2674
+ * const instance = new PersistNotificationInstance(false);
2675
+ * await instance.waitForInit(true);
2676
+ * await instance.writeNotificationData(notifications);
2677
+ * const all = await instance.readNotificationData();
2678
+ * ```
2679
+ */
2680
+ class PersistNotificationInstance {
2681
+ /**
2682
+ * Creates new notification persistence instance.
2683
+ *
2684
+ * @param backtest - True for backtest mode storage, false for live mode
2685
+ */
2686
+ constructor(backtest) {
2687
+ this.backtest = backtest;
2688
+ this._storage = new PersistBase(backtest ? `backtest` : `live`, `./dump/data/notification/`);
2689
+ }
2690
+ /**
2691
+ * Initializes the underlying PersistBase storage.
2692
+ *
2693
+ * @param initial - Whether this is the first initialization
2694
+ * @returns Promise that resolves when initialization is complete
2695
+ */
2696
+ async waitForInit(initial) {
2697
+ await this._storage.waitForInit(initial);
2698
+ }
2699
+ /**
2700
+ * Reads all persisted notifications by iterating storage keys.
2701
+ *
2702
+ * @returns Promise resolving to array of notification entries
2703
+ */
2704
+ async readNotificationData() {
2705
+ const notifications = [];
2706
+ for await (const notificationId of this._storage.keys()) {
2707
+ const notification = await this._storage.readValue(notificationId);
2708
+ notifications.push(notification);
2709
+ }
2710
+ return notifications;
2711
+ }
2712
+ /**
2713
+ * Writes each notification as a separate entity keyed by `notification.id`.
2714
+ *
2715
+ * @param notifications - Notification entries to persist
2716
+ * @returns Promise that resolves when all writes are complete
2717
+ */
2718
+ async writeNotificationData(notifications) {
2719
+ for (const notification of notifications) {
2720
+ await this._storage.writeValue(notification.id, notification);
2721
+ }
2722
+ }
2723
+ }
2724
+ /**
2725
+ * No-op IPersistNotificationInstance implementation used by PersistNotificationUtils.useDummy().
2726
+ * All reads return empty array, all writes are discarded.
2727
+ */
2728
+ class PersistNotificationDummyInstance {
2729
+ /**
2730
+ * No-op constructor.
2731
+ * Context arguments are accepted to satisfy TPersistNotificationInstanceCtor.
2732
+ */
2733
+ constructor(_backtest) { }
2734
+ /**
2735
+ * No-op initialization.
2736
+ * @returns Promise that resolves immediately
2737
+ */
2738
+ async waitForInit(_initial) { }
2739
+ /**
2740
+ * Always returns empty notifications array.
2741
+ * @returns Promise resolving to []
2742
+ */
2743
+ async readNotificationData() { return []; }
2744
+ /**
2745
+ * No-op write (discards notifications).
2746
+ * @returns Promise that resolves immediately
2747
+ */
2748
+ async writeNotificationData(_notifications) { }
2749
+ }
2194
2750
  /**
2195
2751
  * Utility class for managing notification persistence.
2196
2752
  *
@@ -2205,89 +2761,81 @@ const PersistStorageAdapter = new PersistStorageUtils();
2205
2761
  */
2206
2762
  class PersistNotificationUtils {
2207
2763
  constructor() {
2208
- this.PersistNotificationFactory = PersistBase;
2209
- this.getNotificationStorage = functoolsKit.memoize(([backtest]) => backtest ? `backtest` : `live`, (backtest) => Reflect.construct(this.PersistNotificationFactory, [
2210
- backtest ? `backtest` : `live`,
2211
- `./dump/data/notification/`,
2212
- ]));
2213
2764
  /**
2214
- * Reads persisted notifications data.
2215
- *
2216
- * Called by NotificationPersistLiveUtils/NotificationPersistBacktestUtils.waitForInit() to restore state.
2217
- * Uses keys() from PersistBase to iterate over all stored notifications.
2218
- * Returns empty array if no notifications exist.
2765
+ * Constructor used to create per-mode notification instances.
2766
+ * Replaceable via usePersistNotificationAdapter() / useJson() / useDummy().
2767
+ */
2768
+ this.PersistNotificationInstanceCtor = PersistNotificationInstance;
2769
+ /**
2770
+ * Memoized factory creating one IPersistNotificationInstance per mode (backtest/live).
2771
+ * Key: "backtest" or "live".
2772
+ */
2773
+ this.getNotificationStorage = functoolsKit.memoize(([backtest]) => backtest ? `backtest` : `live`, (backtest) => Reflect.construct(this.PersistNotificationInstanceCtor, [backtest]));
2774
+ /**
2775
+ * Reads persisted notifications for the given mode.
2776
+ * Lazily initializes the instance on first access.
2219
2777
  *
2220
- * @param backtest - If true, reads from backtest storage; otherwise from live storage
2778
+ * @param backtest - True for backtest mode storage, false for live mode
2221
2779
  * @returns Promise resolving to array of notification entries
2222
2780
  */
2223
2781
  this.readNotificationData = async (backtest) => {
2224
2782
  LOGGER_SERVICE$7.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_READ_DATA);
2225
2783
  const key = backtest ? `backtest` : `live`;
2226
2784
  const isInitial = !this.getNotificationStorage.has(key);
2227
- const stateStorage = this.getNotificationStorage(backtest);
2228
- await stateStorage.waitForInit(isInitial);
2229
- const notifications = [];
2230
- for await (const notificationId of stateStorage.keys()) {
2231
- const notification = await stateStorage.readValue(notificationId);
2232
- notifications.push(notification);
2233
- }
2234
- return notifications;
2785
+ const instance = this.getNotificationStorage(backtest);
2786
+ await instance.waitForInit(isInitial);
2787
+ return instance.readNotificationData();
2235
2788
  };
2236
2789
  /**
2237
- * Writes notification data to disk with atomic file writes.
2238
- *
2239
- * Called by NotificationPersistLiveUtils/NotificationPersistBacktestUtils after notification changes to persist state.
2240
- * Uses notification.id as the storage key for individual file storage.
2241
- * Uses atomic writes to prevent corruption on crashes.
2790
+ * Writes notifications for the given mode.
2791
+ * Lazily initializes the instance on first access.
2242
2792
  *
2243
2793
  * @param notificationData - Notification entries to persist
2244
- * @param backtest - If true, writes to backtest storage; otherwise to live storage
2794
+ * @param backtest - True for backtest mode storage, false for live mode
2245
2795
  * @returns Promise that resolves when write is complete
2246
2796
  */
2247
2797
  this.writeNotificationData = async (notificationData, backtest) => {
2248
2798
  LOGGER_SERVICE$7.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA);
2249
2799
  const key = backtest ? `backtest` : `live`;
2250
2800
  const isInitial = !this.getNotificationStorage.has(key);
2251
- const stateStorage = this.getNotificationStorage(backtest);
2252
- await stateStorage.waitForInit(isInitial);
2253
- for (const notification of notificationData) {
2254
- await stateStorage.writeValue(notification.id, notification);
2255
- }
2801
+ const instance = this.getNotificationStorage(backtest);
2802
+ await instance.waitForInit(isInitial);
2803
+ return instance.writeNotificationData(notificationData);
2256
2804
  };
2257
2805
  }
2258
2806
  /**
2259
- * Registers a custom persistence adapter.
2807
+ * Registers a custom IPersistNotificationInstance constructor.
2808
+ * Clears the memoization cache so subsequent calls use the new adapter.
2260
2809
  *
2261
- * @param Ctor - Custom PersistBase constructor
2810
+ * @param Ctor - Custom IPersistNotificationInstance constructor
2262
2811
  */
2263
2812
  usePersistNotificationAdapter(Ctor) {
2264
2813
  LOGGER_SERVICE$7.info(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER);
2265
- this.PersistNotificationFactory = Ctor;
2814
+ this.PersistNotificationInstanceCtor = Ctor;
2815
+ this.getNotificationStorage.clear();
2266
2816
  }
2267
2817
  /**
2268
- * Clears the memoized storage cache.
2269
- * Call this when process.cwd() changes between strategy iterations
2270
- * so new storage instances are created with the updated base path.
2818
+ * Clears the memoized instance cache.
2819
+ * Call when process.cwd() changes between strategy iterations so new
2820
+ * instances are created with the updated base path.
2271
2821
  */
2272
2822
  clear() {
2273
2823
  LOGGER_SERVICE$7.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_CLEAR);
2274
2824
  this.getNotificationStorage.clear();
2275
2825
  }
2276
2826
  /**
2277
- * Switches to the default JSON persist adapter.
2278
- * All future persistence writes will use JSON storage.
2827
+ * Switches to the default file-based PersistNotificationInstance.
2279
2828
  */
2280
2829
  useJson() {
2281
2830
  LOGGER_SERVICE$7.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON);
2282
- this.usePersistNotificationAdapter(PersistBase);
2831
+ this.usePersistNotificationAdapter(PersistNotificationInstance);
2283
2832
  }
2284
2833
  /**
2285
- * Switches to a dummy persist adapter that discards all writes.
2286
- * All future persistence writes will be no-ops.
2834
+ * Switches to PersistNotificationDummyInstance (all operations are no-ops).
2287
2835
  */
2288
2836
  useDummy() {
2289
2837
  LOGGER_SERVICE$7.log(PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY);
2290
- this.usePersistNotificationAdapter(PersistDummy);
2838
+ this.usePersistNotificationAdapter(PersistNotificationDummyInstance);
2291
2839
  }
2292
2840
  }
2293
2841
  /**
@@ -2295,11 +2843,95 @@ class PersistNotificationUtils {
2295
2843
  * Used by NotificationPersistLiveUtils/NotificationPersistBacktestUtils for notification persistence.
2296
2844
  */
2297
2845
  const PersistNotificationAdapter = new PersistNotificationUtils();
2846
+ /**
2847
+ * Default file-based implementation of IPersistLogInstance.
2848
+ *
2849
+ * Features:
2850
+ * - Each log entry stored as separate JSON file keyed by entry.id
2851
+ * - Read iterates all keys via PersistBase.keys()
2852
+ * - Append-only: existing keys are skipped on write
2853
+ * - Crash-safe via atomic writes
2854
+ *
2855
+ * @example
2856
+ * ```typescript
2857
+ * const instance = new PersistLogInstance();
2858
+ * await instance.waitForInit(true);
2859
+ * await instance.writeLogData(entries);
2860
+ * const all = await instance.readLogData();
2861
+ * ```
2862
+ */
2863
+ class PersistLogInstance {
2864
+ /**
2865
+ * Creates new log persistence instance.
2866
+ * No context parameters — there is a single global log storage.
2867
+ */
2868
+ constructor() {
2869
+ this._storage = new PersistBase(`log`, `./dump/data/log/`);
2870
+ }
2871
+ /**
2872
+ * Initializes the underlying PersistBase storage.
2873
+ *
2874
+ * @param initial - Whether this is the first initialization
2875
+ * @returns Promise that resolves when initialization is complete
2876
+ */
2877
+ async waitForInit(initial) {
2878
+ await this._storage.waitForInit(initial);
2879
+ }
2880
+ /**
2881
+ * Reads all persisted log entries by iterating storage keys.
2882
+ *
2883
+ * @returns Promise resolving to array of log entries
2884
+ */
2885
+ async readLogData() {
2886
+ const entries = [];
2887
+ for await (const entryId of this._storage.keys()) {
2888
+ const entry = await this._storage.readValue(entryId);
2889
+ entries.push(entry);
2890
+ }
2891
+ return entries;
2892
+ }
2893
+ /**
2894
+ * Writes log entries append-only — skips entries whose id already exists
2895
+ * so the log file is never overwritten.
2896
+ *
2897
+ * @param logData - Log entries to persist
2898
+ * @returns Promise that resolves when all writes are complete
2899
+ */
2900
+ async writeLogData(logData) {
2901
+ for (const entry of logData) {
2902
+ if (await this._storage.hasValue(entry.id)) {
2903
+ continue;
2904
+ }
2905
+ await this._storage.writeValue(entry.id, entry);
2906
+ }
2907
+ }
2908
+ }
2909
+ /**
2910
+ * No-op IPersistLogInstance implementation used by PersistLogUtils.useDummy().
2911
+ * All reads return empty array, all writes are discarded.
2912
+ */
2913
+ class PersistLogDummyInstance {
2914
+ /**
2915
+ * No-op initialization.
2916
+ * @returns Promise that resolves immediately
2917
+ */
2918
+ async waitForInit(_initial) { }
2919
+ /**
2920
+ * Always returns empty log entries array.
2921
+ * @returns Promise resolving to []
2922
+ */
2923
+ async readLogData() { return []; }
2924
+ /**
2925
+ * No-op write (discards log entries).
2926
+ * @returns Promise that resolves immediately
2927
+ */
2928
+ async writeLogData(_entries) { }
2929
+ }
2298
2930
  /**
2299
2931
  * Utility class for managing log entry persistence.
2300
2932
  *
2301
2933
  * Features:
2302
- * - Memoized storage instance
2934
+ * - Cached storage instance
2303
2935
  * - Custom adapter support
2304
2936
  * - Atomic read/write operations for LogData
2305
2937
  * - Each log entry stored as separate file keyed by id
@@ -2309,94 +2941,87 @@ const PersistNotificationAdapter = new PersistNotificationUtils();
2309
2941
  */
2310
2942
  class PersistLogUtils {
2311
2943
  constructor() {
2312
- this.PersistLogFactory = PersistBase;
2313
- this._logStorage = null;
2314
2944
  /**
2315
- * Reads persisted log entries.
2316
- *
2317
- * Called by LogPersistUtils.waitForInit() to restore state.
2318
- * Uses keys() from PersistBase to iterate over all stored entries.
2319
- * Returns empty array if no entries exist.
2945
+ * Constructor used to create the global log instance.
2946
+ * Replaceable via usePersistLogAdapter() / useJson() / useDummy().
2947
+ */
2948
+ this.PersistLogInstanceCtor = PersistLogInstance;
2949
+ /**
2950
+ * Cached singleton log instance. Lazily created on first access.
2951
+ * Reset to null by clear() and usePersistLogAdapter().
2952
+ */
2953
+ this._logInstance = null;
2954
+ /**
2955
+ * Reads all persisted log entries.
2956
+ * Lazily initializes the instance on first access.
2320
2957
  *
2321
2958
  * @returns Promise resolving to array of log entries
2322
2959
  */
2323
2960
  this.readLogData = async () => {
2324
2961
  LOGGER_SERVICE$7.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
2325
- const isInitial = !this._logStorage;
2326
- const stateStorage = this.getLogStorage();
2327
- await stateStorage.waitForInit(isInitial);
2328
- const entries = [];
2329
- for await (const entryId of stateStorage.keys()) {
2330
- const entry = await stateStorage.readValue(entryId);
2331
- entries.push(entry);
2332
- }
2333
- return entries;
2962
+ const isInitial = !this._logInstance;
2963
+ const instance = this.getLogInstance();
2964
+ await instance.waitForInit(isInitial);
2965
+ return instance.readLogData();
2334
2966
  };
2335
2967
  /**
2336
- * Writes log entries to disk with atomic file writes.
2337
- *
2338
- * Called by LogPersistUtils after each log call to persist state.
2339
- * Uses entry.id as the storage key for individual file storage.
2340
- * Uses atomic writes to prevent corruption on crashes.
2968
+ * Writes log entries (append-only duplicates by id are skipped).
2969
+ * Lazily initializes the instance on first access.
2341
2970
  *
2342
2971
  * @param logData - Log entries to persist
2343
2972
  * @returns Promise that resolves when write is complete
2344
2973
  */
2345
2974
  this.writeLogData = async (logData) => {
2346
2975
  LOGGER_SERVICE$7.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
2347
- const isInitial = !this._logStorage;
2348
- const stateStorage = this.getLogStorage();
2349
- await stateStorage.waitForInit(isInitial);
2350
- for (const entry of logData) {
2351
- if (await stateStorage.hasValue(entry.id)) {
2352
- continue;
2353
- }
2354
- await stateStorage.writeValue(entry.id, entry);
2355
- }
2976
+ const isInitial = !this._logInstance;
2977
+ const instance = this.getLogInstance();
2978
+ await instance.waitForInit(isInitial);
2979
+ return instance.writeLogData(logData);
2356
2980
  };
2357
2981
  }
2358
- getLogStorage() {
2359
- if (!this._logStorage) {
2360
- this._logStorage = Reflect.construct(this.PersistLogFactory, [
2361
- `log`,
2362
- `./dump/data/log/`,
2363
- ]);
2982
+ /**
2983
+ * Returns the cached log instance, creating it on first access.
2984
+ *
2985
+ * @returns The IPersistLogInstance singleton
2986
+ */
2987
+ getLogInstance() {
2988
+ if (!this._logInstance) {
2989
+ this._logInstance = Reflect.construct(this.PersistLogInstanceCtor, []);
2364
2990
  }
2365
- return this._logStorage;
2991
+ return this._logInstance;
2366
2992
  }
2367
2993
  /**
2368
- * Registers a custom persistence adapter.
2994
+ * Registers a custom IPersistLogInstance constructor.
2995
+ * Drops the cached instance so the next access uses the new adapter.
2369
2996
  *
2370
- * @param Ctor - Custom PersistBase constructor
2997
+ * @param Ctor - Custom IPersistLogInstance constructor
2371
2998
  */
2372
2999
  usePersistLogAdapter(Ctor) {
2373
3000
  LOGGER_SERVICE$7.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
2374
- this.PersistLogFactory = Ctor;
3001
+ this.PersistLogInstanceCtor = Ctor;
3002
+ this._logInstance = null;
2375
3003
  }
2376
3004
  /**
2377
- * Clears the cached storage instance.
2378
- * Call this when process.cwd() changes between strategy iterations
2379
- * so a new storage instance is created with the updated base path.
3005
+ * Drops the cached log instance.
3006
+ * Call when process.cwd() changes between strategy iterations.
2380
3007
  */
2381
3008
  clear() {
2382
3009
  LOGGER_SERVICE$7.log(PERSIST_LOG_UTILS_METHOD_NAME_CLEAR);
2383
- this._logStorage = null;
3010
+ this._logInstance = null;
2384
3011
  }
2385
3012
  /**
2386
- * Switches to the default JSON persist adapter.
2387
- * All future persistence writes will use JSON storage.
3013
+ * Switches to the default file-based PersistLogInstance.
2388
3014
  */
2389
3015
  useJson() {
2390
3016
  LOGGER_SERVICE$7.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
2391
- this.usePersistLogAdapter(PersistBase);
3017
+ this.usePersistLogAdapter(PersistLogInstance);
2392
3018
  }
2393
3019
  /**
2394
- * Switches to a dummy persist adapter that discards all writes.
2395
- * All future persistence writes will be no-ops.
3020
+ * Switches to PersistLogDummyInstance (all operations are no-ops).
2396
3021
  */
2397
3022
  useDummy() {
2398
3023
  LOGGER_SERVICE$7.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
2399
- this.usePersistLogAdapter(PersistDummy);
3024
+ this.usePersistLogAdapter(PersistLogDummyInstance);
2400
3025
  }
2401
3026
  }
2402
3027
  /**
@@ -2404,6 +3029,128 @@ class PersistLogUtils {
2404
3029
  * Used by LogPersistUtils for log entry persistence.
2405
3030
  */
2406
3031
  const PersistLogAdapter = new PersistLogUtils();
3032
+ /**
3033
+ * Default file-based implementation of IPersistMeasureInstance.
3034
+ *
3035
+ * Features:
3036
+ * - Wraps PersistBase for atomic JSON writes
3037
+ * - Soft delete via `removed: true` flag
3038
+ * - listMeasureData filters out removed entries
3039
+ *
3040
+ * @example
3041
+ * ```typescript
3042
+ * const instance = new PersistMeasureInstance("my-bucket");
3043
+ * await instance.waitForInit(true);
3044
+ * await instance.writeMeasureData({ id: "x", data: {}, removed: false }, "key1");
3045
+ * const data = await instance.readMeasureData("key1");
3046
+ * await instance.removeMeasureData("key1");
3047
+ * ```
3048
+ */
3049
+ class PersistMeasureInstance {
3050
+ /**
3051
+ * Creates new measure cache persistence instance.
3052
+ *
3053
+ * @param bucket - Cache bucket identifier
3054
+ */
3055
+ constructor(bucket) {
3056
+ this.bucket = bucket;
3057
+ this._storage = new PersistBase(bucket, `./dump/data/measure/`);
3058
+ }
3059
+ /**
3060
+ * Initializes the underlying PersistBase storage.
3061
+ *
3062
+ * @param initial - Whether this is the first initialization
3063
+ * @returns Promise that resolves when initialization is complete
3064
+ */
3065
+ async waitForInit(initial) {
3066
+ await this._storage.waitForInit(initial);
3067
+ }
3068
+ /**
3069
+ * Reads a measure entry by key. Returns null if entry is missing or soft-deleted.
3070
+ *
3071
+ * @param key - Cache key within the bucket
3072
+ * @returns Promise resolving to entry data, or null
3073
+ */
3074
+ async readMeasureData(key) {
3075
+ if (await this._storage.hasValue(key)) {
3076
+ const data = await this._storage.readValue(key);
3077
+ return data.removed ? null : data;
3078
+ }
3079
+ return null;
3080
+ }
3081
+ /**
3082
+ * Writes a measure entry under the given key.
3083
+ *
3084
+ * @param data - Data to cache
3085
+ * @param key - Cache key within the bucket
3086
+ * @returns Promise that resolves when write is complete
3087
+ */
3088
+ async writeMeasureData(data, key) {
3089
+ await this._storage.writeValue(key, data);
3090
+ }
3091
+ /**
3092
+ * Soft-deletes an entry by writing `removed: true` flag while preserving the file.
3093
+ *
3094
+ * @param key - Cache key within the bucket
3095
+ * @returns Promise that resolves when removal is complete
3096
+ */
3097
+ async removeMeasureData(key) {
3098
+ const data = await this._storage.readValue(key);
3099
+ if (data) {
3100
+ await this._storage.writeValue(key, Object.assign({}, data, { removed: true }));
3101
+ }
3102
+ }
3103
+ /**
3104
+ * Iterates all entries in the bucket, yielding keys of non-removed entries only.
3105
+ *
3106
+ * @returns AsyncGenerator yielding entry keys
3107
+ */
3108
+ async *listMeasureData() {
3109
+ for await (const key of this._storage.keys()) {
3110
+ const data = await this._storage.readValue(String(key));
3111
+ if (data === null || data.removed) {
3112
+ continue;
3113
+ }
3114
+ yield String(key);
3115
+ }
3116
+ }
3117
+ }
3118
+ /**
3119
+ * No-op IPersistMeasureInstance implementation used by PersistMeasureUtils.useDummy().
3120
+ * All reads return null, all writes/removes are discarded, list yields nothing.
3121
+ */
3122
+ class PersistMeasureDummyInstance {
3123
+ /**
3124
+ * No-op constructor.
3125
+ * Context arguments are accepted to satisfy TPersistMeasureInstanceCtor.
3126
+ */
3127
+ constructor(_bucket) { }
3128
+ /**
3129
+ * No-op initialization.
3130
+ * @returns Promise that resolves immediately
3131
+ */
3132
+ async waitForInit(_initial) { }
3133
+ /**
3134
+ * Always returns null (no cached entries).
3135
+ * @returns Promise resolving to null
3136
+ */
3137
+ async readMeasureData(_key) { return null; }
3138
+ /**
3139
+ * No-op write (discards entry).
3140
+ * @returns Promise that resolves immediately
3141
+ */
3142
+ async writeMeasureData(_data, _key) { }
3143
+ /**
3144
+ * No-op remove.
3145
+ * @returns Promise that resolves immediately
3146
+ */
3147
+ async removeMeasureData(_key) { }
3148
+ /**
3149
+ * Empty generator — yields no entries.
3150
+ * @returns AsyncGenerator that immediately completes
3151
+ */
3152
+ async *listMeasureData() { }
3153
+ }
2407
3154
  /**
2408
3155
  * Utility class for managing external API response cache persistence.
2409
3156
  *
@@ -2417,124 +3164,108 @@ const PersistLogAdapter = new PersistLogUtils();
2417
3164
  */
2418
3165
  class PersistMeasureUtils {
2419
3166
  constructor() {
2420
- this.PersistMeasureFactory = PersistBase;
2421
- this.getMeasureStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistMeasureFactory, [
2422
- bucket,
2423
- `./dump/data/measure/`,
2424
- ]));
2425
3167
  /**
2426
- * Reads cached measure data for a given bucket and key.
3168
+ * Constructor used to create per-bucket measure cache instances.
3169
+ * Replaceable via usePersistMeasureAdapter() / useJson() / useDummy().
3170
+ */
3171
+ this.PersistMeasureInstanceCtor = PersistMeasureInstance;
3172
+ /**
3173
+ * Memoized factory creating one IPersistMeasureInstance per bucket.
3174
+ */
3175
+ this.getMeasureStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistMeasureInstanceCtor, [bucket]));
3176
+ /**
3177
+ * Reads a measure entry from the given bucket by key.
3178
+ * Lazily initializes the bucket instance on first access.
2427
3179
  *
2428
- * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2429
- * @param key - Dynamic cache key within the bucket
2430
- * @returns Promise resolving to cached value or null if not found
3180
+ * @param bucket - Storage bucket identifier
3181
+ * @param key - Cache key within the bucket
3182
+ * @returns Promise resolving to cached value, or null if not found / soft-deleted
2431
3183
  */
2432
3184
  this.readMeasureData = async (bucket, key) => {
2433
- LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, {
2434
- bucket,
2435
- key,
2436
- });
3185
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
2437
3186
  const isInitial = !this.getMeasureStorage.has(bucket);
2438
- const stateStorage = this.getMeasureStorage(bucket);
2439
- await stateStorage.waitForInit(isInitial);
2440
- if (await stateStorage.hasValue(key)) {
2441
- const data = await stateStorage.readValue(key);
2442
- return data.removed ? null : data;
2443
- }
2444
- return null;
3187
+ const instance = this.getMeasureStorage(bucket);
3188
+ await instance.waitForInit(isInitial);
3189
+ return instance.readMeasureData(key);
2445
3190
  };
2446
3191
  /**
2447
- * Writes measure data to disk with atomic file writes.
3192
+ * Writes a measure entry to the given bucket under the given key.
3193
+ * Lazily initializes the bucket instance on first access.
2448
3194
  *
2449
3195
  * @param data - Data to cache
2450
- * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2451
- * @param key - Dynamic cache key within the bucket
3196
+ * @param bucket - Storage bucket identifier
3197
+ * @param key - Cache key within the bucket
2452
3198
  * @returns Promise that resolves when write is complete
2453
3199
  */
2454
3200
  this.writeMeasureData = async (data, bucket, key) => {
2455
- LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, {
2456
- bucket,
2457
- key,
2458
- });
3201
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
2459
3202
  const isInitial = !this.getMeasureStorage.has(bucket);
2460
- const stateStorage = this.getMeasureStorage(bucket);
2461
- await stateStorage.waitForInit(isInitial);
2462
- await stateStorage.writeValue(key, data);
3203
+ const instance = this.getMeasureStorage(bucket);
3204
+ await instance.waitForInit(isInitial);
3205
+ return instance.writeMeasureData(data, key);
2463
3206
  };
2464
3207
  /**
2465
- * Marks a cached entry as removed (soft delete file is kept on disk).
2466
- * After this call `readMeasureData` for the same key returns `null`.
3208
+ * Soft-deletes a measure entry in the given bucket by setting `removed: true`.
3209
+ * Lazily initializes the bucket instance on first access.
2467
3210
  *
2468
- * @param bucket - Storage bucket
2469
- * @param key - Dynamic cache key within the bucket
3211
+ * @param bucket - Storage bucket identifier
3212
+ * @param key - Cache key within the bucket
2470
3213
  * @returns Promise that resolves when removal is complete
2471
3214
  */
2472
3215
  this.removeMeasureData = async (bucket, key) => {
2473
- LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, {
2474
- bucket,
2475
- key,
2476
- });
3216
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
2477
3217
  const isInitial = !this.getMeasureStorage.has(bucket);
2478
- const stateStorage = this.getMeasureStorage(bucket);
2479
- await stateStorage.waitForInit(isInitial);
2480
- const data = await stateStorage.readValue(key);
2481
- if (data) {
2482
- await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2483
- }
3218
+ const instance = this.getMeasureStorage(bucket);
3219
+ await instance.waitForInit(isInitial);
3220
+ return instance.removeMeasureData(key);
2484
3221
  };
2485
3222
  }
2486
3223
  /**
2487
- * Registers a custom persistence adapter.
3224
+ * Registers a custom IPersistMeasureInstance constructor.
3225
+ * Clears the memoization cache so subsequent calls use the new adapter.
2488
3226
  *
2489
- * @param Ctor - Custom PersistBase constructor
3227
+ * @param Ctor - Custom IPersistMeasureInstance constructor
2490
3228
  */
2491
3229
  usePersistMeasureAdapter(Ctor) {
2492
3230
  LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
2493
- this.PersistMeasureFactory = Ctor;
3231
+ this.PersistMeasureInstanceCtor = Ctor;
3232
+ this.getMeasureStorage.clear();
2494
3233
  }
2495
3234
  /**
2496
- * Async generator yielding all non-removed entity keys for a given bucket.
2497
- * Used by `CacheFileInstance.clear()` to iterate and soft-delete all entries.
3235
+ * Iterates all non-removed measure entries for the given bucket.
3236
+ * Lazily initializes the bucket instance on first access.
2498
3237
  *
2499
- * @param bucket - Storage bucket
2500
- * @returns AsyncGenerator yielding entity keys
3238
+ * @param bucket - Storage bucket identifier
3239
+ * @returns AsyncGenerator yielding entry keys
2501
3240
  */
2502
3241
  async *listMeasureData(bucket) {
2503
3242
  LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2504
3243
  const isInitial = !this.getMeasureStorage.has(bucket);
2505
- const stateStorage = this.getMeasureStorage(bucket);
2506
- await stateStorage.waitForInit(isInitial);
2507
- for await (const key of stateStorage.keys()) {
2508
- const data = await stateStorage.readValue(String(key));
2509
- if (data === null || data.removed) {
2510
- continue;
2511
- }
2512
- yield String(key);
2513
- }
3244
+ const instance = this.getMeasureStorage(bucket);
3245
+ await instance.waitForInit(isInitial);
3246
+ yield* instance.listMeasureData();
2514
3247
  }
2515
- ;
2516
3248
  /**
2517
- * Clears the memoized storage cache.
2518
- * Call this when process.cwd() changes between strategy iterations
2519
- * so new storage instances are created with the updated base path.
3249
+ * Clears the memoized bucket instance cache.
3250
+ * Call when process.cwd() changes between strategy iterations.
2520
3251
  */
2521
3252
  clear() {
2522
3253
  LOGGER_SERVICE$7.log(PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR);
2523
3254
  this.getMeasureStorage.clear();
2524
3255
  }
2525
3256
  /**
2526
- * Switches to the default JSON persist adapter.
3257
+ * Switches to the default file-based PersistMeasureInstance.
2527
3258
  */
2528
3259
  useJson() {
2529
3260
  LOGGER_SERVICE$7.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
2530
- this.usePersistMeasureAdapter(PersistBase);
3261
+ this.usePersistMeasureAdapter(PersistMeasureInstance);
2531
3262
  }
2532
3263
  /**
2533
- * Switches to a dummy persist adapter that discards all writes.
3264
+ * Switches to PersistMeasureDummyInstance (all operations are no-ops).
2534
3265
  */
2535
3266
  useDummy() {
2536
3267
  LOGGER_SERVICE$7.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
2537
- this.usePersistMeasureAdapter(PersistDummy);
3268
+ this.usePersistMeasureAdapter(PersistMeasureDummyInstance);
2538
3269
  }
2539
3270
  }
2540
3271
  /**
@@ -2542,6 +3273,129 @@ class PersistMeasureUtils {
2542
3273
  * Used by Cache.file for persistent caching of external API responses.
2543
3274
  */
2544
3275
  const PersistMeasureAdapter = new PersistMeasureUtils();
3276
+ /**
3277
+ * Default file-based implementation of IPersistIntervalInstance.
3278
+ *
3279
+ * Features:
3280
+ * - Wraps PersistBase for atomic JSON writes
3281
+ * - Soft delete via `removed: true` flag
3282
+ * - listIntervalData filters out removed markers
3283
+ *
3284
+ * @example
3285
+ * ```typescript
3286
+ * const instance = new PersistIntervalInstance("my-interval-bucket");
3287
+ * await instance.waitForInit(true);
3288
+ * await instance.writeIntervalData({ id: "x", data: {}, removed: false }, "key1");
3289
+ * const marker = await instance.readIntervalData("key1");
3290
+ * await instance.removeIntervalData("key1");
3291
+ * ```
3292
+ */
3293
+ class PersistIntervalInstance {
3294
+ /**
3295
+ * Creates new interval marker persistence instance.
3296
+ *
3297
+ * @param bucket - Marker bucket identifier
3298
+ */
3299
+ constructor(bucket) {
3300
+ this.bucket = bucket;
3301
+ this._storage = new PersistBase(bucket, `./dump/data/interval/`);
3302
+ }
3303
+ /**
3304
+ * Initializes the underlying PersistBase storage.
3305
+ *
3306
+ * @param initial - Whether this is the first initialization
3307
+ * @returns Promise that resolves when initialization is complete
3308
+ */
3309
+ async waitForInit(initial) {
3310
+ await this._storage.waitForInit(initial);
3311
+ }
3312
+ /**
3313
+ * Reads an interval marker by key. Returns null if marker is missing or soft-deleted.
3314
+ *
3315
+ * @param key - Marker key within the bucket
3316
+ * @returns Promise resolving to stored data, or null
3317
+ */
3318
+ async readIntervalData(key) {
3319
+ if (await this._storage.hasValue(key)) {
3320
+ const data = await this._storage.readValue(key);
3321
+ return data.removed ? null : data;
3322
+ }
3323
+ return null;
3324
+ }
3325
+ /**
3326
+ * Writes an interval marker under the given key.
3327
+ *
3328
+ * @param data - Data to store
3329
+ * @param key - Marker key within the bucket
3330
+ * @returns Promise that resolves when write is complete
3331
+ */
3332
+ async writeIntervalData(data, key) {
3333
+ await this._storage.writeValue(key, data);
3334
+ }
3335
+ /**
3336
+ * Soft-deletes a marker by writing `removed: true` flag while preserving the file.
3337
+ * Subsequent reads will return null, allowing the interval to fire again.
3338
+ *
3339
+ * @param key - Marker key within the bucket
3340
+ * @returns Promise that resolves when removal is complete
3341
+ */
3342
+ async removeIntervalData(key) {
3343
+ const data = await this._storage.readValue(key);
3344
+ if (data) {
3345
+ await this._storage.writeValue(key, Object.assign({}, data, { removed: true }));
3346
+ }
3347
+ }
3348
+ /**
3349
+ * Iterates all markers in the bucket, yielding keys of non-removed markers only.
3350
+ *
3351
+ * @returns AsyncGenerator yielding marker keys
3352
+ */
3353
+ async *listIntervalData() {
3354
+ for await (const key of this._storage.keys()) {
3355
+ const data = await this._storage.readValue(String(key));
3356
+ if (data === null || data.removed) {
3357
+ continue;
3358
+ }
3359
+ yield String(key);
3360
+ }
3361
+ }
3362
+ }
3363
+ /**
3364
+ * No-op IPersistIntervalInstance implementation used by PersistIntervalUtils.useDummy().
3365
+ * All reads return null, all writes/removes are discarded, list yields nothing.
3366
+ */
3367
+ class PersistIntervalDummyInstance {
3368
+ /**
3369
+ * No-op constructor.
3370
+ * Context arguments are accepted to satisfy TPersistIntervalInstanceCtor.
3371
+ */
3372
+ constructor(_bucket) { }
3373
+ /**
3374
+ * No-op initialization.
3375
+ * @returns Promise that resolves immediately
3376
+ */
3377
+ async waitForInit(_initial) { }
3378
+ /**
3379
+ * Always returns null (no interval markers).
3380
+ * @returns Promise resolving to null
3381
+ */
3382
+ async readIntervalData(_key) { return null; }
3383
+ /**
3384
+ * No-op write (discards marker).
3385
+ * @returns Promise that resolves immediately
3386
+ */
3387
+ async writeIntervalData(_data, _key) { }
3388
+ /**
3389
+ * No-op remove.
3390
+ * @returns Promise that resolves immediately
3391
+ */
3392
+ async removeIntervalData(_key) { }
3393
+ /**
3394
+ * Empty generator — yields no markers.
3395
+ * @returns AsyncGenerator that immediately completes
3396
+ */
3397
+ async *listIntervalData() { }
3398
+ }
2545
3399
  /**
2546
3400
  * Persistence layer for Interval.file once-per-interval signal firing.
2547
3401
  *
@@ -2551,124 +3405,108 @@ const PersistMeasureAdapter = new PersistMeasureUtils();
2551
3405
  */
2552
3406
  class PersistIntervalUtils {
2553
3407
  constructor() {
2554
- this.PersistIntervalFactory = PersistBase;
2555
- this.getIntervalStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistIntervalFactory, [
2556
- bucket,
2557
- `./dump/data/interval/`,
2558
- ]));
2559
3408
  /**
2560
- * Reads interval data for a given bucket and key.
3409
+ * Constructor used to create per-bucket interval marker instances.
3410
+ * Replaceable via usePersistIntervalAdapter() / useJson() / useDummy().
3411
+ */
3412
+ this.PersistIntervalInstanceCtor = PersistIntervalInstance;
3413
+ /**
3414
+ * Memoized factory creating one IPersistIntervalInstance per bucket.
3415
+ */
3416
+ this.getIntervalStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistIntervalInstanceCtor, [bucket]));
3417
+ /**
3418
+ * Reads an interval marker from the given bucket by key.
3419
+ * Lazily initializes the bucket instance on first access.
2561
3420
  *
2562
- * @param bucket - Storage bucket (instance name + interval + index)
2563
- * @param key - Entity key within the bucket (symbol + aligned timestamp)
2564
- * @returns Promise resolving to stored value or null if not found
3421
+ * @param bucket - Storage bucket identifier
3422
+ * @param key - Marker key within the bucket
3423
+ * @returns Promise resolving to marker data, or null if not found / soft-deleted
2565
3424
  */
2566
3425
  this.readIntervalData = async (bucket, key) => {
2567
- LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, {
2568
- bucket,
2569
- key,
2570
- });
3426
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, { bucket, key });
2571
3427
  const isInitial = !this.getIntervalStorage.has(bucket);
2572
- const stateStorage = this.getIntervalStorage(bucket);
2573
- await stateStorage.waitForInit(isInitial);
2574
- if (await stateStorage.hasValue(key)) {
2575
- const data = await stateStorage.readValue(key);
2576
- return data.removed ? null : data;
2577
- }
2578
- return null;
3428
+ const instance = this.getIntervalStorage(bucket);
3429
+ await instance.waitForInit(isInitial);
3430
+ return instance.readIntervalData(key);
2579
3431
  };
2580
3432
  /**
2581
- * Writes interval data to disk.
3433
+ * Writes an interval marker to the given bucket under the given key.
3434
+ * Lazily initializes the bucket instance on first access.
2582
3435
  *
2583
3436
  * @param data - Data to store
2584
- * @param bucket - Storage bucket
2585
- * @param key - Entity key within the bucket
3437
+ * @param bucket - Storage bucket identifier
3438
+ * @param key - Marker key within the bucket
2586
3439
  * @returns Promise that resolves when write is complete
2587
3440
  */
2588
3441
  this.writeIntervalData = async (data, bucket, key) => {
2589
- LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, {
2590
- bucket,
2591
- key,
2592
- });
3442
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, { bucket, key });
2593
3443
  const isInitial = !this.getIntervalStorage.has(bucket);
2594
- const stateStorage = this.getIntervalStorage(bucket);
2595
- await stateStorage.waitForInit(isInitial);
2596
- await stateStorage.writeValue(key, data);
3444
+ const instance = this.getIntervalStorage(bucket);
3445
+ await instance.waitForInit(isInitial);
3446
+ return instance.writeIntervalData(data, key);
2597
3447
  };
2598
3448
  /**
2599
- * Marks an interval entry as removed (soft delete file is kept on disk).
2600
- * After this call `readIntervalData` for the same key returns `null`,
2601
- * so the function will fire again on the next `IntervalFileInstance.run` call.
3449
+ * Soft-deletes a marker in the given bucket by setting `removed: true`.
3450
+ * Lazily initializes the bucket instance on first access.
2602
3451
  *
2603
- * @param bucket - Storage bucket
2604
- * @param key - Entity key within the bucket
3452
+ * @param bucket - Storage bucket identifier
3453
+ * @param key - Marker key within the bucket
2605
3454
  * @returns Promise that resolves when removal is complete
2606
3455
  */
2607
3456
  this.removeIntervalData = async (bucket, key) => {
2608
- LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, {
2609
- bucket,
2610
- key,
2611
- });
3457
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, { bucket, key });
2612
3458
  const isInitial = !this.getIntervalStorage.has(bucket);
2613
- const stateStorage = this.getIntervalStorage(bucket);
2614
- await stateStorage.waitForInit(isInitial);
2615
- const data = await stateStorage.readValue(key);
2616
- if (data) {
2617
- await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2618
- }
3459
+ const instance = this.getIntervalStorage(bucket);
3460
+ await instance.waitForInit(isInitial);
3461
+ return instance.removeIntervalData(key);
2619
3462
  };
2620
3463
  }
2621
3464
  /**
2622
- * Registers a custom persistence adapter.
3465
+ * Registers a custom IPersistIntervalInstance constructor.
3466
+ * Clears the memoization cache so subsequent calls use the new adapter.
2623
3467
  *
2624
- * @param Ctor - Custom PersistBase constructor
3468
+ * @param Ctor - Custom IPersistIntervalInstance constructor
2625
3469
  */
2626
3470
  usePersistIntervalAdapter(Ctor) {
2627
3471
  LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
2628
- this.PersistIntervalFactory = Ctor;
3472
+ this.PersistIntervalInstanceCtor = Ctor;
3473
+ this.getIntervalStorage.clear();
2629
3474
  }
2630
3475
  /**
2631
- * Async generator yielding all non-removed entity keys for a given bucket.
2632
- * Used by `IntervalFileInstance.clear()` to iterate and soft-delete all entries.
3476
+ * Iterates all non-removed markers for the given bucket.
3477
+ * Lazily initializes the bucket instance on first access.
2633
3478
  *
2634
- * @param bucket - Storage bucket
2635
- * @returns AsyncGenerator yielding entity keys
3479
+ * @param bucket - Storage bucket identifier
3480
+ * @returns AsyncGenerator yielding marker keys
2636
3481
  */
2637
3482
  async *listIntervalData(bucket) {
2638
3483
  LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2639
3484
  const isInitial = !this.getIntervalStorage.has(bucket);
2640
- const stateStorage = this.getIntervalStorage(bucket);
2641
- await stateStorage.waitForInit(isInitial);
2642
- for await (const key of stateStorage.keys()) {
2643
- const data = await stateStorage.readValue(String(key));
2644
- if (data === null || data.removed) {
2645
- continue;
2646
- }
2647
- yield String(key);
2648
- }
3485
+ const instance = this.getIntervalStorage(bucket);
3486
+ await instance.waitForInit(isInitial);
3487
+ yield* instance.listIntervalData();
2649
3488
  }
2650
- ;
2651
3489
  /**
2652
- * Clears the memoized storage cache.
2653
- * Call this when process.cwd() changes between strategy iterations.
3490
+ * Clears the memoized bucket instance cache.
3491
+ * Call when process.cwd() changes between strategy iterations.
2654
3492
  */
2655
3493
  clear() {
2656
3494
  LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
2657
3495
  this.getIntervalStorage.clear();
2658
3496
  }
2659
3497
  /**
2660
- * Switches to the default JSON persist adapter.
3498
+ * Switches to the default file-based PersistIntervalInstance.
2661
3499
  */
2662
3500
  useJson() {
2663
3501
  LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
2664
- this.usePersistIntervalAdapter(PersistBase);
3502
+ this.usePersistIntervalAdapter(PersistIntervalInstance);
2665
3503
  }
2666
3504
  /**
2667
- * Switches to a dummy persist adapter that discards all writes.
3505
+ * Switches to PersistIntervalDummyInstance (all operations are no-ops).
2668
3506
  */
2669
3507
  useDummy() {
2670
3508
  LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
2671
- this.usePersistIntervalAdapter(PersistDummy);
3509
+ this.usePersistIntervalAdapter(PersistIntervalDummyInstance);
2672
3510
  }
2673
3511
  }
2674
3512
  /**
@@ -2676,6 +3514,154 @@ class PersistIntervalUtils {
2676
3514
  * Used by Interval.file for persistent once-per-interval signal firing.
2677
3515
  */
2678
3516
  const PersistIntervalAdapter = new PersistIntervalUtils();
3517
+ /**
3518
+ * Default file-based implementation of IPersistMemoryInstance.
3519
+ *
3520
+ * Features:
3521
+ * - Wraps PersistBase for atomic JSON writes
3522
+ * - Soft delete via `removed: true` flag
3523
+ * - listMemoryData filters out removed entries
3524
+ * - dispose is a no-op (memo cache is managed by PersistMemoryUtils)
3525
+ *
3526
+ * @example
3527
+ * ```typescript
3528
+ * const instance = new PersistMemoryInstance("signal-1", "context-bucket");
3529
+ * await instance.waitForInit(true);
3530
+ * await instance.writeMemoryData(entryData, "memory-id-1");
3531
+ * const data = await instance.readMemoryData("memory-id-1");
3532
+ * ```
3533
+ */
3534
+ class PersistMemoryInstance {
3535
+ /**
3536
+ * Creates new memory persistence instance.
3537
+ *
3538
+ * @param signalId - Signal identifier (entity folder name)
3539
+ * @param bucketName - Bucket name (subfolder under memory/)
3540
+ */
3541
+ constructor(signalId, bucketName) {
3542
+ this.signalId = signalId;
3543
+ this.bucketName = bucketName;
3544
+ this._storage = new PersistBase(bucketName, `./dump/memory/${signalId}/`);
3545
+ }
3546
+ /**
3547
+ * Initializes the underlying PersistBase storage.
3548
+ *
3549
+ * @param initial - Whether this is the first initialization
3550
+ * @returns Promise that resolves when initialization is complete
3551
+ */
3552
+ async waitForInit(initial) {
3553
+ await this._storage.waitForInit(initial);
3554
+ }
3555
+ /**
3556
+ * Reads a memory entry by id. Returns null if entry is missing or soft-deleted.
3557
+ *
3558
+ * @param memoryId - Memory entry identifier
3559
+ * @returns Promise resolving to entry data, or null
3560
+ */
3561
+ async readMemoryData(memoryId) {
3562
+ if (await this._storage.hasValue(memoryId)) {
3563
+ const data = await this._storage.readValue(memoryId);
3564
+ return data.removed ? null : data;
3565
+ }
3566
+ return null;
3567
+ }
3568
+ /**
3569
+ * Checks whether a memory entry exists on disk (regardless of removed flag).
3570
+ *
3571
+ * @param memoryId - Memory entry identifier
3572
+ * @returns Promise resolving to true if entry file exists
3573
+ */
3574
+ async hasMemoryData(memoryId) {
3575
+ return await this._storage.hasValue(memoryId);
3576
+ }
3577
+ /**
3578
+ * Writes a memory entry under the given id.
3579
+ *
3580
+ * @param data - Entry data to persist
3581
+ * @param memoryId - Memory entry identifier
3582
+ * @returns Promise that resolves when write is complete
3583
+ */
3584
+ async writeMemoryData(data, memoryId) {
3585
+ await this._storage.writeValue(memoryId, data);
3586
+ }
3587
+ /**
3588
+ * Soft-deletes a memory entry by writing `removed: true` flag.
3589
+ *
3590
+ * @param memoryId - Memory entry identifier
3591
+ * @returns Promise that resolves when removal is complete
3592
+ */
3593
+ async removeMemoryData(memoryId) {
3594
+ const data = await this._storage.readValue(memoryId);
3595
+ if (data) {
3596
+ await this._storage.writeValue(memoryId, Object.assign({}, data, { removed: true }));
3597
+ }
3598
+ }
3599
+ /**
3600
+ * Iterates all memory entries in the bucket, yielding id + data tuples
3601
+ * for non-removed entries only.
3602
+ *
3603
+ * @returns AsyncGenerator yielding `{ memoryId, data }` tuples
3604
+ */
3605
+ async *listMemoryData() {
3606
+ for await (const memoryId of this._storage.keys()) {
3607
+ const data = await this._storage.readValue(String(memoryId));
3608
+ if (data === null || data.removed) {
3609
+ continue;
3610
+ }
3611
+ yield { memoryId: String(memoryId), data };
3612
+ }
3613
+ }
3614
+ /**
3615
+ * No-op for the default file-based implementation.
3616
+ * Resource cleanup (memo cache invalidation) is handled by PersistMemoryUtils.dispose().
3617
+ */
3618
+ dispose() { }
3619
+ }
3620
+ /**
3621
+ * No-op IPersistMemoryInstance implementation used by PersistMemoryUtils.useDummy().
3622
+ * All reads return null/false, all writes/removes are discarded, list yields nothing.
3623
+ */
3624
+ class PersistMemoryDummyInstance {
3625
+ /**
3626
+ * No-op constructor.
3627
+ * Context arguments are accepted to satisfy TPersistMemoryInstanceCtor.
3628
+ */
3629
+ constructor(_signalId, _bucketName) { }
3630
+ /**
3631
+ * No-op initialization.
3632
+ * @returns Promise that resolves immediately
3633
+ */
3634
+ async waitForInit(_initial) { }
3635
+ /**
3636
+ * Always returns null (no memory entries).
3637
+ * @returns Promise resolving to null
3638
+ */
3639
+ async readMemoryData(_memoryId) { return null; }
3640
+ /**
3641
+ * Always returns false (no memory entries exist).
3642
+ * @returns Promise resolving to false
3643
+ */
3644
+ async hasMemoryData(_memoryId) { return false; }
3645
+ /**
3646
+ * No-op write (discards entry).
3647
+ * @returns Promise that resolves immediately
3648
+ */
3649
+ async writeMemoryData(_data, _memoryId) { }
3650
+ /**
3651
+ * No-op remove.
3652
+ * @returns Promise that resolves immediately
3653
+ */
3654
+ async removeMemoryData(_memoryId) { }
3655
+ /**
3656
+ * Empty generator — yields no entries.
3657
+ * @returns AsyncGenerator that immediately completes
3658
+ */
3659
+ async *listMemoryData() { }
3660
+ /**
3661
+ * No-op dispose.
3662
+ */
3663
+ dispose() { }
3664
+ }
2679
3665
  /**
2680
3666
  * Utility class for managing memory entry persistence.
2681
3667
  *
@@ -2691,51 +3677,50 @@ const PersistIntervalAdapter = new PersistIntervalUtils();
2691
3677
  */
2692
3678
  class PersistMemoryUtils {
2693
3679
  constructor() {
2694
- this.PersistMemoryFactory = PersistBase;
2695
- this.getMemoryStorage = functoolsKit.memoize(([signalId, bucketName]) => `${signalId}:${bucketName}`, (signalId, bucketName) => Reflect.construct(this.PersistMemoryFactory, [
2696
- bucketName,
2697
- `./dump/memory/${signalId}/`,
2698
- ]));
2699
3680
  /**
2700
- * Initializes the storage for a given (signalId, bucketName) pair.
3681
+ * Constructor used to create per-context memory instances.
3682
+ * Replaceable via usePersistMemoryAdapter() / useJson() / useDummy().
3683
+ */
3684
+ this.PersistMemoryInstanceCtor = PersistMemoryInstance;
3685
+ /**
3686
+ * Memoized factory creating one IPersistMemoryInstance per (signalId, bucketName) pair.
3687
+ */
3688
+ this.getMemoryStorage = functoolsKit.memoize(([signalId, bucketName]) => `${signalId}:${bucketName}`, (signalId, bucketName) => Reflect.construct(this.PersistMemoryInstanceCtor, [signalId, bucketName]));
3689
+ /**
3690
+ * Initializes the memory storage for the given context.
3691
+ * Skips initialization when `initial` is false (used to gate first-time setup).
2701
3692
  *
2702
- * @param signalId - Signal identifier (entity folder name)
2703
- * @param bucketName - Bucket name (subfolder under memory/)
3693
+ * @param signalId - Signal identifier
3694
+ * @param bucketName - Bucket name
2704
3695
  * @param initial - Whether this is the first initialization
2705
3696
  * @returns Promise that resolves when initialization is complete
2706
3697
  */
2707
3698
  this.waitForInit = async (signalId, bucketName, initial) => {
2708
3699
  const key = `${signalId}:${bucketName}`;
2709
3700
  const isInitial = initial && !this.getMemoryStorage.has(key);
2710
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2711
- await stateStorage.waitForInit(isInitial);
3701
+ const instance = this.getMemoryStorage(signalId, bucketName);
3702
+ await instance.waitForInit(isInitial);
2712
3703
  };
2713
3704
  /**
2714
- * Reads a memory entry from persistence storage.
3705
+ * Reads a memory entry for the given context and id.
3706
+ * Lazily initializes the instance on first access.
2715
3707
  *
2716
3708
  * @param signalId - Signal identifier
2717
3709
  * @param bucketName - Bucket name
2718
3710
  * @param memoryId - Memory entry identifier
2719
- * @returns Promise resolving to entry data or null if not found
3711
+ * @returns Promise resolving to entry data, or null if not found / soft-deleted
2720
3712
  */
2721
3713
  this.readMemoryData = async (signalId, bucketName, memoryId) => {
2722
- LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, {
2723
- signalId,
2724
- bucketName,
2725
- memoryId,
2726
- });
3714
+ LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName, memoryId });
2727
3715
  const key = `${signalId}:${bucketName}`;
2728
3716
  const isInitial = !this.getMemoryStorage.has(key);
2729
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2730
- await stateStorage.waitForInit(isInitial);
2731
- if (await stateStorage.hasValue(memoryId)) {
2732
- const data = await stateStorage.readValue(memoryId);
2733
- return data.removed ? null : data;
2734
- }
2735
- return null;
3717
+ const instance = this.getMemoryStorage(signalId, bucketName);
3718
+ await instance.waitForInit(isInitial);
3719
+ return instance.readMemoryData(memoryId);
2736
3720
  };
2737
3721
  /**
2738
- * Checks if a memory entry exists in persistence storage.
3722
+ * Checks whether a memory entry exists on disk for the given context.
3723
+ * Lazily initializes the instance on first access.
2739
3724
  *
2740
3725
  * @param signalId - Signal identifier
2741
3726
  * @param bucketName - Bucket name
@@ -2743,19 +3728,16 @@ class PersistMemoryUtils {
2743
3728
  * @returns Promise resolving to true if entry exists
2744
3729
  */
2745
3730
  this.hasMemoryData = async (signalId, bucketName, memoryId) => {
2746
- LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, {
2747
- signalId,
2748
- bucketName,
2749
- memoryId,
2750
- });
3731
+ LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_HAS_DATA, { signalId, bucketName, memoryId });
2751
3732
  const key = `${signalId}:${bucketName}`;
2752
3733
  const isInitial = !this.getMemoryStorage.has(key);
2753
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2754
- await stateStorage.waitForInit(isInitial);
2755
- return await stateStorage.hasValue(memoryId);
3734
+ const instance = this.getMemoryStorage(signalId, bucketName);
3735
+ await instance.waitForInit(isInitial);
3736
+ return instance.hasMemoryData(memoryId);
2756
3737
  };
2757
3738
  /**
2758
- * Writes a memory entry to disk with atomic file writes.
3739
+ * Writes a memory entry for the given context.
3740
+ * Lazily initializes the instance on first access.
2759
3741
  *
2760
3742
  * @param data - Entry data to persist
2761
3743
  * @param signalId - Signal identifier
@@ -2764,19 +3746,16 @@ class PersistMemoryUtils {
2764
3746
  * @returns Promise that resolves when write is complete
2765
3747
  */
2766
3748
  this.writeMemoryData = async (data, signalId, bucketName, memoryId) => {
2767
- LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, {
2768
- signalId,
2769
- bucketName,
2770
- memoryId,
2771
- });
3749
+ LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName, memoryId });
2772
3750
  const key = `${signalId}:${bucketName}`;
2773
3751
  const isInitial = !this.getMemoryStorage.has(key);
2774
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2775
- await stateStorage.waitForInit(isInitial);
2776
- await stateStorage.writeValue(memoryId, data);
3752
+ const instance = this.getMemoryStorage(signalId, bucketName);
3753
+ await instance.waitForInit(isInitial);
3754
+ return instance.writeMemoryData(data, memoryId);
2777
3755
  };
2778
3756
  /**
2779
- * Marks a memory entry as removed (soft delete — file is kept on disk).
3757
+ * Soft-deletes a memory entry for the given context.
3758
+ * Lazily initializes the instance on first access.
2780
3759
  *
2781
3760
  * @param signalId - Signal identifier
2782
3761
  * @param bucketName - Bucket name
@@ -2784,36 +3763,27 @@ class PersistMemoryUtils {
2784
3763
  * @returns Promise that resolves when removal is complete
2785
3764
  */
2786
3765
  this.removeMemoryData = async (signalId, bucketName, memoryId) => {
2787
- LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, {
2788
- signalId,
2789
- bucketName,
2790
- memoryId,
2791
- });
3766
+ LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_REMOVE_DATA, { signalId, bucketName, memoryId });
2792
3767
  const key = `${signalId}:${bucketName}`;
2793
3768
  const isInitial = !this.getMemoryStorage.has(key);
2794
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2795
- await stateStorage.waitForInit(isInitial);
2796
- const data = await stateStorage.readValue(memoryId);
2797
- if (data) {
2798
- await stateStorage.writeValue(memoryId, Object.assign({}, data, { removed: true }));
2799
- }
3769
+ const instance = this.getMemoryStorage(signalId, bucketName);
3770
+ await instance.waitForInit(isInitial);
3771
+ return instance.removeMemoryData(memoryId);
2800
3772
  };
2801
3773
  /**
2802
- * Clears the memoized storage cache.
2803
- * Call this when process.cwd() changes between strategy iterations
2804
- * so new storage instances are created with the updated base path.
3774
+ * Clears the memoized instance cache.
3775
+ * Call when process.cwd() changes between strategy iterations.
2805
3776
  */
2806
3777
  this.clear = () => {
2807
3778
  LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_CLEAR);
2808
3779
  this.getMemoryStorage.clear();
2809
3780
  };
2810
3781
  /**
2811
- * Disposes of the memory adapter and releases any resources.
2812
- * Call this when a signal is removed to clean up its associated storage.
3782
+ * Drops the memoized instance for the given context.
3783
+ * Call when a signal is removed to clean up its associated storage entry.
2813
3784
  *
2814
3785
  * @param signalId - Signal identifier
2815
3786
  * @param bucketName - Bucket name
2816
- * @returns void
2817
3787
  */
2818
3788
  this.dispose = (signalId, bucketName) => {
2819
3789
  LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_DISPOSE);
@@ -2822,64 +3792,46 @@ class PersistMemoryUtils {
2822
3792
  };
2823
3793
  }
2824
3794
  /**
2825
- * Registers a custom persistence adapter.
3795
+ * Registers a custom IPersistMemoryInstance constructor.
3796
+ * Clears the memoization cache so subsequent calls use the new adapter.
2826
3797
  *
2827
- * @param Ctor - Custom PersistBase constructor
2828
- *
2829
- * @example
2830
- * ```typescript
2831
- * class RedisPersist extends PersistBase {
2832
- * async readValue(id) { return JSON.parse(await redis.get(id)); }
2833
- * async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
2834
- * }
2835
- * PersistMemoryAdapter.usePersistMemoryAdapter(RedisPersist);
2836
- * ```
3798
+ * @param Ctor - Custom IPersistMemoryInstance constructor
2837
3799
  */
2838
3800
  usePersistMemoryAdapter(Ctor) {
2839
3801
  LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER);
2840
- this.PersistMemoryFactory = Ctor;
3802
+ this.PersistMemoryInstanceCtor = Ctor;
3803
+ this.getMemoryStorage.clear();
2841
3804
  }
2842
3805
  /**
2843
- * Lists all memory entry IDs for a given (signalId, bucketName) pair.
3806
+ * Iterates all non-removed memory entries for the given context.
2844
3807
  * Used by MemoryPersistInstance to rebuild the BM25 index on init.
3808
+ * Lazily initializes the instance on first access.
2845
3809
  *
2846
3810
  * @param signalId - Signal identifier
2847
3811
  * @param bucketName - Bucket name
2848
- * @returns AsyncGenerator yielding memory entry IDs
3812
+ * @returns AsyncGenerator yielding `{ memoryId, data }` tuples
2849
3813
  */
2850
3814
  async *listMemoryData(signalId, bucketName) {
2851
- LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, {
2852
- signalId,
2853
- bucketName,
2854
- });
3815
+ LOGGER_SERVICE$7.info(PERSIST_MEMORY_UTILS_METHOD_NAME_LIST_DATA, { signalId, bucketName });
2855
3816
  const key = `${signalId}:${bucketName}`;
2856
3817
  const isInitial = !this.getMemoryStorage.has(key);
2857
- const stateStorage = this.getMemoryStorage(signalId, bucketName);
2858
- await stateStorage.waitForInit(isInitial);
2859
- for await (const memoryId of stateStorage.keys()) {
2860
- const data = await stateStorage.readValue(String(memoryId));
2861
- if (data === null || data.removed) {
2862
- continue;
2863
- }
2864
- yield { memoryId: String(memoryId), data };
2865
- }
3818
+ const instance = this.getMemoryStorage(signalId, bucketName);
3819
+ await instance.waitForInit(isInitial);
3820
+ yield* instance.listMemoryData();
2866
3821
  }
2867
- ;
2868
3822
  /**
2869
- * Switches to the default JSON persist adapter.
2870
- * All future persistence writes will use JSON storage.
3823
+ * Switches to the default file-based PersistMemoryInstance.
2871
3824
  */
2872
3825
  useJson() {
2873
3826
  LOGGER_SERVICE$7.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_JSON);
2874
- this.usePersistMemoryAdapter(PersistBase);
3827
+ this.usePersistMemoryAdapter(PersistMemoryInstance);
2875
3828
  }
2876
3829
  /**
2877
- * Switches to a dummy persist adapter that discards all writes.
2878
- * All future persistence writes will be no-ops.
3830
+ * Switches to PersistMemoryDummyInstance (all operations are no-ops).
2879
3831
  */
2880
3832
  useDummy() {
2881
3833
  LOGGER_SERVICE$7.log(PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_DUMMY);
2882
- this.usePersistMemoryAdapter(PersistDummy);
3834
+ this.usePersistMemoryAdapter(PersistMemoryDummyInstance);
2883
3835
  }
2884
3836
  }
2885
3837
  /**
@@ -2899,6 +3851,100 @@ class PersistMemoryUtils {
2899
3851
  * ```
2900
3852
  */
2901
3853
  const PersistMemoryAdapter = new PersistMemoryUtils();
3854
+ /**
3855
+ * Default file-based implementation of IPersistRecentInstance.
3856
+ *
3857
+ * Features:
3858
+ * - Wraps PersistBase for atomic JSON writes
3859
+ * - Uses symbol as entity ID within a per-context PersistBase
3860
+ * - Context key includes backtest/live mode and optional frameName
3861
+ *
3862
+ * @example
3863
+ * ```typescript
3864
+ * const instance = new PersistRecentInstance("BTCUSDT", "my-strategy", "binance", "frame-1", false);
3865
+ * await instance.waitForInit(true);
3866
+ * await instance.writeRecentData(publicSignalRow);
3867
+ * const recent = await instance.readRecentData();
3868
+ * ```
3869
+ */
3870
+ class PersistRecentInstance {
3871
+ /**
3872
+ * Creates new recent signal persistence instance.
3873
+ *
3874
+ * @param symbol - Trading pair symbol
3875
+ * @param strategyName - Strategy identifier
3876
+ * @param exchangeName - Exchange identifier
3877
+ * @param frameName - Frame identifier (may be empty for live mode)
3878
+ * @param backtest - True for backtest mode, false for live mode
3879
+ */
3880
+ constructor(symbol, strategyName, exchangeName, frameName, backtest) {
3881
+ this.symbol = symbol;
3882
+ this.strategyName = strategyName;
3883
+ this.exchangeName = exchangeName;
3884
+ this.frameName = frameName;
3885
+ this.backtest = backtest;
3886
+ const parts = [symbol, strategyName, exchangeName];
3887
+ if (frameName)
3888
+ parts.push(frameName);
3889
+ parts.push(backtest ? "backtest" : "live");
3890
+ this._storage = new PersistBase(parts.join("_"), `./dump/data/recent/`);
3891
+ }
3892
+ /**
3893
+ * Initializes the underlying PersistBase storage.
3894
+ *
3895
+ * @param initial - Whether this is the first initialization
3896
+ * @returns Promise that resolves when initialization is complete
3897
+ */
3898
+ async waitForInit(initial) {
3899
+ await this._storage.waitForInit(initial);
3900
+ }
3901
+ /**
3902
+ * Reads the persisted recent signal using `symbol` as the entity key.
3903
+ *
3904
+ * @returns Promise resolving to recent signal or null if not found
3905
+ */
3906
+ async readRecentData() {
3907
+ if (await this._storage.hasValue(this.symbol)) {
3908
+ return await this._storage.readValue(this.symbol);
3909
+ }
3910
+ return null;
3911
+ }
3912
+ /**
3913
+ * Writes the recent signal using `symbol` as the entity key.
3914
+ *
3915
+ * @param signalRow - Recent signal data to persist
3916
+ * @returns Promise that resolves when write is complete
3917
+ */
3918
+ async writeRecentData(signalRow) {
3919
+ await this._storage.writeValue(this.symbol, signalRow);
3920
+ }
3921
+ }
3922
+ /**
3923
+ * No-op IPersistRecentInstance implementation used by PersistRecentUtils.useDummy().
3924
+ * All reads return null, all writes are discarded.
3925
+ */
3926
+ class PersistRecentDummyInstance {
3927
+ /**
3928
+ * No-op constructor.
3929
+ * Context arguments are accepted to satisfy TPersistRecentInstanceCtor.
3930
+ */
3931
+ constructor(_symbol, _strategyName, _exchangeName, _frameName, _backtest) { }
3932
+ /**
3933
+ * No-op initialization.
3934
+ * @returns Promise that resolves immediately
3935
+ */
3936
+ async waitForInit(_initial) { }
3937
+ /**
3938
+ * Always returns null (no recent signal).
3939
+ * @returns Promise resolving to null
3940
+ */
3941
+ async readRecentData() { return null; }
3942
+ /**
3943
+ * No-op write (discards recent signal).
3944
+ * @returns Promise that resolves immediately
3945
+ */
3946
+ async writeRecentData(_signalRow) { }
3947
+ }
2902
3948
  /**
2903
3949
  * Utility class for managing recent signal persistence.
2904
3950
  *
@@ -2912,95 +3958,105 @@ const PersistMemoryAdapter = new PersistMemoryUtils();
2912
3958
  */
2913
3959
  class PersistRecentUtils {
2914
3960
  constructor() {
2915
- this.PersistRecentFactory = PersistBase;
2916
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => this.createKeyParts(symbol, strategyName, exchangeName, frameName, backtest).join(":"), (symbol, strategyName, exchangeName, frameName, backtest) => Reflect.construct(this.PersistRecentFactory, [
2917
- this.createKeyParts(symbol, strategyName, exchangeName, frameName, backtest).join("_"),
2918
- `./dump/data/recent/`,
2919
- ]));
2920
3961
  /**
2921
- * Reads the latest persisted recent signal for a given context.
2922
- *
2923
- * Returns null if no recent signal exists.
3962
+ * Constructor used to create per-context recent signal instances.
3963
+ * Replaceable via usePersistRecentAdapter() / useJson() / useDummy().
3964
+ */
3965
+ this.PersistRecentInstanceCtor = PersistRecentInstance;
3966
+ /**
3967
+ * Memoized factory creating one IPersistRecentInstance per context tuple.
3968
+ */
3969
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => this.createKey(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => Reflect.construct(this.PersistRecentInstanceCtor, [symbol, strategyName, exchangeName, frameName, backtest]));
3970
+ /**
3971
+ * Reads the latest recent signal for the given context.
3972
+ * Lazily initializes the instance on first access.
2924
3973
  *
2925
3974
  * @param symbol - Trading pair symbol
2926
3975
  * @param strategyName - Strategy identifier
2927
3976
  * @param exchangeName - Exchange identifier
2928
- * @param frameName - Frame identifier
2929
- * @returns Promise resolving to recent signal or null
3977
+ * @param frameName - Frame identifier (may be empty)
3978
+ * @param backtest - True for backtest mode, false for live mode
3979
+ * @returns Promise resolving to recent signal or null if none persisted
2930
3980
  */
2931
3981
  this.readRecentData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
2932
3982
  LOGGER_SERVICE$7.info(PERSIST_RECENT_UTILS_METHOD_NAME_READ_DATA);
2933
- const key = this.createKeyParts(symbol, strategyName, exchangeName, frameName, backtest).join(":");
3983
+ const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
2934
3984
  const isInitial = !this.getStorage.has(key);
2935
- const stateStorage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
2936
- await stateStorage.waitForInit(isInitial);
2937
- if (await stateStorage.hasValue(symbol)) {
2938
- return await stateStorage.readValue(symbol);
2939
- }
2940
- return null;
3985
+ const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
3986
+ await instance.waitForInit(isInitial);
3987
+ return instance.readRecentData();
2941
3988
  };
2942
3989
  /**
2943
- * Writes the latest recent signal to disk with atomic file writes.
2944
- *
2945
- * Uses symbol as the entity ID within the per-context storage instance.
2946
- * Uses atomic writes to prevent corruption on crashes.
3990
+ * Writes the latest recent signal for the given context.
3991
+ * Lazily initializes the instance on first access.
2947
3992
  *
2948
3993
  * @param signalRow - Recent signal data to persist
2949
3994
  * @param symbol - Trading pair symbol
2950
3995
  * @param strategyName - Strategy identifier
2951
3996
  * @param exchangeName - Exchange identifier
2952
- * @param frameName - Frame identifier
3997
+ * @param frameName - Frame identifier (may be empty)
3998
+ * @param backtest - True for backtest mode, false for live mode
2953
3999
  * @returns Promise that resolves when write is complete
2954
4000
  */
2955
4001
  this.writeRecentData = async (signalRow, symbol, strategyName, exchangeName, frameName, backtest) => {
2956
4002
  LOGGER_SERVICE$7.info(PERSIST_RECENT_UTILS_METHOD_NAME_WRITE_DATA);
2957
- const key = this.createKeyParts(symbol, strategyName, exchangeName, frameName, backtest).join(":");
4003
+ const key = this.createKey(symbol, strategyName, exchangeName, frameName, backtest);
2958
4004
  const isInitial = !this.getStorage.has(key);
2959
- const stateStorage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
2960
- await stateStorage.waitForInit(isInitial);
2961
- await stateStorage.writeValue(symbol, signalRow);
4005
+ const instance = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
4006
+ await instance.waitForInit(isInitial);
4007
+ return instance.writeRecentData(signalRow);
2962
4008
  };
2963
4009
  }
2964
- createKeyParts(symbol, strategyName, exchangeName, frameName, backtest) {
4010
+ /**
4011
+ * Builds the composite memoization key for a recent signal context.
4012
+ * Includes optional frameName and the backtest/live mode flag.
4013
+ *
4014
+ * @param symbol - Trading pair symbol
4015
+ * @param strategyName - Strategy identifier
4016
+ * @param exchangeName - Exchange identifier
4017
+ * @param frameName - Frame identifier (omitted from key if empty)
4018
+ * @param backtest - True for backtest mode, false for live mode
4019
+ * @returns Composite key string
4020
+ */
4021
+ createKey(symbol, strategyName, exchangeName, frameName, backtest) {
2965
4022
  const parts = [symbol, strategyName, exchangeName];
2966
4023
  if (frameName)
2967
4024
  parts.push(frameName);
2968
4025
  parts.push(backtest ? "backtest" : "live");
2969
- return parts;
4026
+ return parts.join(":");
2970
4027
  }
2971
4028
  /**
2972
- * Registers a custom persistence adapter.
4029
+ * Registers a custom IPersistRecentInstance constructor.
4030
+ * Clears the memoization cache so subsequent calls use the new adapter.
2973
4031
  *
2974
- * @param Ctor - Custom PersistBase constructor
4032
+ * @param Ctor - Custom IPersistRecentInstance constructor
2975
4033
  */
2976
4034
  usePersistRecentAdapter(Ctor) {
2977
4035
  LOGGER_SERVICE$7.info(PERSIST_RECENT_UTILS_METHOD_NAME_USE_PERSIST_RECENT_ADAPTER);
2978
- this.PersistRecentFactory = Ctor;
4036
+ this.PersistRecentInstanceCtor = Ctor;
4037
+ this.getStorage.clear();
2979
4038
  }
2980
4039
  /**
2981
- * Clears the memoized storage cache.
2982
- * Call this when process.cwd() changes between strategy iterations
2983
- * so new storage instances are created with the updated base path.
4040
+ * Clears the memoized instance cache.
4041
+ * Call when process.cwd() changes between strategy iterations.
2984
4042
  */
2985
4043
  clear() {
2986
4044
  LOGGER_SERVICE$7.log(PERSIST_RECENT_UTILS_METHOD_NAME_CLEAR);
2987
4045
  this.getStorage.clear();
2988
4046
  }
2989
4047
  /**
2990
- * Switches to the default JSON persist adapter.
2991
- * All future persistence writes will use JSON storage.
4048
+ * Switches to the default file-based PersistRecentInstance.
2992
4049
  */
2993
4050
  useJson() {
2994
4051
  LOGGER_SERVICE$7.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_JSON);
2995
- this.usePersistRecentAdapter(PersistBase);
4052
+ this.usePersistRecentAdapter(PersistRecentInstance);
2996
4053
  }
2997
4054
  /**
2998
- * Switches to a dummy persist adapter that discards all writes.
2999
- * All future persistence writes will be no-ops.
4055
+ * Switches to PersistRecentDummyInstance (all operations are no-ops).
3000
4056
  */
3001
4057
  useDummy() {
3002
4058
  LOGGER_SERVICE$7.log(PERSIST_RECENT_UTILS_METHOD_NAME_USE_DUMMY);
3003
- this.usePersistRecentAdapter(PersistDummy);
4059
+ this.usePersistRecentAdapter(PersistRecentDummyInstance);
3004
4060
  }
3005
4061
  }
3006
4062
  /**
@@ -3008,6 +4064,99 @@ class PersistRecentUtils {
3008
4064
  * Used by RecentPersistBacktestUtils/RecentPersistLiveUtils for recent signal persistence.
3009
4065
  */
3010
4066
  const PersistRecentAdapter = new PersistRecentUtils();
4067
+ /**
4068
+ * Default file-based implementation of IPersistStateInstance.
4069
+ *
4070
+ * Features:
4071
+ * - Wraps PersistBase for atomic JSON writes
4072
+ * - Uses bucketName as entity ID within a per-signal PersistBase
4073
+ * - dispose is a no-op (memo cache is managed by PersistStateUtils)
4074
+ *
4075
+ * @example
4076
+ * ```typescript
4077
+ * const instance = new PersistStateInstance("signal-1", "counter");
4078
+ * await instance.waitForInit(true);
4079
+ * await instance.writeStateData({ id: "counter", data: { count: 1 } });
4080
+ * const state = await instance.readStateData();
4081
+ * ```
4082
+ */
4083
+ class PersistStateInstance {
4084
+ /**
4085
+ * Creates new state persistence instance.
4086
+ *
4087
+ * @param signalId - Signal identifier (folder name under state/)
4088
+ * @param bucketName - Bucket name (file name)
4089
+ */
4090
+ constructor(signalId, bucketName) {
4091
+ this.signalId = signalId;
4092
+ this.bucketName = bucketName;
4093
+ this._storage = new PersistBase(bucketName, `./dump/state/${signalId}/`);
4094
+ }
4095
+ /**
4096
+ * Initializes the underlying PersistBase storage.
4097
+ *
4098
+ * @param initial - Whether this is the first initialization
4099
+ * @returns Promise that resolves when initialization is complete
4100
+ */
4101
+ async waitForInit(initial) {
4102
+ await this._storage.waitForInit(initial);
4103
+ }
4104
+ /**
4105
+ * Reads the persisted state using `bucketName` as the entity key.
4106
+ *
4107
+ * @returns Promise resolving to state data or null if not found
4108
+ */
4109
+ async readStateData() {
4110
+ if (await this._storage.hasValue(this.bucketName)) {
4111
+ return await this._storage.readValue(this.bucketName);
4112
+ }
4113
+ return null;
4114
+ }
4115
+ /**
4116
+ * Writes the state using `bucketName` as the entity key.
4117
+ *
4118
+ * @param data - State data to persist
4119
+ * @returns Promise that resolves when write is complete
4120
+ */
4121
+ async writeStateData(data) {
4122
+ await this._storage.writeValue(this.bucketName, data);
4123
+ }
4124
+ /**
4125
+ * No-op for the default file-based implementation.
4126
+ * Resource cleanup (memo cache invalidation) is handled by PersistStateUtils.dispose().
4127
+ */
4128
+ dispose() { }
4129
+ }
4130
+ /**
4131
+ * No-op IPersistStateInstance implementation used by PersistStateUtils.useDummy().
4132
+ * All reads return null, all writes are discarded.
4133
+ */
4134
+ class PersistStateDummyInstance {
4135
+ /**
4136
+ * No-op constructor.
4137
+ * Context arguments are accepted to satisfy TPersistStateInstanceCtor.
4138
+ */
4139
+ constructor(_signalId, _bucketName) { }
4140
+ /**
4141
+ * No-op initialization.
4142
+ * @returns Promise that resolves immediately
4143
+ */
4144
+ async waitForInit(_initial) { }
4145
+ /**
4146
+ * Always returns null (no persisted state).
4147
+ * @returns Promise resolving to null
4148
+ */
4149
+ async readStateData() { return null; }
4150
+ /**
4151
+ * No-op write (discards state).
4152
+ * @returns Promise that resolves immediately
4153
+ */
4154
+ async writeStateData(_data) { }
4155
+ /**
4156
+ * No-op dispose.
4157
+ */
4158
+ dispose() { }
4159
+ }
3011
4160
  /**
3012
4161
  * Utility class for managing state persistence.
3013
4162
  *
@@ -3022,96 +4171,89 @@ const PersistRecentAdapter = new PersistRecentUtils();
3022
4171
  */
3023
4172
  class PersistStateUtils {
3024
4173
  constructor() {
3025
- this.PersistStateFactory = PersistBase;
3026
- this.getStateStorage = functoolsKit.memoize(([signalId, bucketName]) => `${signalId}:${bucketName}`, (signalId, bucketName) => Reflect.construct(this.PersistStateFactory, [
3027
- bucketName,
3028
- `./dump/state/${signalId}/`,
3029
- ]));
3030
4174
  /**
3031
- * Initializes the storage for a given (signalId, bucketName) pair.
4175
+ * Constructor used to create per-context state instances.
4176
+ * Replaceable via usePersistStateAdapter() / useJson() / useDummy().
4177
+ */
4178
+ this.PersistStateInstanceCtor = PersistStateInstance;
4179
+ /**
4180
+ * Memoized factory creating one IPersistStateInstance per (signalId, bucketName) pair.
4181
+ */
4182
+ this.getStateStorage = functoolsKit.memoize(([signalId, bucketName]) => `${signalId}:${bucketName}`, (signalId, bucketName) => Reflect.construct(this.PersistStateInstanceCtor, [signalId, bucketName]));
4183
+ /**
4184
+ * Initializes the state storage for the given context.
4185
+ * Skips initialization when `initial` is false (used to gate first-time setup).
3032
4186
  *
3033
4187
  * @param signalId - Signal identifier
3034
4188
  * @param bucketName - Bucket name
3035
4189
  * @param initial - Whether this is the first initialization
4190
+ * @returns Promise that resolves when initialization is complete
3036
4191
  */
3037
4192
  this.waitForInit = async (signalId, bucketName, initial) => {
3038
- LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, {
3039
- signalId,
3040
- bucketName,
3041
- initial,
3042
- });
4193
+ LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_WAIT_FOR_INIT, { signalId, bucketName, initial });
3043
4194
  const key = `${signalId}:${bucketName}`;
3044
4195
  const isInitial = initial && !this.getStateStorage.has(key);
3045
- const stateStorage = this.getStateStorage(signalId, bucketName);
3046
- await stateStorage.waitForInit(isInitial);
4196
+ const instance = this.getStateStorage(signalId, bucketName);
4197
+ await instance.waitForInit(isInitial);
3047
4198
  };
3048
4199
  /**
3049
- * Reads a state entry from persistence storage.
4200
+ * Reads persisted state for the given context.
4201
+ * Lazily initializes the instance on first access.
3050
4202
  *
3051
4203
  * @param signalId - Signal identifier
3052
4204
  * @param bucketName - Bucket name
3053
- * @returns Promise resolving to entry data or null if not found
4205
+ * @returns Promise resolving to state data or null if none persisted
3054
4206
  */
3055
4207
  this.readStateData = async (signalId, bucketName) => {
3056
- LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, {
3057
- signalId,
3058
- bucketName,
3059
- });
4208
+ LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_READ_DATA, { signalId, bucketName });
3060
4209
  const key = `${signalId}:${bucketName}`;
3061
4210
  const isInitial = !this.getStateStorage.has(key);
3062
- const stateStorage = this.getStateStorage(signalId, bucketName);
3063
- await stateStorage.waitForInit(isInitial);
3064
- if (await stateStorage.hasValue(bucketName)) {
3065
- return await stateStorage.readValue(bucketName);
3066
- }
3067
- return null;
4211
+ const instance = this.getStateStorage(signalId, bucketName);
4212
+ await instance.waitForInit(isInitial);
4213
+ return instance.readStateData();
3068
4214
  };
3069
4215
  /**
3070
- * Writes a state entry to disk with atomic file writes.
4216
+ * Writes state for the given context.
4217
+ * Lazily initializes the instance on first access.
3071
4218
  *
3072
- * @param data - Entry data to persist
4219
+ * @param data - State data to persist
3073
4220
  * @param signalId - Signal identifier
3074
4221
  * @param bucketName - Bucket name
4222
+ * @returns Promise that resolves when write is complete
3075
4223
  */
3076
4224
  this.writeStateData = async (data, signalId, bucketName) => {
3077
- LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, {
3078
- signalId,
3079
- bucketName,
3080
- });
4225
+ LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_WRITE_DATA, { signalId, bucketName });
3081
4226
  const key = `${signalId}:${bucketName}`;
3082
4227
  const isInitial = !this.getStateStorage.has(key);
3083
- const stateStorage = this.getStateStorage(signalId, bucketName);
3084
- await stateStorage.waitForInit(isInitial);
3085
- await stateStorage.writeValue(bucketName, data);
4228
+ const instance = this.getStateStorage(signalId, bucketName);
4229
+ await instance.waitForInit(isInitial);
4230
+ return instance.writeStateData(data);
3086
4231
  };
3087
4232
  /**
3088
- * Switches to a dummy persist adapter that discards all writes.
3089
- * All future persistence writes will be no-ops.
4233
+ * Switches to PersistStateDummyInstance (all operations are no-ops).
3090
4234
  */
3091
4235
  this.useDummy = () => {
3092
4236
  LOGGER_SERVICE$7.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_DUMMY);
3093
- this.usePersistStateAdapter(PersistDummy);
4237
+ this.usePersistStateAdapter(PersistStateDummyInstance);
3094
4238
  };
3095
4239
  /**
3096
- * Switches to the default JSON persist adapter.
3097
- * All future persistence writes will use JSON storage.
4240
+ * Switches to the default file-based PersistStateInstance.
3098
4241
  */
3099
4242
  this.useJson = () => {
3100
4243
  LOGGER_SERVICE$7.log(PERSIST_STATE_UTILS_METHOD_NAME_USE_JSON);
3101
- this.usePersistStateAdapter(PersistBase);
4244
+ this.usePersistStateAdapter(PersistStateInstance);
3102
4245
  };
3103
4246
  /**
3104
- * Clears the memoized storage cache.
3105
- * Call this when process.cwd() changes between strategy iterations
3106
- * so new storage instances are created with the updated base path.
4247
+ * Clears the memoized instance cache.
4248
+ * Call when process.cwd() changes between strategy iterations.
3107
4249
  */
3108
4250
  this.clear = () => {
3109
4251
  LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_CLEAR);
3110
4252
  this.getStateStorage.clear();
3111
4253
  };
3112
4254
  /**
3113
- * Disposes of the state adapter and releases any resources.
3114
- * Call this when a signal is removed to clean up its associated storage.
4255
+ * Drops the memoized instance for the given context.
4256
+ * Call when a signal is removed to clean up its associated storage entry.
3115
4257
  *
3116
4258
  * @param signalId - Signal identifier
3117
4259
  * @param bucketName - Bucket name
@@ -3123,13 +4265,15 @@ class PersistStateUtils {
3123
4265
  };
3124
4266
  }
3125
4267
  /**
3126
- * Registers a custom persistence adapter.
4268
+ * Registers a custom IPersistStateInstance constructor.
4269
+ * Clears the memoization cache so subsequent calls use the new adapter.
3127
4270
  *
3128
- * @param Ctor - Custom PersistBase constructor
4271
+ * @param Ctor - Custom IPersistStateInstance constructor
3129
4272
  */
3130
4273
  usePersistStateAdapter(Ctor) {
3131
4274
  LOGGER_SERVICE$7.info(PERSIST_STATE_UTILS_METHOD_NAME_USE_PERSIST_STATE_ADAPTER);
3132
- this.PersistStateFactory = Ctor;
4275
+ this.PersistStateInstanceCtor = Ctor;
4276
+ this.getStateStorage.clear();
3133
4277
  }
3134
4278
  }
3135
4279
  /**
@@ -3137,6 +4281,101 @@ class PersistStateUtils {
3137
4281
  * Used by StatePersistInstance for crash-safe state persistence.
3138
4282
  */
3139
4283
  const PersistStateAdapter = new PersistStateUtils();
4284
+ /**
4285
+ * Default file-based implementation of IPersistSessionInstance.
4286
+ *
4287
+ * Features:
4288
+ * - Wraps PersistBase for atomic JSON writes
4289
+ * - Uses frameName as entity ID within a per-strategy/exchange PersistBase
4290
+ * - dispose is a no-op (memo cache is managed by PersistSessionUtils)
4291
+ *
4292
+ * @example
4293
+ * ```typescript
4294
+ * const instance = new PersistSessionInstance("my-strategy", "binance", "frame-1");
4295
+ * await instance.waitForInit(true);
4296
+ * await instance.writeSessionData({ id: "frame-1", data: { session: "state" } });
4297
+ * const session = await instance.readSessionData();
4298
+ * ```
4299
+ */
4300
+ class PersistSessionInstance {
4301
+ /**
4302
+ * Creates new session persistence instance.
4303
+ *
4304
+ * @param strategyName - Strategy identifier
4305
+ * @param exchangeName - Exchange identifier
4306
+ * @param frameName - Frame identifier (also used as entity ID)
4307
+ */
4308
+ constructor(strategyName, exchangeName, frameName) {
4309
+ this.strategyName = strategyName;
4310
+ this.exchangeName = exchangeName;
4311
+ this.frameName = frameName;
4312
+ this._storage = new PersistBase(frameName, `./dump/session/${strategyName}/${exchangeName}/`);
4313
+ }
4314
+ /**
4315
+ * Initializes the underlying PersistBase storage.
4316
+ *
4317
+ * @param initial - Whether this is the first initialization
4318
+ * @returns Promise that resolves when initialization is complete
4319
+ */
4320
+ async waitForInit(initial) {
4321
+ await this._storage.waitForInit(initial);
4322
+ }
4323
+ /**
4324
+ * Reads the persisted session data using `frameName` as the entity key.
4325
+ *
4326
+ * @returns Promise resolving to session data or null if not found
4327
+ */
4328
+ async readSessionData() {
4329
+ if (await this._storage.hasValue(this.frameName)) {
4330
+ return await this._storage.readValue(this.frameName);
4331
+ }
4332
+ return null;
4333
+ }
4334
+ /**
4335
+ * Writes the session data using `frameName` as the entity key.
4336
+ *
4337
+ * @param data - Session data to persist
4338
+ * @returns Promise that resolves when write is complete
4339
+ */
4340
+ async writeSessionData(data) {
4341
+ await this._storage.writeValue(this.frameName, data);
4342
+ }
4343
+ /**
4344
+ * No-op for the default file-based implementation.
4345
+ * Resource cleanup (memo cache invalidation) is handled by PersistSessionUtils.dispose().
4346
+ */
4347
+ dispose() { }
4348
+ }
4349
+ /**
4350
+ * No-op IPersistSessionInstance implementation used by PersistSessionUtils.useDummy().
4351
+ * All reads return null, all writes are discarded.
4352
+ */
4353
+ class PersistSessionDummyInstance {
4354
+ /**
4355
+ * No-op constructor.
4356
+ * Context arguments are accepted to satisfy TPersistSessionInstanceCtor.
4357
+ */
4358
+ constructor(_strategyName, _exchangeName, _frameName) { }
4359
+ /**
4360
+ * No-op initialization.
4361
+ * @returns Promise that resolves immediately
4362
+ */
4363
+ async waitForInit(_initial) { }
4364
+ /**
4365
+ * Always returns null (no persisted session).
4366
+ * @returns Promise resolving to null
4367
+ */
4368
+ async readSessionData() { return null; }
4369
+ /**
4370
+ * No-op write (discards session data).
4371
+ * @returns Promise that resolves immediately
4372
+ */
4373
+ async writeSessionData(_data) { }
4374
+ /**
4375
+ * No-op dispose.
4376
+ */
4377
+ dispose() { }
4378
+ }
3140
4379
  /**
3141
4380
  * Utility class for managing session persistence.
3142
4381
  *
@@ -3151,102 +4390,93 @@ const PersistStateAdapter = new PersistStateUtils();
3151
4390
  */
3152
4391
  class PersistSessionUtils {
3153
4392
  constructor() {
3154
- this.PersistSessionFactory = PersistBase;
3155
- this.getSessionStorage = functoolsKit.memoize(([strategyName, exchangeName, frameName]) => `${strategyName}:${exchangeName}:${frameName}`, (strategyName, exchangeName, frameName) => Reflect.construct(this.PersistSessionFactory, [
3156
- frameName,
3157
- `./dump/session/${strategyName}/${exchangeName}/`,
3158
- ]));
3159
4393
  /**
3160
- * Initializes the storage for a given (strategyName, exchangeName, frameName) triple.
4394
+ * Constructor used to create per-context session instances.
4395
+ * Replaceable via usePersistSessionAdapter() / useJson() / useDummy().
4396
+ */
4397
+ this.PersistSessionInstanceCtor = PersistSessionInstance;
4398
+ /**
4399
+ * Memoized factory creating one IPersistSessionInstance per
4400
+ * (strategyName, exchangeName, frameName) triple.
4401
+ */
4402
+ this.getSessionStorage = functoolsKit.memoize(([strategyName, exchangeName, frameName]) => `${strategyName}:${exchangeName}:${frameName}`, (strategyName, exchangeName, frameName) => Reflect.construct(this.PersistSessionInstanceCtor, [strategyName, exchangeName, frameName]));
4403
+ /**
4404
+ * Initializes the session storage for the given context.
4405
+ * Skips initialization when `initial` is false (used to gate first-time setup).
3161
4406
  *
3162
4407
  * @param strategyName - Strategy identifier
3163
4408
  * @param exchangeName - Exchange identifier
3164
4409
  * @param frameName - Frame identifier
3165
4410
  * @param initial - Whether this is the first initialization
4411
+ * @returns Promise that resolves when initialization is complete
3166
4412
  */
3167
4413
  this.waitForInit = async (strategyName, exchangeName, frameName, initial) => {
3168
- LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, {
3169
- strategyName,
3170
- exchangeName,
3171
- frameName,
3172
- initial,
3173
- });
4414
+ LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_WAIT_FOR_INIT, { strategyName, exchangeName, frameName, initial });
3174
4415
  const key = `${strategyName}:${exchangeName}:${frameName}`;
3175
4416
  const isInitial = initial && !this.getSessionStorage.has(key);
3176
- const sessionStorage = this.getSessionStorage(strategyName, exchangeName, frameName);
3177
- await sessionStorage.waitForInit(isInitial);
4417
+ const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
4418
+ await instance.waitForInit(isInitial);
3178
4419
  };
3179
4420
  /**
3180
- * Reads a session entry from persistence storage.
4421
+ * Reads persisted session data for the given context.
4422
+ * Lazily initializes the instance on first access.
3181
4423
  *
3182
4424
  * @param strategyName - Strategy identifier
3183
4425
  * @param exchangeName - Exchange identifier
3184
4426
  * @param frameName - Frame identifier
3185
- * @returns Promise resolving to entry data or null if not found
4427
+ * @returns Promise resolving to session data or null if none persisted
3186
4428
  */
3187
4429
  this.readSessionData = async (strategyName, exchangeName, frameName) => {
3188
- LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, {
3189
- strategyName,
3190
- exchangeName,
3191
- frameName,
3192
- });
4430
+ LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_READ_DATA, { strategyName, exchangeName, frameName });
3193
4431
  const key = `${strategyName}:${exchangeName}:${frameName}`;
3194
4432
  const isInitial = !this.getSessionStorage.has(key);
3195
- const sessionStorage = this.getSessionStorage(strategyName, exchangeName, frameName);
3196
- await sessionStorage.waitForInit(isInitial);
3197
- if (await sessionStorage.hasValue(frameName)) {
3198
- return await sessionStorage.readValue(frameName);
3199
- }
3200
- return null;
4433
+ const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
4434
+ await instance.waitForInit(isInitial);
4435
+ return instance.readSessionData();
3201
4436
  };
3202
4437
  /**
3203
- * Writes a session entry to disk with atomic file writes.
4438
+ * Writes session data for the given context.
4439
+ * Lazily initializes the instance on first access.
3204
4440
  *
3205
- * @param data - Entry data to persist
4441
+ * @param data - Session data to persist
3206
4442
  * @param strategyName - Strategy identifier
3207
4443
  * @param exchangeName - Exchange identifier
3208
4444
  * @param frameName - Frame identifier
4445
+ * @returns Promise that resolves when write is complete
3209
4446
  */
3210
4447
  this.writeSessionData = async (data, strategyName, exchangeName, frameName) => {
3211
- LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, {
3212
- strategyName,
3213
- exchangeName,
3214
- frameName,
3215
- });
4448
+ LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_WRITE_DATA, { strategyName, exchangeName, frameName });
3216
4449
  const key = `${strategyName}:${exchangeName}:${frameName}`;
3217
4450
  const isInitial = !this.getSessionStorage.has(key);
3218
- const sessionStorage = this.getSessionStorage(strategyName, exchangeName, frameName);
3219
- await sessionStorage.waitForInit(isInitial);
3220
- await sessionStorage.writeValue(frameName, data);
4451
+ const instance = this.getSessionStorage(strategyName, exchangeName, frameName);
4452
+ await instance.waitForInit(isInitial);
4453
+ return instance.writeSessionData(data);
3221
4454
  };
3222
4455
  /**
3223
- * Switches to a dummy persist adapter that discards all writes.
3224
- * All future persistence writes will be no-ops.
4456
+ * Switches to PersistSessionDummyInstance (all operations are no-ops).
3225
4457
  */
3226
4458
  this.useDummy = () => {
3227
4459
  LOGGER_SERVICE$7.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_DUMMY);
3228
- this.usePersistSessionAdapter(PersistDummy);
4460
+ this.usePersistSessionAdapter(PersistSessionDummyInstance);
3229
4461
  };
3230
4462
  /**
3231
- * Switches to the default JSON persist adapter.
3232
- * All future persistence writes will use JSON storage.
4463
+ * Switches to the default file-based PersistSessionInstance.
3233
4464
  */
3234
4465
  this.useJson = () => {
3235
4466
  LOGGER_SERVICE$7.log(PERSIST_SESSION_UTILS_METHOD_NAME_USE_JSON);
3236
- this.usePersistSessionAdapter(PersistBase);
4467
+ this.usePersistSessionAdapter(PersistSessionInstance);
3237
4468
  };
3238
4469
  /**
3239
- * Clears the memoized storage cache.
3240
- * Call this when process.cwd() changes between strategy iterations
3241
- * so new storage instances are created with the updated base path.
4470
+ * Clears the memoized instance cache.
4471
+ * Call when process.cwd() changes between strategy iterations.
3242
4472
  */
3243
4473
  this.clear = () => {
3244
4474
  LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_CLEAR);
3245
4475
  this.getSessionStorage.clear();
3246
4476
  };
3247
4477
  /**
3248
- * Disposes of the session adapter and releases any resources.
3249
- * Call this when a session is removed to clean up its associated storage.
4478
+ * Drops the memoized instance for the given context.
4479
+ * Call when a session is removed to clean up its associated storage entry.
3250
4480
  *
3251
4481
  * @param strategyName - Strategy identifier
3252
4482
  * @param exchangeName - Exchange identifier
@@ -3259,13 +4489,15 @@ class PersistSessionUtils {
3259
4489
  };
3260
4490
  }
3261
4491
  /**
3262
- * Registers a custom persistence adapter.
4492
+ * Registers a custom IPersistSessionInstance constructor.
4493
+ * Clears the memoization cache so subsequent calls use the new adapter.
3263
4494
  *
3264
- * @param Ctor - Custom PersistBase constructor
4495
+ * @param Ctor - Custom IPersistSessionInstance constructor
3265
4496
  */
3266
4497
  usePersistSessionAdapter(Ctor) {
3267
4498
  LOGGER_SERVICE$7.info(PERSIST_SESSION_UTILS_METHOD_NAME_USE_PERSIST_SESSION_ADAPTER);
3268
- this.PersistSessionFactory = Ctor;
4499
+ this.PersistSessionInstanceCtor = Ctor;
4500
+ this.getSessionStorage.clear();
3269
4501
  }
3270
4502
  }
3271
4503
  /**
@@ -6697,7 +7929,7 @@ const CALL_PARTIAL_CLEAR_FN = functoolsKit.trycatch(beginTime(async (self, symbo
6697
7929
  });
6698
7930
  const CALL_RISK_CHECK_SIGNAL_FN = functoolsKit.trycatch(beginTime(async (self, symbol, pendingSignal, currentPrice, timestamp, backtest) => {
6699
7931
  return await ExecutionContextService.runInContext(async () => {
6700
- return await self.params.risk.checkSignal({
7932
+ return await self.params.risk.checkSignalAndReserve({
6701
7933
  currentSignal: TO_PUBLIC_SIGNAL("scheduled", pendingSignal, currentPrice),
6702
7934
  symbol: symbol,
6703
7935
  strategyName: self.params.method.context.strategyName,
@@ -10578,6 +11810,7 @@ class ClientStrategy {
10578
11810
  }
10579
11811
 
10580
11812
  const RISK_METHOD_NAME_CHECK_SIGNAL = "MergeRisk.checkSignal";
11813
+ const RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE = "MergeRisk.checkSignalAndReserve";
10581
11814
  const RISK_METHOD_NAME_ADD_SIGNAL = "MergeRisk.addSignal";
10582
11815
  const RISK_METHOD_NAME_REMOVE_SIGNAL = "MergeRisk.removeSignal";
10583
11816
  /** Logger service injected as DI singleton */
@@ -10636,7 +11869,7 @@ class MergeRisk {
10636
11869
  * @param params - Risk check parameters (symbol, strategy, position, exchange)
10637
11870
  * @returns Promise resolving to true if all risks approve, false if any risk rejects
10638
11871
  */
10639
- async checkSignal(params) {
11872
+ async checkSignal(params, options = {}) {
10640
11873
  LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL, {
10641
11874
  params,
10642
11875
  });
@@ -10644,6 +11877,36 @@ class MergeRisk {
10644
11877
  if (await functoolsKit.not(risk.checkSignal({
10645
11878
  ...params,
10646
11879
  riskName,
11880
+ }, options))) {
11881
+ return false;
11882
+ }
11883
+ }
11884
+ return true;
11885
+ }
11886
+ /**
11887
+ * Concurrency-safe variant of {@link checkSignal} — validates the signal AND
11888
+ * reserves a placeholder in every child risk's active position map atomically.
11889
+ *
11890
+ * Iterates child risks sequentially. On the first rejection it returns false
11891
+ * immediately; child risks checked earlier in the loop are left with their
11892
+ * reservations in place — `removeSignal` on the parent will roll them back
11893
+ * (it propagates to all children unconditionally).
11894
+ *
11895
+ * Use from strategy execution paths where the caller will follow up with
11896
+ * `addSignal` on success. See {@link IRisk.checkSignalAndReserve} for the
11897
+ * full rationale on why reserving inside the lock is necessary.
11898
+ *
11899
+ * @param params - Risk check parameters (symbol, strategy, position, exchange)
11900
+ * @returns Promise resolving to true if all risks approve (and reserved), false if any risk rejects
11901
+ */
11902
+ async checkSignalAndReserve(params) {
11903
+ LOGGER_SERVICE$5.info(RISK_METHOD_NAME_CHECK_SIGNAL_AND_RESERVE, {
11904
+ params,
11905
+ });
11906
+ for (const [riskName, risk] of Object.entries(this._riskMap)) {
11907
+ if (await functoolsKit.not(risk.checkSignalAndReserve({
11908
+ ...params,
11909
+ riskName,
10647
11910
  }))) {
10648
11911
  return false;
10649
11912
  }
@@ -10753,6 +12016,7 @@ const CALL_SIGNAL_EMIT_FN = functoolsKit.trycatch(beginTime(async (self, tick, c
10753
12016
  */
10754
12017
  const NOOP_RISK = {
10755
12018
  checkSignal: () => Promise.resolve(true),
12019
+ checkSignalAndReserve: () => Promise.resolve(true),
10756
12020
  addSignal: () => Promise.resolve(),
10757
12021
  removeSignal: () => Promise.resolve(),
10758
12022
  };
@@ -12928,6 +14192,8 @@ const alignToInterval = (date, interval) => {
12928
14192
  return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
12929
14193
  };
12930
14194
 
14195
+ /** Used to prevent race confition between concurent strategies */
14196
+ const RISK_LOCK = new Lock();
12931
14197
  /** Symbol indicating that positions need to be fetched from persistence */
12932
14198
  const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
12933
14199
  /** Get timestamp from execution context or fallback to aligned current time */
@@ -13113,60 +14379,115 @@ class ClientRisk {
13113
14379
  * @param params - Risk check arguments (passthrough from ClientStrategy)
13114
14380
  * @returns Promise resolving to true if allowed, false if rejected
13115
14381
  */
13116
- this.checkSignal = async (params) => {
14382
+ this.checkSignal = async (params, options = {}) => {
13117
14383
  this.params.logger.debug("ClientRisk checkSignal", {
13118
14384
  symbol: params.symbol,
13119
14385
  strategyName: params.strategyName,
13120
14386
  backtest: this.params.backtest,
13121
14387
  });
13122
- if (this._activePositions === POSITION_NEED_FETCH) {
13123
- await this.waitForInit();
13124
- }
13125
- const riskMap = this._activePositions;
13126
- const timestamp = GET_CONTEXT_TIMESTAMP_FN(this);
13127
- const payload = {
13128
- ...params,
13129
- currentSignal: TO_RISK_SIGNAL(params.currentSignal, params.currentPrice, timestamp),
13130
- activePositionCount: riskMap.size,
13131
- activePositions: Array.from(riskMap.values()),
13132
- };
13133
- let rejectionResult = null;
13134
- if (this.params.validations) {
13135
- for (const validation of this.params.validations) {
13136
- const rejection = await DO_VALIDATION_FN(this, typeof validation === "function" ? validation : validation.validate, payload);
13137
- if (!rejection) {
13138
- continue;
13139
- }
13140
- if (typeof rejection === "string") {
13141
- rejectionResult = {
13142
- id: null,
13143
- note: rejection
13144
- ? rejection
13145
- : "note" in validation
13146
- ? validation.note
13147
- : "Validation failed",
13148
- };
13149
- break;
13150
- }
13151
- if (functoolsKit.isObject(rejection)) {
13152
- rejectionResult = {
13153
- id: get(rejection, "id") || null,
13154
- note: get(rejection, "note") || "Validation rejected the signal",
13155
- };
13156
- break;
14388
+ await RISK_LOCK.acquireLock();
14389
+ try {
14390
+ if (this._activePositions === POSITION_NEED_FETCH) {
14391
+ await this.waitForInit();
14392
+ }
14393
+ const riskMap = this._activePositions;
14394
+ const timestamp = GET_CONTEXT_TIMESTAMP_FN(this);
14395
+ const payload = {
14396
+ ...params,
14397
+ currentSignal: TO_RISK_SIGNAL(params.currentSignal, params.currentPrice, timestamp),
14398
+ activePositionCount: riskMap.size,
14399
+ activePositions: Array.from(riskMap.values()),
14400
+ };
14401
+ let rejectionResult = null;
14402
+ if (this.params.validations) {
14403
+ for (const validation of this.params.validations) {
14404
+ const rejection = await DO_VALIDATION_FN(this, typeof validation === "function" ? validation : validation.validate, payload);
14405
+ if (!rejection) {
14406
+ continue;
14407
+ }
14408
+ if (typeof rejection === "string") {
14409
+ rejectionResult = {
14410
+ id: null,
14411
+ note: rejection
14412
+ ? rejection
14413
+ : "note" in validation
14414
+ ? validation.note
14415
+ : "Validation failed",
14416
+ };
14417
+ break;
14418
+ }
14419
+ if (functoolsKit.isObject(rejection)) {
14420
+ rejectionResult = {
14421
+ id: get(rejection, "id") || null,
14422
+ note: get(rejection, "note") || "Validation rejected the signal",
14423
+ };
14424
+ break;
14425
+ }
13157
14426
  }
13158
14427
  }
14428
+ if (rejectionResult) {
14429
+ // Call params.onRejected for riskSubject emission
14430
+ await this.params.onRejected(params.symbol, params, riskMap.size, rejectionResult, params.timestamp, this.params.backtest);
14431
+ // Call schema callbacks.onRejected if defined
14432
+ await CALL_REJECTED_CALLBACKS_FN(this, params.symbol, params);
14433
+ return false;
14434
+ }
14435
+ // Optional placeholder reservation: when caller plans to addSignal next,
14436
+ // pre-write into riskMap inside the same critical section so concurrent
14437
+ // checkSignal calls observe the incremented size before addSignal lands.
14438
+ // The placeholder shares the same key as the future addSignal — it will
14439
+ // be overwritten with real position data, not duplicated.
14440
+ if (options?.reserve) {
14441
+ const reserveKey = CREATE_NAME_FN(params.strategyName, params.exchangeName, params.symbol);
14442
+ const signal = params.currentSignal;
14443
+ riskMap.set(reserveKey, {
14444
+ strategyName: params.strategyName,
14445
+ exchangeName: params.exchangeName,
14446
+ frameName: params.frameName,
14447
+ symbol: params.symbol,
14448
+ position: signal.position,
14449
+ priceOpen: signal.priceOpen ?? params.currentPrice,
14450
+ priceStopLoss: signal.priceStopLoss,
14451
+ priceTakeProfit: signal.priceTakeProfit,
14452
+ minuteEstimatedTime: signal.minuteEstimatedTime,
14453
+ openTimestamp: timestamp,
14454
+ });
14455
+ }
14456
+ // All checks passed
14457
+ await CALL_ALLOWED_CALLBACKS_FN(this, params.symbol, params);
14458
+ return true;
13159
14459
  }
13160
- if (rejectionResult) {
13161
- // Call params.onRejected for riskSubject emission
13162
- await this.params.onRejected(params.symbol, params, riskMap.size, rejectionResult, params.timestamp, this.params.backtest);
13163
- // Call schema callbacks.onRejected if defined
13164
- await CALL_REJECTED_CALLBACKS_FN(this, params.symbol, params);
13165
- return false;
14460
+ finally {
14461
+ await RISK_LOCK.releaseLock();
13166
14462
  }
13167
- // All checks passed
13168
- await CALL_ALLOWED_CALLBACKS_FN(this, params.symbol, params);
13169
- return true;
14463
+ };
14464
+ /**
14465
+ * Concurrency-safe variant of {@link checkSignal}: validates the signal AND
14466
+ * reserves a placeholder slot in the active position map atomically.
14467
+ *
14468
+ * **Why this exists.** `checkSignal` followed later by `addSignal` is not
14469
+ * atomic — between the two calls the caller does signal setup work that
14470
+ * yields to the event loop (sync-open callback, persist writes, etc.). When
14471
+ * several strategies sharing the same risk profile run in parallel, all of
14472
+ * them can pass `checkSignal` while the active position map is still empty,
14473
+ * then each call `addSignal` and blow past the limit. Reserving inside the
14474
+ * lock guarantees the next concurrent caller observes the incremented size
14475
+ * before its own validation runs.
14476
+ *
14477
+ * The reservation uses the same map key as the eventual `addSignal` call
14478
+ * (`strategyName + exchangeName + symbol`), so `addSignal` overwrites the
14479
+ * placeholder rather than appending a duplicate.
14480
+ *
14481
+ * Callers MUST ensure that every successful return is followed by either
14482
+ * `addSignal` (overwrites the placeholder with real data) or `removeSignal`
14483
+ * (clears the placeholder if opening is aborted). Otherwise the riskMap
14484
+ * accumulates stale reservations.
14485
+ *
14486
+ * @param params - Risk check arguments (passthrough from ClientStrategy)
14487
+ * @returns Promise resolving to true if allowed (and reserved), false if rejected (no reservation)
14488
+ */
14489
+ this.checkSignalAndReserve = async (params) => {
14490
+ return await this.checkSignal(params, { reserve: true });
13170
14491
  };
13171
14492
  }
13172
14493
  /**
@@ -13193,24 +14514,30 @@ class ClientRisk {
13193
14514
  positionData,
13194
14515
  backtest: this.params.backtest,
13195
14516
  });
13196
- if (this._activePositions === POSITION_NEED_FETCH) {
13197
- await this.waitForInit();
14517
+ await RISK_LOCK.acquireLock();
14518
+ try {
14519
+ if (this._activePositions === POSITION_NEED_FETCH) {
14520
+ await this.waitForInit();
14521
+ }
14522
+ const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
14523
+ const riskMap = this._activePositions;
14524
+ riskMap.set(key, {
14525
+ strategyName: context.strategyName,
14526
+ exchangeName: context.exchangeName,
14527
+ frameName: context.frameName,
14528
+ symbol,
14529
+ position: positionData.position,
14530
+ priceOpen: positionData.priceOpen,
14531
+ priceStopLoss: positionData.priceStopLoss,
14532
+ priceTakeProfit: positionData.priceTakeProfit,
14533
+ minuteEstimatedTime: positionData.minuteEstimatedTime,
14534
+ openTimestamp: positionData.openTimestamp,
14535
+ });
14536
+ await this._updatePositions();
14537
+ }
14538
+ finally {
14539
+ await RISK_LOCK.releaseLock();
13198
14540
  }
13199
- const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
13200
- const riskMap = this._activePositions;
13201
- riskMap.set(key, {
13202
- strategyName: context.strategyName,
13203
- exchangeName: context.exchangeName,
13204
- frameName: context.frameName,
13205
- symbol,
13206
- position: positionData.position,
13207
- priceOpen: positionData.priceOpen,
13208
- priceStopLoss: positionData.priceStopLoss,
13209
- priceTakeProfit: positionData.priceTakeProfit,
13210
- minuteEstimatedTime: positionData.minuteEstimatedTime,
13211
- openTimestamp: positionData.openTimestamp,
13212
- });
13213
- await this._updatePositions();
13214
14541
  }
13215
14542
  /**
13216
14543
  * Removes a closed signal.
@@ -13222,13 +14549,19 @@ class ClientRisk {
13222
14549
  context,
13223
14550
  backtest: this.params.backtest,
13224
14551
  });
13225
- if (this._activePositions === POSITION_NEED_FETCH) {
13226
- await this.waitForInit();
14552
+ await RISK_LOCK.acquireLock();
14553
+ try {
14554
+ if (this._activePositions === POSITION_NEED_FETCH) {
14555
+ await this.waitForInit();
14556
+ }
14557
+ const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
14558
+ const riskMap = this._activePositions;
14559
+ riskMap.delete(key);
14560
+ await this._updatePositions();
14561
+ }
14562
+ finally {
14563
+ await RISK_LOCK.releaseLock();
13227
14564
  }
13228
- const key = CREATE_NAME_FN(context.strategyName, context.exchangeName, symbol);
13229
- const riskMap = this._activePositions;
13230
- riskMap.delete(key);
13231
- await this._updatePositions();
13232
14565
  }
13233
14566
  }
13234
14567
 
@@ -13363,12 +14696,33 @@ class RiskConnectionService {
13363
14696
  * @param payload - Execution payload with risk name, exchangeName, frameName and backtest mode
13364
14697
  * @returns Promise resolving to risk check result
13365
14698
  */
13366
- this.checkSignal = async (params, payload) => {
14699
+ this.checkSignal = async (params, payload, options = {}) => {
13367
14700
  this.loggerService.log("riskConnectionService checkSignal", {
13368
14701
  symbol: params.symbol,
13369
14702
  payload,
13370
14703
  });
13371
- return await this.getRisk(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest).checkSignal(params);
14704
+ return await this.getRisk(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest).checkSignal(params, options);
14705
+ };
14706
+ /**
14707
+ * Concurrency-safe variant of {@link checkSignal} — validates the signal AND
14708
+ * reserves a placeholder in the active position map atomically.
14709
+ *
14710
+ * Routes to the same ClientRisk instance as {@link checkSignal} but delegates
14711
+ * to its `checkSignalAndReserve` method. Use from execution paths where the
14712
+ * caller will follow up with `addSignal` on success — guarantees concurrent
14713
+ * callers cannot all pass validation against a stale empty map. See
14714
+ * {@link IRisk.checkSignalAndReserve} for the full rationale.
14715
+ *
14716
+ * @param params - Risk check arguments (portfolio state, position details)
14717
+ * @param payload - Execution payload with risk name, exchangeName, frameName and backtest mode
14718
+ * @returns Promise resolving to true if allowed (and reserved), false if rejected (no reservation)
14719
+ */
14720
+ this.checkSignalAndReserve = async (params, payload) => {
14721
+ this.loggerService.log("riskConnectionService checkSignalAndReserve", {
14722
+ symbol: params.symbol,
14723
+ payload,
14724
+ });
14725
+ return await this.getRisk(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest).checkSignalAndReserve(params);
13372
14726
  };
13373
14727
  /**
13374
14728
  * Registers an opened signal with the risk management system.
@@ -16572,13 +17926,34 @@ class RiskGlobalService {
16572
17926
  * @param payload - Execution payload with risk name, exchangeName, frameName and backtest mode
16573
17927
  * @returns Promise resolving to risk check result
16574
17928
  */
16575
- this.checkSignal = async (params, payload) => {
17929
+ this.checkSignal = async (params, payload, options = {}) => {
16576
17930
  this.loggerService.log("riskGlobalService checkSignal", {
16577
17931
  symbol: params.symbol,
16578
17932
  payload,
16579
17933
  });
16580
17934
  await this.validate(payload);
16581
- return await this.riskConnectionService.checkSignal(params, payload);
17935
+ return await this.riskConnectionService.checkSignal(params, payload, options);
17936
+ };
17937
+ /**
17938
+ * Concurrency-safe variant of {@link checkSignal} — validates the signal AND
17939
+ * reserves a placeholder in the active position map atomically.
17940
+ *
17941
+ * Use from strategy execution paths where the caller will follow up with
17942
+ * `addSignal` on success — guarantees concurrent callers cannot all pass
17943
+ * validation against a stale empty map. See {@link IRisk.checkSignalAndReserve}
17944
+ * for the full rationale.
17945
+ *
17946
+ * @param params - Risk check arguments (portfolio state, position details)
17947
+ * @param payload - Execution payload with risk name, exchangeName, frameName and backtest mode
17948
+ * @returns Promise resolving to true if allowed (and reserved), false if rejected (no reservation)
17949
+ */
17950
+ this.checkSignalAndReserve = async (params, payload) => {
17951
+ this.loggerService.log("riskGlobalService checkSignalAndReserve", {
17952
+ symbol: params.symbol,
17953
+ payload,
17954
+ });
17955
+ await this.validate(payload);
17956
+ return await this.riskConnectionService.checkSignalAndReserve(params, payload);
16582
17957
  };
16583
17958
  /**
16584
17959
  * Registers an opened signal with the risk management system.
@@ -61977,20 +63352,35 @@ exports.Partial = Partial;
61977
63352
  exports.Performance = Performance;
61978
63353
  exports.PersistBase = PersistBase;
61979
63354
  exports.PersistBreakevenAdapter = PersistBreakevenAdapter;
63355
+ exports.PersistBreakevenInstance = PersistBreakevenInstance;
61980
63356
  exports.PersistCandleAdapter = PersistCandleAdapter;
63357
+ exports.PersistCandleInstance = PersistCandleInstance;
61981
63358
  exports.PersistIntervalAdapter = PersistIntervalAdapter;
63359
+ exports.PersistIntervalInstance = PersistIntervalInstance;
61982
63360
  exports.PersistLogAdapter = PersistLogAdapter;
63361
+ exports.PersistLogInstance = PersistLogInstance;
61983
63362
  exports.PersistMeasureAdapter = PersistMeasureAdapter;
63363
+ exports.PersistMeasureInstance = PersistMeasureInstance;
61984
63364
  exports.PersistMemoryAdapter = PersistMemoryAdapter;
63365
+ exports.PersistMemoryInstance = PersistMemoryInstance;
61985
63366
  exports.PersistNotificationAdapter = PersistNotificationAdapter;
63367
+ exports.PersistNotificationInstance = PersistNotificationInstance;
61986
63368
  exports.PersistPartialAdapter = PersistPartialAdapter;
63369
+ exports.PersistPartialInstance = PersistPartialInstance;
61987
63370
  exports.PersistRecentAdapter = PersistRecentAdapter;
63371
+ exports.PersistRecentInstance = PersistRecentInstance;
61988
63372
  exports.PersistRiskAdapter = PersistRiskAdapter;
63373
+ exports.PersistRiskInstance = PersistRiskInstance;
61989
63374
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
63375
+ exports.PersistScheduleInstance = PersistScheduleInstance;
61990
63376
  exports.PersistSessionAdapter = PersistSessionAdapter;
63377
+ exports.PersistSessionInstance = PersistSessionInstance;
61991
63378
  exports.PersistSignalAdapter = PersistSignalAdapter;
63379
+ exports.PersistSignalInstance = PersistSignalInstance;
61992
63380
  exports.PersistStateAdapter = PersistStateAdapter;
63381
+ exports.PersistStateInstance = PersistStateInstance;
61993
63382
  exports.PersistStorageAdapter = PersistStorageAdapter;
63383
+ exports.PersistStorageInstance = PersistStorageInstance;
61994
63384
  exports.Position = Position;
61995
63385
  exports.PositionSize = PositionSize;
61996
63386
  exports.Recent = Recent;