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