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