backtest-kit 1.3.2 β†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +841 -6
  2. package/build/index.cjs +3349 -183
  3. package/build/index.mjs +3339 -184
  4. package/package.json +3 -2
  5. package/types.d.ts +2260 -122
package/README.md CHANGED
@@ -16,7 +16,7 @@ Build sophisticated trading systems with confidence. Backtest Kit empowers you t
16
16
 
17
17
  - πŸš€ **Production-Ready Architecture**: Seamlessly switch between backtest and live modes with robust error recovery and graceful shutdown mechanisms. Your strategy code remains identical across environments.
18
18
 
19
- - πŸ’Ύ **Crash-Safe Persistence**: Atomic file writes with automatic state recovery ensure no duplicate signals or lost dataβ€”even after crashes. Resume execution exactly where you left off. πŸ”„
19
+ - πŸ’Ύ **Crash-Safe Persistence**: Atomic file writes with automatic state recovery ensure no duplicate signals or lost dataβ€”even after crashes. Resume execution exactly where you left off.
20
20
 
21
21
  - βœ… **Signal Validation**: Comprehensive validation prevents invalid trades before execution. Catches price logic errors (TP/SL), throttles signal spam, and ensures data integrity. πŸ›‘οΈ
22
22
 
@@ -44,11 +44,13 @@ Build sophisticated trading systems with confidence. Backtest Kit empowers you t
44
44
 
45
45
  - πŸ’Ύ **Zero Data Download**: Unlike Freqtrade, no need to download gigabytes of historical dataβ€”plug any data source (CCXT, database, API). πŸš€
46
46
 
47
- - πŸ”Œ **Pluggable Persistence**: Replace default file-based persistence with custom adapters (Redis, MongoDB, PostgreSQL) for distributed systems and high-performance scenarios. πŸ’Ύ
47
+ - πŸ”Œ **Pluggable Persistence**: Replace default file-based persistence with custom adapters (Redis, MongoDB, PostgreSQL) for distributed systems and high-performance scenarios.
48
48
 
49
49
  - πŸ”’ **Safe Math & Robustness**: All metrics protected against NaN/Infinity with unsafe numeric checks. Returns N/A for invalid calculations. ✨
50
50
 
51
- - πŸ§ͺ **Comprehensive Test Coverage**: 123 unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, and event system.
51
+ - πŸ€– **AI Strategy Optimizer**: LLM-powered strategy generation from historical data. Train multiple strategy variants, compare performance, and auto-generate executable code. Supports Ollama integration with multi-timeframe analysis. 🧠
52
+
53
+ - πŸ§ͺ **Comprehensive Test Coverage**: Unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, crash recovery, optimizer, and event system.
52
54
 
53
55
  ---
54
56
 
@@ -249,12 +251,15 @@ Backtest.background("BTCUSDT", {
249
251
  - 🌐 **`addFrame`**: Configure timeframes for backtesting. πŸ“…
250
252
  - πŸ”„ **`Backtest` / `Live`**: Run strategies in backtest or live mode (generator or background). ⚑
251
253
  - πŸ“… **`Schedule`**: Track scheduled signals and cancellation rate for limit orders. πŸ“Š
254
+ - πŸ“Š **`Partial`**: Access partial profit/loss statistics and reports for risk management. Track signals reaching milestone levels (10%, 20%, 30%, etc.). πŸ’Ή
255
+ - 🎯 **`Constant`**: Kelly Criterion-based constants for optimal take profit (TP_LEVEL1-3) and stop loss (SL_LEVEL1-2) levels. πŸ“
252
256
  - πŸƒ **`Walker`**: Compare multiple strategies in parallel with ranking. πŸ†
253
257
  - πŸ”₯ **`Heat`**: Portfolio-wide performance analysis across multiple symbols. πŸ“Š
254
258
  - πŸ’° **`PositionSize`**: Calculate position sizes with Fixed %, Kelly Criterion, or ATR-based methods. πŸ’΅
255
259
  - πŸ›‘οΈ **`addRisk`**: Portfolio-level risk management with custom validation logic. πŸ”
256
- - πŸ’Ύ **`PersistBase`**: Base class for custom persistence adapters (Redis, MongoDB, PostgreSQL). πŸ—„οΈ
257
- - πŸ”Œ **`PersistSignalAdapter` / `PersistRiskAdapter`**: Register custom adapters for signal and risk persistence. πŸ”„
260
+ - πŸ’Ύ **`PersistBase`**: Base class for custom persistence adapters (Redis, MongoDB, PostgreSQL).
261
+ - πŸ”Œ **`PersistSignalAdapter` / `PersistScheduleAdapter` / `PersistRiskAdapter` / `PersistPartialAdapter`**: Register custom adapters for signal, scheduled signal, risk, and partial state persistence.
262
+ - πŸ€– **`Optimizer`**: AI-powered strategy generation with LLM integration. Auto-generate strategies from historical data and export executable code. 🧠
258
263
 
259
264
  Check out the sections below for detailed examples! πŸ“š
260
265
 
@@ -1071,6 +1076,777 @@ test("Custom Redis adapter works correctly", async ({ pass, fail }) => {
1071
1076
  });
1072
1077
  ```
1073
1078
 
1079
+ ### 10. Partial Profit/Loss Tracking
1080
+
1081
+ Partial Profit/Loss system tracks signal performance at fixed percentage levels (10%, 20%, 30%, etc.) for risk management and position scaling strategies.
1082
+
1083
+ #### Understanding Partial Levels
1084
+
1085
+ The system automatically monitors profit/loss milestones and emits events when signals reach specific levels:
1086
+
1087
+ ```typescript
1088
+ import {
1089
+ listenPartialProfit,
1090
+ listenPartialLoss,
1091
+ listenPartialProfitOnce,
1092
+ listenPartialLossOnce,
1093
+ Constant
1094
+ } from "backtest-kit";
1095
+
1096
+ // Listen to all profit levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
1097
+ listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1098
+ console.log(`${symbol} profit: ${level}% at ${price}`);
1099
+
1100
+ // Close portions at Kelly-optimized levels
1101
+ if (level === Constant.TP_LEVEL3) {
1102
+ console.log("Close 33% at 25% profit");
1103
+ }
1104
+ if (level === Constant.TP_LEVEL2) {
1105
+ console.log("Close 33% at 50% profit");
1106
+ }
1107
+ if (level === Constant.TP_LEVEL1) {
1108
+ console.log("Close 34% at 100% profit");
1109
+ }
1110
+ });
1111
+
1112
+ // Listen to all loss levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
1113
+ listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
1114
+ console.log(`${symbol} loss: -${level}% at ${price}`);
1115
+
1116
+ // Close portions at stop levels
1117
+ if (level === Constant.SL_LEVEL2) {
1118
+ console.log("Close 50% at -50% loss");
1119
+ }
1120
+ if (level === Constant.SL_LEVEL1) {
1121
+ console.log("Close 50% at -100% loss");
1122
+ }
1123
+ });
1124
+
1125
+ // Listen once to first profit level reached
1126
+ listenPartialProfitOnce(
1127
+ () => true, // Accept any profit event
1128
+ ({ symbol, signal, price, level, backtest }) => {
1129
+ console.log(`First profit milestone: ${level}%`);
1130
+ }
1131
+ );
1132
+
1133
+ // Listen once to first loss level reached
1134
+ listenPartialLossOnce(
1135
+ () => true, // Accept any loss event
1136
+ ({ symbol, signal, price, level, backtest }) => {
1137
+ console.log(`First loss milestone: -${level}%`);
1138
+ }
1139
+ );
1140
+ ```
1141
+
1142
+ #### Constant Utility - Kelly-Optimized Levels
1143
+
1144
+ The `Constant` class provides predefined Kelly Criterion-based levels for optimal position sizing:
1145
+
1146
+ ```typescript
1147
+ import { Constant } from "backtest-kit";
1148
+
1149
+ // Take Profit Levels
1150
+ console.log(Constant.TP_LEVEL1); // 100% (aggressive target)
1151
+ console.log(Constant.TP_LEVEL2); // 50% (moderate target)
1152
+ console.log(Constant.TP_LEVEL3); // 25% (conservative target)
1153
+
1154
+ // Stop Loss Levels
1155
+ console.log(Constant.SL_LEVEL1); // 100% (maximum risk)
1156
+ console.log(Constant.SL_LEVEL2); // 50% (standard stop)
1157
+ ```
1158
+
1159
+ **Use Case - Scale Out Strategy:**
1160
+
1161
+ ```typescript
1162
+ // Strategy: Close position in 3 tranches at optimal levels
1163
+ listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1164
+ if (level === Constant.TP_LEVEL3) {
1165
+ // Close 33% at 25% profit (secure early gains)
1166
+ executePartialClose(symbol, signal.id, 0.33);
1167
+ }
1168
+ if (level === Constant.TP_LEVEL2) {
1169
+ // Close 33% at 50% profit (lock in medium gains)
1170
+ executePartialClose(symbol, signal.id, 0.33);
1171
+ }
1172
+ if (level === Constant.TP_LEVEL1) {
1173
+ // Close 34% at 100% profit (maximize winners)
1174
+ executePartialClose(symbol, signal.id, 0.34);
1175
+ }
1176
+ });
1177
+ ```
1178
+
1179
+ #### Partial Reports and Statistics
1180
+
1181
+ The `Partial` utility provides access to accumulated partial profit/loss data:
1182
+
1183
+ ```typescript
1184
+ import { Partial } from "backtest-kit";
1185
+
1186
+ // Get statistical data
1187
+ const stats = await Partial.getData("BTCUSDT");
1188
+ console.log(stats);
1189
+ // Returns:
1190
+ // {
1191
+ // totalEvents: 15, // Total profit/loss events
1192
+ // totalProfit: 10, // Number of profit events
1193
+ // totalLoss: 5, // Number of loss events
1194
+ // eventList: [
1195
+ // {
1196
+ // timestamp: 1704370800000,
1197
+ // action: "PROFIT", // PROFIT or LOSS
1198
+ // symbol: "BTCUSDT",
1199
+ // signalId: "abc123",
1200
+ // position: "LONG", // or SHORT
1201
+ // level: 10, // Percentage level reached
1202
+ // price: 51500.00, // Current price at level
1203
+ // mode: "Backtest" // or Live
1204
+ // },
1205
+ // // ... more events
1206
+ // ]
1207
+ // }
1208
+
1209
+ // Generate markdown report
1210
+ const markdown = await Partial.getReport("BTCUSDT");
1211
+ console.log(markdown);
1212
+
1213
+ // Save report to disk (default: ./dump/partial/BTCUSDT.md)
1214
+ await Partial.dump("BTCUSDT");
1215
+
1216
+ // Custom output path
1217
+ await Partial.dump("BTCUSDT", "./reports/partial");
1218
+ ```
1219
+
1220
+ **Partial Report Example:**
1221
+
1222
+ ```markdown
1223
+ # Partial Profit/Loss Report: BTCUSDT
1224
+
1225
+ | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
1226
+ | --- | --- | --- | --- | --- | --- | --- | --- |
1227
+ | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
1228
+ | PROFIT | BTCUSDT | abc123 | LONG | +20% | 53000.00000000 USD | 2024-01-15T11:15:00.000Z | Backtest |
1229
+ | LOSS | BTCUSDT | def456 | SHORT | -10% | 51500.00000000 USD | 2024-01-15T14:00:00.000Z | Backtest |
1230
+
1231
+ **Total events:** 15
1232
+ **Profit events:** 10
1233
+ **Loss events:** 5
1234
+ ```
1235
+
1236
+ #### Strategy Callbacks
1237
+
1238
+ Partial profit/loss callbacks can also be configured at the strategy level:
1239
+
1240
+ ```typescript
1241
+ import { addStrategy } from "backtest-kit";
1242
+
1243
+ addStrategy({
1244
+ strategyName: "my-strategy",
1245
+ interval: "5m",
1246
+ getSignal: async (symbol) => { /* ... */ },
1247
+ callbacks: {
1248
+ onPartialProfit: (symbol, data, currentPrice, revenuePercent, backtest) => {
1249
+ console.log(`Signal ${data.id} at ${revenuePercent.toFixed(2)}% profit`);
1250
+ },
1251
+ onPartialLoss: (symbol, data, currentPrice, lossPercent, backtest) => {
1252
+ console.log(`Signal ${data.id} at ${lossPercent.toFixed(2)}% loss`);
1253
+ },
1254
+ },
1255
+ });
1256
+ ```
1257
+
1258
+ #### How Partial Levels Work
1259
+
1260
+ **Architecture:**
1261
+
1262
+ 1. `ClientPartial` - Tracks levels using `Map<signalId, Set<level>>` to prevent duplicates
1263
+ 2. `ClientStrategy` - Calls `partial.profit()` / `partial.loss()` on every tick
1264
+ 3. `PartialMarkdownService` - Accumulates events (max 250 per symbol) for reports
1265
+ 4. State persisted to disk: `./dump/data/partial/{symbol}/levels.json`
1266
+
1267
+ **Level Detection:**
1268
+
1269
+ ```typescript
1270
+ // For LONG position at entry price 50000
1271
+ // Current price = 55000 β†’ revenue = 10%
1272
+ // Levels triggered: 10%
1273
+
1274
+ // Current price = 61000 β†’ revenue = 22%
1275
+ // Levels triggered: 10%, 20% (only 20% event emitted if 10% already triggered)
1276
+
1277
+ // For SHORT position at entry price 50000
1278
+ // Current price = 45000 β†’ revenue = 10%
1279
+ // Levels triggered: 10%
1280
+ ```
1281
+
1282
+ **Deduplication Guarantee:**
1283
+
1284
+ Each level is emitted **exactly once per signal**:
1285
+
1286
+ - Uses `Set<level>` to track reached levels
1287
+ - Persisted to disk for crash recovery
1288
+ - Restored on system restart
1289
+
1290
+ **Crash Recovery:**
1291
+
1292
+ ```typescript
1293
+ // Before crash:
1294
+ // Signal opened at 50000, reached 10% and 20% profit
1295
+ // State: { profitLevels: [10, 20], lossLevels: [] }
1296
+ // Persisted to: ./dump/data/partial/BTCUSDT/levels.json
1297
+
1298
+ // After restart:
1299
+ // State restored from disk
1300
+ // Only new levels (30%, 40%, etc.) will emit events
1301
+ // 10% and 20% won't fire again
1302
+ ```
1303
+
1304
+ #### Best Practices
1305
+
1306
+ 1. **Use Constant for Kelly-Optimized Levels** - Don't hardcode profit/loss levels
1307
+ 2. **Scale Out Gradually** - Close positions in tranches (25%, 50%, 100%)
1308
+ 3. **Monitor Partial Statistics** - Use `Partial.getData()` to track scaling effectiveness
1309
+ 4. **Filter Events** - Use `listenPartialProfitOnce` for first-level-only logic
1310
+ 5. **Combine with Position Sizing** - Scale out inversely to volatility
1311
+
1312
+ ```typescript
1313
+ import { Constant, listenPartialProfit } from "backtest-kit";
1314
+
1315
+ // Advanced: Dynamic scaling based on level
1316
+ listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1317
+ const percentToClose =
1318
+ level === Constant.TP_LEVEL3 ? 0.25 : // 25% at first level
1319
+ level === Constant.TP_LEVEL2 ? 0.35 : // 35% at second level
1320
+ level === Constant.TP_LEVEL1 ? 0.40 : // 40% at third level
1321
+ 0;
1322
+
1323
+ if (percentToClose > 0) {
1324
+ executePartialClose(symbol, signal.id, percentToClose);
1325
+ }
1326
+ });
1327
+ ```
1328
+
1329
+ ---
1330
+
1331
+ ### 11. Scheduled Signal Persistence
1332
+
1333
+ The framework includes a separate persistence system for scheduled signals (`PersistScheduleAdapter`) that works independently from pending/active signal persistence (`PersistSignalAdapter`). This separation ensures crash-safe recovery of both signal types.
1334
+
1335
+ #### Understanding the Dual Persistence System
1336
+
1337
+ The library uses **two independent persistence layers** for signals:
1338
+
1339
+ 1. **PersistSignalAdapter** - Manages pending/active signals (signals that are already opened or waiting to reach TP/SL)
1340
+ 2. **PersistScheduleAdapter** - Manages scheduled signals (signals waiting for entry price to activate)
1341
+
1342
+ This dual-layer architecture ensures that both signal types can be recovered independently after crashes, with proper callbacks (`onActive` for pending signals, `onSchedule` for scheduled signals).
1343
+
1344
+ #### Default Storage Structure
1345
+
1346
+ By default, scheduled signals are stored separately from pending signals:
1347
+
1348
+ ```
1349
+ ./dump/data/
1350
+ signal/
1351
+ my-strategy/
1352
+ BTCUSDT.json # Pending/active signal state
1353
+ ETHUSDT.json
1354
+ schedule/
1355
+ my-strategy/
1356
+ BTCUSDT.json # Scheduled signal state
1357
+ ETHUSDT.json
1358
+ ```
1359
+
1360
+ #### How Scheduled Signal Persistence Works
1361
+
1362
+ **During Normal Operation:**
1363
+
1364
+ When a strategy generates a scheduled signal (limit order waiting for entry), the framework:
1365
+
1366
+ 1. Stores the signal to disk using atomic writes: `./dump/data/schedule/{strategyName}/{symbol}.json`
1367
+ 2. Monitors price movements for activation
1368
+ 3. When price reaches entry point OR cancellation condition occurs:
1369
+ - Deletes scheduled signal from storage
1370
+ - Optionally creates pending signal in `PersistSignalAdapter`
1371
+
1372
+ **After System Crash:**
1373
+
1374
+ When the system restarts:
1375
+
1376
+ 1. Framework checks for stored scheduled signals during initialization
1377
+ 2. Validates exchange name and strategy name match (security protection)
1378
+ 3. Restores scheduled signal to memory (`_scheduledSignal`)
1379
+ 4. Calls `onSchedule()` callback to notify about restored signal
1380
+ 5. Continues monitoring from where it left off
1381
+
1382
+ **Crash Recovery Flow:**
1383
+
1384
+ ```typescript
1385
+ // Before crash:
1386
+ // 1. Strategy generates signal with priceOpen = 50000 (current price = 49500)
1387
+ // 2. Signal stored to ./dump/data/schedule/my-strategy/BTCUSDT.json
1388
+ // 3. System waits for price to reach 50000
1389
+ // 4. CRASH OCCURS at current price = 49800
1390
+
1391
+ // After restart:
1392
+ // 1. System reads ./dump/data/schedule/my-strategy/BTCUSDT.json
1393
+ // 2. Validates exchangeName and strategyName
1394
+ // 3. Restores signal to _scheduledSignal
1395
+ // 4. Calls onSchedule() callback with restored signal
1396
+ // 5. Continues monitoring for price = 50000
1397
+ // 6. When price reaches 50000, signal activates normally
1398
+ ```
1399
+
1400
+ #### Scheduled Signal Data Structure
1401
+
1402
+ ```typescript
1403
+ interface IScheduledSignalRow {
1404
+ id: string; // Unique signal ID
1405
+ position: "long" | "short";
1406
+ priceOpen: number; // Entry price (trigger price for scheduled signal)
1407
+ priceTakeProfit: number;
1408
+ priceStopLoss: number;
1409
+ minuteEstimatedTime: number;
1410
+ exchangeName: string; // Used for validation during restore
1411
+ strategyName: string; // Used for validation during restore
1412
+ timestamp: number;
1413
+ pendingAt: number;
1414
+ scheduledAt: number;
1415
+ symbol: string;
1416
+ _isScheduled: true; // Marker for scheduled signals
1417
+ note?: string;
1418
+ }
1419
+ ```
1420
+
1421
+ #### Integration with ClientStrategy
1422
+
1423
+ The `ClientStrategy` class uses `setScheduledSignal()` method to ensure all scheduled signal changes are persisted:
1424
+
1425
+ ```typescript
1426
+ // WRONG - Direct assignment (not persisted)
1427
+ this._scheduledSignal = newSignal;
1428
+
1429
+ // CORRECT - Using setScheduledSignal() method (persisted)
1430
+ await this.setScheduledSignal(newSignal);
1431
+ ```
1432
+
1433
+ **Automatic Persistence Locations:**
1434
+
1435
+ All scheduled signal state changes are automatically persisted:
1436
+
1437
+ - Signal generation (new scheduled signal created)
1438
+ - Signal activation (scheduled β†’ pending transition)
1439
+ - Signal cancellation (timeout or stop loss hit before activation)
1440
+ - Manual signal clearing
1441
+
1442
+ **BACKTEST Mode Exception:**
1443
+
1444
+ In backtest mode, persistence is **skipped** for performance reasons:
1445
+
1446
+ ```typescript
1447
+ public async setScheduledSignal(scheduledSignal: IScheduledSignalRow | null) {
1448
+ this._scheduledSignal = scheduledSignal;
1449
+
1450
+ if (this.params.execution.context.backtest) {
1451
+ return; // Skip persistence in backtest mode
1452
+ }
1453
+
1454
+ await PersistScheduleAdapter.writeScheduleData(
1455
+ this._scheduledSignal,
1456
+ this.params.strategyName,
1457
+ this.params.execution.context.symbol
1458
+ );
1459
+ }
1460
+ ```
1461
+
1462
+ #### Custom Scheduled Signal Adapters
1463
+
1464
+ You can replace file-based scheduled signal persistence with custom adapters (Redis, MongoDB, etc.):
1465
+
1466
+ ```typescript
1467
+ import { PersistScheduleAdapter, PersistBase } from "backtest-kit";
1468
+ import Redis from "ioredis";
1469
+
1470
+ const redis = new Redis();
1471
+
1472
+ class RedisSchedulePersist extends PersistBase {
1473
+ async waitForInit(initial: boolean): Promise<void> {
1474
+ console.log(`Redis scheduled signal persistence initialized for ${this.entityName}`);
1475
+ }
1476
+
1477
+ async readValue<T>(entityId: string | number): Promise<T> {
1478
+ const key = `schedule:${this.entityName}:${entityId}`;
1479
+ const data = await redis.get(key);
1480
+
1481
+ if (!data) {
1482
+ throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found`);
1483
+ }
1484
+
1485
+ return JSON.parse(data) as T;
1486
+ }
1487
+
1488
+ async hasValue(entityId: string | number): Promise<boolean> {
1489
+ const key = `schedule:${this.entityName}:${entityId}`;
1490
+ const exists = await redis.exists(key);
1491
+ return exists === 1;
1492
+ }
1493
+
1494
+ async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
1495
+ const key = `schedule:${this.entityName}:${entityId}`;
1496
+ const serializedData = JSON.stringify(entity);
1497
+ await redis.set(key, serializedData);
1498
+
1499
+ // Optional: Set TTL for scheduled signals (e.g., 24 hours)
1500
+ await redis.expire(key, 86400);
1501
+ }
1502
+
1503
+ async removeValue(entityId: string | number): Promise<void> {
1504
+ const key = `schedule:${this.entityName}:${entityId}`;
1505
+ const result = await redis.del(key);
1506
+
1507
+ if (result === 0) {
1508
+ throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found for deletion`);
1509
+ }
1510
+ }
1511
+
1512
+ async removeAll(): Promise<void> {
1513
+ const pattern = `schedule:${this.entityName}:*`;
1514
+ const keys = await redis.keys(pattern);
1515
+
1516
+ if (keys.length > 0) {
1517
+ await redis.del(...keys);
1518
+ }
1519
+ }
1520
+
1521
+ async *values<T>(): AsyncGenerator<T> {
1522
+ const pattern = `schedule:${this.entityName}:*`;
1523
+ const keys = await redis.keys(pattern);
1524
+
1525
+ keys.sort((a, b) => a.localeCompare(b, undefined, {
1526
+ numeric: true,
1527
+ sensitivity: "base"
1528
+ }));
1529
+
1530
+ for (const key of keys) {
1531
+ const data = await redis.get(key);
1532
+ if (data) {
1533
+ yield JSON.parse(data) as T;
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ async *keys(): AsyncGenerator<string> {
1539
+ const pattern = `schedule:${this.entityName}:*`;
1540
+ const keys = await redis.keys(pattern);
1541
+
1542
+ keys.sort((a, b) => a.localeCompare(b, undefined, {
1543
+ numeric: true,
1544
+ sensitivity: "base"
1545
+ }));
1546
+
1547
+ for (const key of keys) {
1548
+ const entityId = key.slice(`schedule:${this.entityName}:`.length);
1549
+ yield entityId;
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ // Register Redis adapter for scheduled signal persistence
1555
+ PersistScheduleAdapter.usePersistScheduleAdapter(RedisSchedulePersist);
1556
+ ```
1557
+
1558
+ #### Best Practices
1559
+
1560
+ 1. **Always use `setScheduledSignal()`** - Never assign `_scheduledSignal` directly (except in `waitForInit` for restoration)
1561
+
1562
+ 2. **Validate signal metadata** - Always store `exchangeName` and `strategyName` with signals for validation
1563
+
1564
+ 3. **Handle empty storage gracefully** - Don't crash when `readScheduleData()` returns `null`
1565
+
1566
+ 4. **Test crash recovery** - Write E2E tests that simulate system crashes and verify restoration
1567
+
1568
+ 5. **Choose persistence adapter wisely**:
1569
+ - Use file-based for single-instance deployments
1570
+ - Use Redis for distributed systems with multiple instances
1571
+ - Use MongoDB for analytics and complex queries
1572
+
1573
+ 6. **Monitor persistence operations** - Use callbacks to track storage operations:
1574
+
1575
+ ```typescript
1576
+ addStrategy({
1577
+ strategyName: "my-strategy",
1578
+ interval: "5m",
1579
+ getSignal: async (symbol) => { /* ... */ },
1580
+ callbacks: {
1581
+ onSchedule: (symbol, signal, price, backtest) => {
1582
+ console.log(`Scheduled signal created/restored: ${signal.id}`);
1583
+ // Signal was either:
1584
+ // 1. Newly generated and persisted
1585
+ // 2. Restored from storage after crash
1586
+ },
1587
+ onCancel: (symbol, signal, price, backtest) => {
1588
+ console.log(`Scheduled signal cancelled: ${signal.id}`);
1589
+ // Signal was removed from storage
1590
+ },
1591
+ },
1592
+ });
1593
+ ```
1594
+
1595
+ ---
1596
+
1597
+ ## πŸ€– AI Strategy Optimizer
1598
+
1599
+ The Optimizer uses LLM (Large Language Models) to generate trading strategies from historical data. It automates the process of analyzing backtest results, generating strategy logic, and creating executable code.
1600
+
1601
+ ### How It Works
1602
+
1603
+ 1. **Data Collection**: Fetch historical data from multiple sources (backtest results, market data, indicators)
1604
+ 2. **LLM Training**: Build conversation context with data for each training period
1605
+ 3. **Strategy Generation**: LLM analyzes patterns and generates strategy logic
1606
+ 4. **Code Export**: Auto-generate complete executable code with Walker for testing
1607
+
1608
+ ### Basic Example
1609
+
1610
+ ```typescript
1611
+ import { addOptimizer, Optimizer } from "backtest-kit";
1612
+
1613
+ // Register optimizer configuration
1614
+ addOptimizer({
1615
+ optimizerName: "btc-optimizer",
1616
+
1617
+ // Training periods (multiple strategies generated)
1618
+ rangeTrain: [
1619
+ {
1620
+ note: "Bull market Q1 2024",
1621
+ startDate: new Date("2024-01-01T00:00:00Z"),
1622
+ endDate: new Date("2024-03-31T23:59:59Z"),
1623
+ },
1624
+ {
1625
+ note: "Consolidation Q2 2024",
1626
+ startDate: new Date("2024-04-01T00:00:00Z"),
1627
+ endDate: new Date("2024-06-30T23:59:59Z"),
1628
+ },
1629
+ ],
1630
+
1631
+ // Testing period (Walker validates strategies)
1632
+ rangeTest: {
1633
+ note: "Validation Q3 2024",
1634
+ startDate: new Date("2024-07-01T00:00:00Z"),
1635
+ endDate: new Date("2024-09-30T23:59:59Z"),
1636
+ },
1637
+
1638
+ // Data sources for strategy generation
1639
+ source: [
1640
+ {
1641
+ name: "backtest-results",
1642
+ fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
1643
+ // Fetch closed signals from your backtest database
1644
+ return await db.getBacktestResults({
1645
+ symbol,
1646
+ startDate,
1647
+ endDate,
1648
+ limit,
1649
+ offset,
1650
+ });
1651
+ },
1652
+ },
1653
+ {
1654
+ name: "market-indicators",
1655
+ fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
1656
+ // Fetch RSI, MACD, volume data, etc.
1657
+ return await db.getIndicators({
1658
+ symbol,
1659
+ startDate,
1660
+ endDate,
1661
+ limit,
1662
+ offset,
1663
+ });
1664
+ },
1665
+ },
1666
+ ],
1667
+
1668
+ // LLM prompt generation from conversation history
1669
+ getPrompt: async (symbol, messages) => {
1670
+ // Analyze messages and create strategy prompt
1671
+ return `
1672
+ Based on the historical data, create a strategy that:
1673
+ - Uses multi-timeframe analysis (1h, 15m, 5m, 1m)
1674
+ - Identifies high-probability entry points
1675
+ - Uses proper risk/reward ratios (min 1.5:1)
1676
+ - Adapts to market conditions
1677
+ `;
1678
+ },
1679
+ });
1680
+
1681
+ // Generate strategies and export code
1682
+ await Optimizer.dump("BTCUSDT", {
1683
+ optimizerName: "btc-optimizer"
1684
+ }, "./generated");
1685
+
1686
+ // Output: ./generated/btc-optimizer_BTCUSDT.mjs
1687
+ ```
1688
+
1689
+ ### Generated Code Structure
1690
+
1691
+ The Optimizer auto-generates a complete executable file with:
1692
+
1693
+ 1. **Imports** - All necessary dependencies (backtest-kit, ollama, ccxt)
1694
+ 2. **Helper Functions**:
1695
+ - `text()` - LLM text generation for analysis
1696
+ - `json()` - Structured signal generation with schema validation
1697
+ - `dumpJson()` - Debug logging to ./dump/strategy
1698
+ 3. **Exchange Configuration** - CCXT Binance integration
1699
+ 4. **Frame Definitions** - Training and testing timeframes
1700
+ 5. **Strategy Implementations** - One per training range with multi-timeframe analysis
1701
+ 6. **Walker Setup** - Automatic strategy comparison on test period
1702
+ 7. **Event Listeners** - Progress tracking and result collection
1703
+
1704
+ ### Advanced Configuration
1705
+
1706
+ #### Custom Message Templates
1707
+
1708
+ Override default LLM message formatting:
1709
+
1710
+ ```typescript
1711
+ addOptimizer({
1712
+ optimizerName: "custom-optimizer",
1713
+ rangeTrain: [...],
1714
+ rangeTest: {...},
1715
+ source: [...],
1716
+ getPrompt: async (symbol, messages) => "...",
1717
+
1718
+ // Custom templates
1719
+ template: {
1720
+ getUserMessage: async (symbol, data, sourceName) => {
1721
+ return `Analyze ${sourceName} data for ${symbol}:\n${JSON.stringify(data, null, 2)}`;
1722
+ },
1723
+ getAssistantMessage: async (symbol, data, sourceName) => {
1724
+ return `Data from ${sourceName} analyzed successfully`;
1725
+ },
1726
+ },
1727
+ });
1728
+ ```
1729
+
1730
+ #### Lifecycle Callbacks
1731
+
1732
+ Monitor optimizer operations:
1733
+
1734
+ ```typescript
1735
+ addOptimizer({
1736
+ optimizerName: "monitored-optimizer",
1737
+ rangeTrain: [...],
1738
+ rangeTest: {...},
1739
+ source: [...],
1740
+ getPrompt: async (symbol, messages) => "...",
1741
+
1742
+ callbacks: {
1743
+ onSourceData: async (symbol, sourceName, data, startDate, endDate) => {
1744
+ console.log(`Fetched ${data.length} rows from ${sourceName}`);
1745
+ },
1746
+ onData: async (symbol, strategyData) => {
1747
+ console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
1748
+ },
1749
+ onCode: async (symbol, code) => {
1750
+ console.log(`Generated ${code.length} bytes of code`);
1751
+ },
1752
+ onDump: async (symbol, filepath) => {
1753
+ console.log(`Saved strategy to ${filepath}`);
1754
+ },
1755
+ },
1756
+ });
1757
+ ```
1758
+
1759
+ #### Multiple Data Sources
1760
+
1761
+ Combine different data types for comprehensive analysis:
1762
+
1763
+ ```typescript
1764
+ addOptimizer({
1765
+ optimizerName: "multi-source-optimizer",
1766
+ rangeTrain: [...],
1767
+ rangeTest: {...},
1768
+
1769
+ source: [
1770
+ // Source 1: Backtest results
1771
+ {
1772
+ name: "backtest-signals",
1773
+ fetch: async (args) => await getBacktestSignals(args),
1774
+ },
1775
+
1776
+ // Source 2: Market indicators
1777
+ {
1778
+ name: "technical-indicators",
1779
+ fetch: async (args) => await getTechnicalIndicators(args),
1780
+ },
1781
+
1782
+ // Source 3: Volume profile
1783
+ {
1784
+ name: "volume-analysis",
1785
+ fetch: async (args) => await getVolumeProfile(args),
1786
+ },
1787
+
1788
+ // Source 4: Order book depth
1789
+ {
1790
+ name: "order-book",
1791
+ fetch: async (args) => await getOrderBookData(args),
1792
+ },
1793
+ ],
1794
+
1795
+ getPrompt: async (symbol, messages) => {
1796
+ // LLM has full context from all sources
1797
+ return `Create strategy using all available data sources`;
1798
+ },
1799
+ });
1800
+ ```
1801
+
1802
+ ### API Methods
1803
+
1804
+ ```typescript
1805
+ // Get strategy metadata (no code generation)
1806
+ const strategies = await Optimizer.getData("BTCUSDT", {
1807
+ optimizerName: "my-optimizer"
1808
+ });
1809
+
1810
+ // strategies[0].messages - LLM conversation history
1811
+ // strategies[0].strategy - Generated strategy prompt
1812
+
1813
+ // Generate executable code
1814
+ const code = await Optimizer.getCode("BTCUSDT", {
1815
+ optimizerName: "my-optimizer"
1816
+ });
1817
+
1818
+ // Save to file
1819
+ await Optimizer.dump("BTCUSDT", {
1820
+ optimizerName: "my-optimizer"
1821
+ }, "./output"); // Default: "./"
1822
+ ```
1823
+
1824
+ ### LLM Integration
1825
+
1826
+ The Optimizer uses Ollama for LLM inference:
1827
+
1828
+ ```bash
1829
+ # Set up Ollama API
1830
+ export OLLAMA_API_KEY=your-api-key
1831
+
1832
+ # Run generated strategy
1833
+ node generated/btc-optimizer_BTCUSDT.mjs
1834
+ ```
1835
+
1836
+ Generated strategies use:
1837
+ - **Model**: `gpt-oss:20b` (configurable in templates)
1838
+ - **Multi-timeframe analysis**: 1h, 15m, 5m, 1m candles
1839
+ - **Structured output**: JSON schema with signal validation
1840
+ - **Debug logging**: Saves conversations to ./dump/strategy
1841
+
1842
+ ### Best Practices
1843
+
1844
+ 1. **Training Periods**: Use 2-4 diverse market conditions (bull, bear, sideways)
1845
+ 2. **Data Quality**: Ensure data sources have unique IDs for deduplication
1846
+ 3. **Pagination**: Sources automatically paginated (25 records per request)
1847
+ 4. **Testing**: Always validate generated strategies on unseen data (rangeTest)
1848
+ 5. **Monitoring**: Use callbacks to track data fetching and code generation
1849
+
1074
1850
  ---
1075
1851
 
1076
1852
  ## πŸ“ Architecture Overview
@@ -1273,6 +2049,63 @@ listenSignal((event) => {
1273
2049
  });
1274
2050
  ```
1275
2051
 
2052
+ ### Listen to Partial Profit/Loss Events
2053
+
2054
+ ```typescript
2055
+ import {
2056
+ listenPartialProfit,
2057
+ listenPartialLoss,
2058
+ listenPartialProfitOnce,
2059
+ listenPartialLossOnce,
2060
+ Constant
2061
+ } from "backtest-kit";
2062
+
2063
+ // Listen to all profit milestones
2064
+ listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
2065
+ console.log(`${symbol} reached ${level}% profit at ${price}`);
2066
+
2067
+ // Scale out at Kelly-optimized levels
2068
+ if (level === Constant.TP_LEVEL3) {
2069
+ console.log("Close 33% at 25% profit");
2070
+ }
2071
+ if (level === Constant.TP_LEVEL2) {
2072
+ console.log("Close 33% at 50% profit");
2073
+ }
2074
+ if (level === Constant.TP_LEVEL1) {
2075
+ console.log("Close 34% at 100% profit");
2076
+ }
2077
+ });
2078
+
2079
+ // Listen to all loss milestones
2080
+ listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
2081
+ console.log(`${symbol} reached -${level}% loss at ${price}`);
2082
+
2083
+ // Scale out at stop levels
2084
+ if (level === Constant.SL_LEVEL2) {
2085
+ console.log("Close 50% at -50% loss");
2086
+ }
2087
+ if (level === Constant.SL_LEVEL1) {
2088
+ console.log("Close 50% at -100% loss");
2089
+ }
2090
+ });
2091
+
2092
+ // Listen once to first profit level
2093
+ listenPartialProfitOnce(
2094
+ () => true, // Accept any profit event
2095
+ ({ symbol, signal, price, level, backtest }) => {
2096
+ console.log(`First profit milestone: ${level}%`);
2097
+ }
2098
+ );
2099
+
2100
+ // Listen once to first loss level
2101
+ listenPartialLossOnce(
2102
+ () => true, // Accept any loss event
2103
+ ({ symbol, signal, price, level, backtest }) => {
2104
+ console.log(`First loss milestone: -${level}%`);
2105
+ }
2106
+ );
2107
+ ```
2108
+
1276
2109
  ### Listen Once with Filter
1277
2110
 
1278
2111
  ```typescript
@@ -1432,11 +2265,13 @@ await setConfig({
1432
2265
 
1433
2266
  ## βœ… Tested & Reliable
1434
2267
 
1435
- `backtest-kit` comes with **123 unit and integration tests** covering:
2268
+ `backtest-kit` comes with **217 unit and integration tests** covering:
1436
2269
 
1437
2270
  - Signal validation and throttling
1438
2271
  - PNL calculation with fees and slippage
1439
2272
  - Crash recovery and state persistence
2273
+ - Dual-layer persistence (pending signals and scheduled signals)
2274
+ - Crash recovery validation (exchange/strategy name mismatch protection)
1440
2275
  - Callback execution order (onSchedule, onOpen, onActive, onClose, onCancel)
1441
2276
  - Markdown report generation (backtest, live, scheduled signals)
1442
2277
  - Walker strategy comparison