backtest-kit 1.1.9 → 1.2.1
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/README.md +396 -34
- package/build/index.cjs +1906 -450
- package/build/index.mjs +1905 -451
- package/package.json +1 -1
- package/types.d.ts +530 -28
package/build/index.cjs
CHANGED
|
@@ -8,6 +8,75 @@ var path = require('path');
|
|
|
8
8
|
var crypto = require('crypto');
|
|
9
9
|
var os = require('os');
|
|
10
10
|
|
|
11
|
+
const GLOBAL_CONFIG = {
|
|
12
|
+
/**
|
|
13
|
+
* Time to wait for scheduled signal to activate (in minutes)
|
|
14
|
+
* If signal does not activate within this time, it will be cancelled.
|
|
15
|
+
*/
|
|
16
|
+
CC_SCHEDULE_AWAIT_MINUTES: 120,
|
|
17
|
+
/**
|
|
18
|
+
* Number of candles to use for average price calculation (VWAP)
|
|
19
|
+
* Default: 5 candles (last 5 minutes when using 1m interval)
|
|
20
|
+
*/
|
|
21
|
+
CC_AVG_PRICE_CANDLES_COUNT: 5,
|
|
22
|
+
/**
|
|
23
|
+
* Minimum TakeProfit distance from priceOpen (percentage)
|
|
24
|
+
* Must be greater than trading fees to ensure profitable trades
|
|
25
|
+
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
26
|
+
*/
|
|
27
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.1,
|
|
28
|
+
/**
|
|
29
|
+
* Maximum StopLoss distance from priceOpen (percentage)
|
|
30
|
+
* Prevents catastrophic losses from extreme StopLoss values
|
|
31
|
+
* Default: 20% (one signal cannot lose more than 20% of position)
|
|
32
|
+
*/
|
|
33
|
+
CC_MAX_STOPLOSS_DISTANCE_PERCENT: 20,
|
|
34
|
+
/**
|
|
35
|
+
* Maximum signal lifetime in minutes
|
|
36
|
+
* Prevents eternal signals that block risk limits for weeks/months
|
|
37
|
+
* Default: 1440 minutes (1 day)
|
|
38
|
+
*/
|
|
39
|
+
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
40
|
+
/**
|
|
41
|
+
* Number of retries for getCandles function
|
|
42
|
+
* Default: 3 retries
|
|
43
|
+
*/
|
|
44
|
+
CC_GET_CANDLES_RETRY_COUNT: 3,
|
|
45
|
+
/**
|
|
46
|
+
* Delay between retries for getCandles function (in milliseconds)
|
|
47
|
+
* Default: 5000 ms (5 seconds)
|
|
48
|
+
*/
|
|
49
|
+
CC_GET_CANDLES_RETRY_DELAY_MS: 5000,
|
|
50
|
+
/**
|
|
51
|
+
* Maximum allowed deviation factor for price anomaly detection.
|
|
52
|
+
* Price should not be more than this factor lower than reference price.
|
|
53
|
+
*
|
|
54
|
+
* Reasoning:
|
|
55
|
+
* - Incomplete candles from Binance API typically have prices near 0 (e.g., $0.01-1)
|
|
56
|
+
* - Normal BTC price ranges: $20,000-100,000
|
|
57
|
+
* - Factor 1000 catches prices below $20-100 when median is $20,000-100,000
|
|
58
|
+
* - Factor 100 would be too permissive (allows $200 when median is $20,000)
|
|
59
|
+
* - Factor 10000 might be too strict for low-cap altcoins
|
|
60
|
+
*
|
|
61
|
+
* Example: BTC at $50,000 median → threshold $50 (catches $0.01-1 anomalies)
|
|
62
|
+
*/
|
|
63
|
+
CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR: 1000,
|
|
64
|
+
/**
|
|
65
|
+
* Minimum number of candles required for reliable median calculation.
|
|
66
|
+
* Below this threshold, use simple average instead of median.
|
|
67
|
+
*
|
|
68
|
+
* Reasoning:
|
|
69
|
+
* - Each candle provides 4 price points (OHLC)
|
|
70
|
+
* - 5 candles = 20 price points, sufficient for robust median calculation
|
|
71
|
+
* - Below 5 candles, single anomaly can heavily skew median
|
|
72
|
+
* - Statistical rule of thumb: minimum 7-10 data points for median stability
|
|
73
|
+
* - Average is more stable than median for small datasets (n < 20)
|
|
74
|
+
*
|
|
75
|
+
* Example: 3 candles = 12 points (use average), 5 candles = 20 points (use median)
|
|
76
|
+
*/
|
|
77
|
+
CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN: 5,
|
|
78
|
+
};
|
|
79
|
+
|
|
11
80
|
const { init, inject, provide } = diKit.createActivator("backtest");
|
|
12
81
|
|
|
13
82
|
/**
|
|
@@ -81,6 +150,7 @@ const logicPublicServices$1 = {
|
|
|
81
150
|
const markdownServices$1 = {
|
|
82
151
|
backtestMarkdownService: Symbol('backtestMarkdownService'),
|
|
83
152
|
liveMarkdownService: Symbol('liveMarkdownService'),
|
|
153
|
+
scheduleMarkdownService: Symbol('scheduleMarkdownService'),
|
|
84
154
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
85
155
|
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
86
156
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
@@ -239,6 +309,92 @@ const INTERVAL_MINUTES$2 = {
|
|
|
239
309
|
"6h": 360,
|
|
240
310
|
"8h": 480,
|
|
241
311
|
};
|
|
312
|
+
/**
|
|
313
|
+
* Validates that all candles have valid OHLCV data without anomalies.
|
|
314
|
+
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
315
|
+
* Incomplete candles often have prices like 0.1 instead of normal 100,000 or zero volume.
|
|
316
|
+
*
|
|
317
|
+
* @param candles - Array of candle data to validate
|
|
318
|
+
* @throws Error if any candles have anomalous OHLCV values
|
|
319
|
+
*/
|
|
320
|
+
const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
321
|
+
if (candles.length === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Calculate reference price (median or average depending on candle count)
|
|
325
|
+
const allPrices = candles.flatMap((c) => [c.open, c.high, c.low, c.close]);
|
|
326
|
+
const validPrices = allPrices.filter(p => p > 0);
|
|
327
|
+
let referencePrice;
|
|
328
|
+
if (candles.length >= GLOBAL_CONFIG.CC_GET_CANDLES_MIN_CANDLES_FOR_MEDIAN) {
|
|
329
|
+
// Use median for reliable statistics with enough data
|
|
330
|
+
const sortedPrices = [...validPrices].sort((a, b) => a - b);
|
|
331
|
+
referencePrice = sortedPrices[Math.floor(sortedPrices.length / 2)] || 0;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Use average for small datasets (more stable than median)
|
|
335
|
+
const sum = validPrices.reduce((acc, p) => acc + p, 0);
|
|
336
|
+
referencePrice = validPrices.length > 0 ? sum / validPrices.length : 0;
|
|
337
|
+
}
|
|
338
|
+
if (referencePrice === 0) {
|
|
339
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: cannot calculate reference price (all prices are zero)`);
|
|
340
|
+
}
|
|
341
|
+
const minValidPrice = referencePrice / GLOBAL_CONFIG.CC_GET_CANDLES_PRICE_ANOMALY_THRESHOLD_FACTOR;
|
|
342
|
+
for (let i = 0; i < candles.length; i++) {
|
|
343
|
+
const candle = candles[i];
|
|
344
|
+
// Check for invalid numeric values
|
|
345
|
+
if (!Number.isFinite(candle.open) ||
|
|
346
|
+
!Number.isFinite(candle.high) ||
|
|
347
|
+
!Number.isFinite(candle.low) ||
|
|
348
|
+
!Number.isFinite(candle.close) ||
|
|
349
|
+
!Number.isFinite(candle.volume) ||
|
|
350
|
+
!Number.isFinite(candle.timestamp)) {
|
|
351
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has invalid numeric values (NaN or Infinity)`);
|
|
352
|
+
}
|
|
353
|
+
// Check for negative values
|
|
354
|
+
if (candle.open <= 0 ||
|
|
355
|
+
candle.high <= 0 ||
|
|
356
|
+
candle.low <= 0 ||
|
|
357
|
+
candle.close <= 0 ||
|
|
358
|
+
candle.volume < 0) {
|
|
359
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has zero or negative values`);
|
|
360
|
+
}
|
|
361
|
+
// Check for anomalously low prices (incomplete candle indicator)
|
|
362
|
+
if (candle.open < minValidPrice ||
|
|
363
|
+
candle.high < minValidPrice ||
|
|
364
|
+
candle.low < minValidPrice ||
|
|
365
|
+
candle.close < minValidPrice) {
|
|
366
|
+
throw new Error(`VALIDATE_NO_INCOMPLETE_CANDLES_FN: candle[${i}] has anomalously low price. ` +
|
|
367
|
+
`OHLC: [${candle.open}, ${candle.high}, ${candle.low}, ${candle.close}], ` +
|
|
368
|
+
`reference: ${referencePrice}, threshold: ${minValidPrice}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Retries the getCandles function with specified retry count and delay.
|
|
374
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
375
|
+
* @param since - Date object representing the start time for fetching candles
|
|
376
|
+
* @param self - Instance of ClientExchange
|
|
377
|
+
* @returns Promise resolving to array of candle data
|
|
378
|
+
*/
|
|
379
|
+
const GET_CANDLES_FN = async (dto, since, self) => {
|
|
380
|
+
let lastError;
|
|
381
|
+
for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
|
|
382
|
+
try {
|
|
383
|
+
const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit);
|
|
384
|
+
VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
self.params.logger.warn(`ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`, {
|
|
389
|
+
error: functoolsKit.errorData(err),
|
|
390
|
+
message: functoolsKit.getErrorMessage(err),
|
|
391
|
+
});
|
|
392
|
+
lastError = err;
|
|
393
|
+
await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
throw lastError;
|
|
397
|
+
};
|
|
242
398
|
/**
|
|
243
399
|
* Client implementation for exchange data access.
|
|
244
400
|
*
|
|
@@ -289,7 +445,7 @@ class ClientExchange {
|
|
|
289
445
|
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
290
446
|
}
|
|
291
447
|
const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
|
|
292
|
-
const data = await
|
|
448
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
293
449
|
// Filter candles to strictly match the requested range
|
|
294
450
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
295
451
|
const sinceTimestamp = since.getTime();
|
|
@@ -327,7 +483,7 @@ class ClientExchange {
|
|
|
327
483
|
if (endTime > now) {
|
|
328
484
|
return [];
|
|
329
485
|
}
|
|
330
|
-
const data = await
|
|
486
|
+
const data = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
331
487
|
// Filter candles to strictly match the requested range
|
|
332
488
|
const sinceTimestamp = since.getTime();
|
|
333
489
|
const filteredData = data.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp <= endTime);
|
|
@@ -340,7 +496,8 @@ class ClientExchange {
|
|
|
340
496
|
return filteredData;
|
|
341
497
|
}
|
|
342
498
|
/**
|
|
343
|
-
* Calculates VWAP (Volume Weighted Average Price) from last
|
|
499
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
500
|
+
* The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
|
|
344
501
|
*
|
|
345
502
|
* Formula:
|
|
346
503
|
* - Typical Price = (high + low + close) / 3
|
|
@@ -356,7 +513,7 @@ class ClientExchange {
|
|
|
356
513
|
this.params.logger.debug(`ClientExchange getAveragePrice`, {
|
|
357
514
|
symbol,
|
|
358
515
|
});
|
|
359
|
-
const candles = await this.getCandles(symbol, "1m",
|
|
516
|
+
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
360
517
|
if (candles.length === 0) {
|
|
361
518
|
throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
|
|
362
519
|
}
|
|
@@ -1093,7 +1250,7 @@ class PersistSignalUtils {
|
|
|
1093
1250
|
* async readValue(id) { return JSON.parse(await redis.get(id)); }
|
|
1094
1251
|
* async writeValue(id, entity) { await redis.set(id, JSON.stringify(entity)); }
|
|
1095
1252
|
* }
|
|
1096
|
-
*
|
|
1253
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1097
1254
|
* ```
|
|
1098
1255
|
*/
|
|
1099
1256
|
usePersistSignalAdapter(Ctor) {
|
|
@@ -1108,16 +1265,16 @@ class PersistSignalUtils {
|
|
|
1108
1265
|
* @example
|
|
1109
1266
|
* ```typescript
|
|
1110
1267
|
* // Custom adapter
|
|
1111
|
-
*
|
|
1268
|
+
* PersistSignalAdapter.usePersistSignalAdapter(RedisPersist);
|
|
1112
1269
|
*
|
|
1113
1270
|
* // Read signal
|
|
1114
|
-
* const signal = await
|
|
1271
|
+
* const signal = await PersistSignalAdapter.readSignalData("my-strategy", "BTCUSDT");
|
|
1115
1272
|
*
|
|
1116
1273
|
* // Write signal
|
|
1117
|
-
* await
|
|
1274
|
+
* await PersistSignalAdapter.writeSignalData(signal, "my-strategy", "BTCUSDT");
|
|
1118
1275
|
* ```
|
|
1119
1276
|
*/
|
|
1120
|
-
const
|
|
1277
|
+
const PersistSignalAdapter = new PersistSignalUtils();
|
|
1121
1278
|
const PERSIST_RISK_UTILS_METHOD_NAME_USE_PERSIST_RISK_ADAPTER = "PersistRiskUtils.usePersistRiskAdapter";
|
|
1122
1279
|
const PERSIST_RISK_UTILS_METHOD_NAME_READ_DATA = "PersistRiskUtils.readPositionData";
|
|
1123
1280
|
const PERSIST_RISK_UTILS_METHOD_NAME_WRITE_DATA = "PersistRiskUtils.writePositionData";
|
|
@@ -1302,14 +1459,24 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1302
1459
|
};
|
|
1303
1460
|
const VALIDATE_SIGNAL_FN = (signal) => {
|
|
1304
1461
|
const errors = [];
|
|
1305
|
-
//
|
|
1306
|
-
if (signal.priceOpen
|
|
1462
|
+
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
1463
|
+
if (!isFinite(signal.priceOpen)) {
|
|
1464
|
+
errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
1465
|
+
}
|
|
1466
|
+
if (!isFinite(signal.priceTakeProfit)) {
|
|
1467
|
+
errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
1468
|
+
}
|
|
1469
|
+
if (!isFinite(signal.priceStopLoss)) {
|
|
1470
|
+
errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
1471
|
+
}
|
|
1472
|
+
// Валидация цен (только если они конечные)
|
|
1473
|
+
if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
|
|
1307
1474
|
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
1308
1475
|
}
|
|
1309
|
-
if (signal.priceTakeProfit <= 0) {
|
|
1476
|
+
if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
|
|
1310
1477
|
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
1311
1478
|
}
|
|
1312
|
-
if (signal.priceStopLoss <= 0) {
|
|
1479
|
+
if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
|
|
1313
1480
|
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
1314
1481
|
}
|
|
1315
1482
|
// Валидация для long позиции
|
|
@@ -1320,6 +1487,24 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1320
1487
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1321
1488
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1322
1489
|
}
|
|
1490
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1491
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1492
|
+
const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1493
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1494
|
+
errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1495
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1496
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1500
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1501
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1502
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1503
|
+
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1504
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1505
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1323
1508
|
}
|
|
1324
1509
|
// Валидация для short позиции
|
|
1325
1510
|
if (signal.position === "short") {
|
|
@@ -1329,13 +1514,44 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1329
1514
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1330
1515
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1331
1516
|
}
|
|
1517
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1518
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1519
|
+
const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
|
|
1520
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1521
|
+
errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1522
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1523
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1527
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1528
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1529
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1530
|
+
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1531
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1532
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1332
1535
|
}
|
|
1333
1536
|
// Валидация временных параметров
|
|
1334
1537
|
if (signal.minuteEstimatedTime <= 0) {
|
|
1335
1538
|
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
1336
1539
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1540
|
+
// ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
|
|
1541
|
+
if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1542
|
+
if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1543
|
+
const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
|
|
1544
|
+
const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
|
|
1545
|
+
errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
|
|
1546
|
+
`Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
|
|
1547
|
+
`Eternal signals block risk limits and prevent new trades.`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (signal.scheduledAt <= 0) {
|
|
1551
|
+
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
1552
|
+
}
|
|
1553
|
+
if (signal.pendingAt <= 0) {
|
|
1554
|
+
errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
|
|
1339
1555
|
}
|
|
1340
1556
|
// Кидаем ошибку если есть проблемы
|
|
1341
1557
|
if (errors.length > 0) {
|
|
@@ -1371,6 +1587,27 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1371
1587
|
if (!signal) {
|
|
1372
1588
|
return null;
|
|
1373
1589
|
}
|
|
1590
|
+
// Если priceOpen указан - создаем scheduled signal (risk check при активации)
|
|
1591
|
+
if (signal.priceOpen !== undefined) {
|
|
1592
|
+
const scheduledSignalRow = {
|
|
1593
|
+
id: functoolsKit.randomString(),
|
|
1594
|
+
priceOpen: signal.priceOpen,
|
|
1595
|
+
position: signal.position,
|
|
1596
|
+
note: signal.note,
|
|
1597
|
+
priceTakeProfit: signal.priceTakeProfit,
|
|
1598
|
+
priceStopLoss: signal.priceStopLoss,
|
|
1599
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
1600
|
+
symbol: self.params.execution.context.symbol,
|
|
1601
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1602
|
+
strategyName: self.params.method.context.strategyName,
|
|
1603
|
+
scheduledAt: currentTime,
|
|
1604
|
+
pendingAt: currentTime, // Временно, обновится при активации
|
|
1605
|
+
_isScheduled: true,
|
|
1606
|
+
};
|
|
1607
|
+
// Валидируем сигнал перед возвратом
|
|
1608
|
+
VALIDATE_SIGNAL_FN(scheduledSignalRow);
|
|
1609
|
+
return scheduledSignalRow;
|
|
1610
|
+
}
|
|
1374
1611
|
const signalRow = {
|
|
1375
1612
|
id: functoolsKit.randomString(),
|
|
1376
1613
|
priceOpen: currentPrice,
|
|
@@ -1378,7 +1615,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1378
1615
|
symbol: self.params.execution.context.symbol,
|
|
1379
1616
|
exchangeName: self.params.method.context.exchangeName,
|
|
1380
1617
|
strategyName: self.params.method.context.strategyName,
|
|
1381
|
-
|
|
1618
|
+
scheduledAt: currentTime,
|
|
1619
|
+
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
1620
|
+
_isScheduled: false,
|
|
1382
1621
|
};
|
|
1383
1622
|
// Валидируем сигнал перед возвратом
|
|
1384
1623
|
VALIDATE_SIGNAL_FN(signalRow);
|
|
@@ -1408,7 +1647,7 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1408
1647
|
if (self.params.execution.context.backtest) {
|
|
1409
1648
|
return;
|
|
1410
1649
|
}
|
|
1411
|
-
const pendingSignal = await
|
|
1650
|
+
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.strategyName, self.params.execution.context.symbol);
|
|
1412
1651
|
if (!pendingSignal) {
|
|
1413
1652
|
return;
|
|
1414
1653
|
}
|
|
@@ -1419,6 +1658,563 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1419
1658
|
return;
|
|
1420
1659
|
}
|
|
1421
1660
|
self._pendingSignal = pendingSignal;
|
|
1661
|
+
// Call onActive callback for restored signal
|
|
1662
|
+
if (self.params.callbacks?.onActive) {
|
|
1663
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1664
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
1668
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1669
|
+
const signalTime = scheduled.scheduledAt; // Таймаут для scheduled signal считается от scheduledAt
|
|
1670
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
1671
|
+
const elapsedTime = currentTime - signalTime;
|
|
1672
|
+
if (elapsedTime < maxTimeToWait) {
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled by timeout", {
|
|
1676
|
+
symbol: self.params.execution.context.symbol,
|
|
1677
|
+
signalId: scheduled.id,
|
|
1678
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
1679
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
1680
|
+
});
|
|
1681
|
+
self._scheduledSignal = null;
|
|
1682
|
+
if (self.params.callbacks?.onCancel) {
|
|
1683
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
1684
|
+
}
|
|
1685
|
+
const result = {
|
|
1686
|
+
action: "cancelled",
|
|
1687
|
+
signal: scheduled,
|
|
1688
|
+
currentPrice: currentPrice,
|
|
1689
|
+
closeTimestamp: currentTime,
|
|
1690
|
+
strategyName: self.params.method.context.strategyName,
|
|
1691
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1692
|
+
symbol: self.params.execution.context.symbol,
|
|
1693
|
+
};
|
|
1694
|
+
if (self.params.callbacks?.onTick) {
|
|
1695
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1696
|
+
}
|
|
1697
|
+
return result;
|
|
1698
|
+
};
|
|
1699
|
+
const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
|
|
1700
|
+
let shouldActivate = false;
|
|
1701
|
+
let shouldCancel = false;
|
|
1702
|
+
if (scheduled.position === "long") {
|
|
1703
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1704
|
+
// Отмена если цена упала СЛИШКОМ низко (ниже SL)
|
|
1705
|
+
if (currentPrice <= scheduled.priceStopLoss) {
|
|
1706
|
+
shouldCancel = true;
|
|
1707
|
+
}
|
|
1708
|
+
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1709
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1710
|
+
else if (currentPrice <= scheduled.priceOpen) {
|
|
1711
|
+
shouldActivate = true;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (scheduled.position === "short") {
|
|
1715
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1716
|
+
// Отмена если цена выросла СЛИШКОМ высоко (выше SL)
|
|
1717
|
+
if (currentPrice >= scheduled.priceStopLoss) {
|
|
1718
|
+
shouldCancel = true;
|
|
1719
|
+
}
|
|
1720
|
+
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
1721
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1722
|
+
else if (currentPrice >= scheduled.priceOpen) {
|
|
1723
|
+
shouldActivate = true;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return { shouldActivate, shouldCancel };
|
|
1727
|
+
};
|
|
1728
|
+
const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPrice) => {
|
|
1729
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled", {
|
|
1730
|
+
symbol: self.params.execution.context.symbol,
|
|
1731
|
+
signalId: scheduled.id,
|
|
1732
|
+
position: scheduled.position,
|
|
1733
|
+
averagePrice: currentPrice,
|
|
1734
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1735
|
+
});
|
|
1736
|
+
self._scheduledSignal = null;
|
|
1737
|
+
const result = {
|
|
1738
|
+
action: "idle",
|
|
1739
|
+
signal: null,
|
|
1740
|
+
strategyName: self.params.method.context.strategyName,
|
|
1741
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1742
|
+
symbol: self.params.execution.context.symbol,
|
|
1743
|
+
currentPrice: currentPrice,
|
|
1744
|
+
};
|
|
1745
|
+
if (self.params.callbacks?.onTick) {
|
|
1746
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1747
|
+
}
|
|
1748
|
+
return result;
|
|
1749
|
+
};
|
|
1750
|
+
const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
|
|
1751
|
+
// Check if strategy was stopped
|
|
1752
|
+
if (self._isStopped) {
|
|
1753
|
+
self.params.logger.info("ClientStrategy scheduled signal activation cancelled (stopped)", {
|
|
1754
|
+
symbol: self.params.execution.context.symbol,
|
|
1755
|
+
signalId: scheduled.id,
|
|
1756
|
+
});
|
|
1757
|
+
self._scheduledSignal = null;
|
|
1758
|
+
return null;
|
|
1759
|
+
}
|
|
1760
|
+
// В LIVE режиме activationTimestamp - это текущее время при tick()
|
|
1761
|
+
// В отличие от BACKTEST (где используется candle.timestamp + 60s),
|
|
1762
|
+
// здесь мы не знаем ТОЧНОЕ время достижения priceOpen,
|
|
1763
|
+
// поэтому используем время обнаружения активации
|
|
1764
|
+
const activationTime = activationTimestamp;
|
|
1765
|
+
self.params.logger.info("ClientStrategy scheduled signal activation begin", {
|
|
1766
|
+
symbol: self.params.execution.context.symbol,
|
|
1767
|
+
signalId: scheduled.id,
|
|
1768
|
+
position: scheduled.position,
|
|
1769
|
+
averagePrice: scheduled.priceOpen,
|
|
1770
|
+
priceOpen: scheduled.priceOpen,
|
|
1771
|
+
scheduledAt: scheduled.scheduledAt,
|
|
1772
|
+
pendingAt: activationTime,
|
|
1773
|
+
});
|
|
1774
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1775
|
+
symbol: self.params.execution.context.symbol,
|
|
1776
|
+
strategyName: self.params.method.context.strategyName,
|
|
1777
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1778
|
+
currentPrice: scheduled.priceOpen,
|
|
1779
|
+
timestamp: activationTime,
|
|
1780
|
+
}))) {
|
|
1781
|
+
self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
|
|
1782
|
+
symbol: self.params.execution.context.symbol,
|
|
1783
|
+
signalId: scheduled.id,
|
|
1784
|
+
});
|
|
1785
|
+
self._scheduledSignal = null;
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
self._scheduledSignal = null;
|
|
1789
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
1790
|
+
const activatedSignal = {
|
|
1791
|
+
...scheduled,
|
|
1792
|
+
pendingAt: activationTime,
|
|
1793
|
+
_isScheduled: false,
|
|
1794
|
+
};
|
|
1795
|
+
await self.setPendingSignal(activatedSignal);
|
|
1796
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1797
|
+
strategyName: self.params.method.context.strategyName,
|
|
1798
|
+
riskName: self.params.riskName,
|
|
1799
|
+
});
|
|
1800
|
+
if (self.params.callbacks?.onOpen) {
|
|
1801
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
|
|
1802
|
+
}
|
|
1803
|
+
const result = {
|
|
1804
|
+
action: "opened",
|
|
1805
|
+
signal: self._pendingSignal,
|
|
1806
|
+
strategyName: self.params.method.context.strategyName,
|
|
1807
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1808
|
+
symbol: self.params.execution.context.symbol,
|
|
1809
|
+
currentPrice: self._pendingSignal.priceOpen,
|
|
1810
|
+
};
|
|
1811
|
+
if (self.params.callbacks?.onTick) {
|
|
1812
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1813
|
+
}
|
|
1814
|
+
return result;
|
|
1815
|
+
};
|
|
1816
|
+
const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
|
|
1817
|
+
const result = {
|
|
1818
|
+
action: "active",
|
|
1819
|
+
signal: scheduled,
|
|
1820
|
+
currentPrice: currentPrice,
|
|
1821
|
+
strategyName: self.params.method.context.strategyName,
|
|
1822
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1823
|
+
symbol: self.params.execution.context.symbol,
|
|
1824
|
+
};
|
|
1825
|
+
if (self.params.callbacks?.onTick) {
|
|
1826
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1827
|
+
}
|
|
1828
|
+
return result;
|
|
1829
|
+
};
|
|
1830
|
+
const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
1831
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1832
|
+
self.params.logger.info("ClientStrategy scheduled signal created", {
|
|
1833
|
+
symbol: self.params.execution.context.symbol,
|
|
1834
|
+
signalId: signal.id,
|
|
1835
|
+
position: signal.position,
|
|
1836
|
+
priceOpen: signal.priceOpen,
|
|
1837
|
+
currentPrice: currentPrice,
|
|
1838
|
+
});
|
|
1839
|
+
if (self.params.callbacks?.onSchedule) {
|
|
1840
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1841
|
+
}
|
|
1842
|
+
const result = {
|
|
1843
|
+
action: "scheduled",
|
|
1844
|
+
signal: signal,
|
|
1845
|
+
strategyName: self.params.method.context.strategyName,
|
|
1846
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1847
|
+
symbol: self.params.execution.context.symbol,
|
|
1848
|
+
currentPrice: currentPrice,
|
|
1849
|
+
};
|
|
1850
|
+
if (self.params.callbacks?.onTick) {
|
|
1851
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1852
|
+
}
|
|
1853
|
+
return result;
|
|
1854
|
+
};
|
|
1855
|
+
const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
1856
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1857
|
+
symbol: self.params.execution.context.symbol,
|
|
1858
|
+
strategyName: self.params.method.context.strategyName,
|
|
1859
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1860
|
+
currentPrice: signal.priceOpen,
|
|
1861
|
+
timestamp: self.params.execution.context.when.getTime(),
|
|
1862
|
+
}))) {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1866
|
+
strategyName: self.params.method.context.strategyName,
|
|
1867
|
+
riskName: self.params.riskName,
|
|
1868
|
+
});
|
|
1869
|
+
if (self.params.callbacks?.onOpen) {
|
|
1870
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
|
|
1871
|
+
}
|
|
1872
|
+
const result = {
|
|
1873
|
+
action: "opened",
|
|
1874
|
+
signal: signal,
|
|
1875
|
+
strategyName: self.params.method.context.strategyName,
|
|
1876
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1877
|
+
symbol: self.params.execution.context.symbol,
|
|
1878
|
+
currentPrice: signal.priceOpen,
|
|
1879
|
+
};
|
|
1880
|
+
if (self.params.callbacks?.onTick) {
|
|
1881
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1882
|
+
}
|
|
1883
|
+
return result;
|
|
1884
|
+
};
|
|
1885
|
+
const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
|
|
1886
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1887
|
+
const signalTime = signal.pendingAt; // КРИТИЧНО: используем pendingAt, а не scheduledAt!
|
|
1888
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
1889
|
+
const elapsedTime = currentTime - signalTime;
|
|
1890
|
+
// Check time expiration
|
|
1891
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
1892
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, averagePrice, "time_expired");
|
|
1893
|
+
}
|
|
1894
|
+
// Check take profit
|
|
1895
|
+
if (signal.position === "long" && averagePrice >= signal.priceTakeProfit) {
|
|
1896
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1897
|
+
"take_profit");
|
|
1898
|
+
}
|
|
1899
|
+
if (signal.position === "short" && averagePrice <= signal.priceTakeProfit) {
|
|
1900
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1901
|
+
"take_profit");
|
|
1902
|
+
}
|
|
1903
|
+
// Check stop loss
|
|
1904
|
+
if (signal.position === "long" && averagePrice <= signal.priceStopLoss) {
|
|
1905
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1906
|
+
"stop_loss");
|
|
1907
|
+
}
|
|
1908
|
+
if (signal.position === "short" && averagePrice >= signal.priceStopLoss) {
|
|
1909
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1910
|
+
"stop_loss");
|
|
1911
|
+
}
|
|
1912
|
+
return null;
|
|
1913
|
+
};
|
|
1914
|
+
const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
|
|
1915
|
+
const pnl = toProfitLossDto(signal, currentPrice);
|
|
1916
|
+
self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
|
|
1917
|
+
symbol: self.params.execution.context.symbol,
|
|
1918
|
+
signalId: signal.id,
|
|
1919
|
+
closeReason,
|
|
1920
|
+
priceClose: currentPrice,
|
|
1921
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
1922
|
+
});
|
|
1923
|
+
if (self.params.callbacks?.onClose) {
|
|
1924
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1925
|
+
}
|
|
1926
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1927
|
+
strategyName: self.params.method.context.strategyName,
|
|
1928
|
+
riskName: self.params.riskName,
|
|
1929
|
+
});
|
|
1930
|
+
await self.setPendingSignal(null);
|
|
1931
|
+
const result = {
|
|
1932
|
+
action: "closed",
|
|
1933
|
+
signal: signal,
|
|
1934
|
+
currentPrice: currentPrice,
|
|
1935
|
+
closeReason: closeReason,
|
|
1936
|
+
closeTimestamp: self.params.execution.context.when.getTime(),
|
|
1937
|
+
pnl: pnl,
|
|
1938
|
+
strategyName: self.params.method.context.strategyName,
|
|
1939
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1940
|
+
symbol: self.params.execution.context.symbol,
|
|
1941
|
+
};
|
|
1942
|
+
if (self.params.callbacks?.onTick) {
|
|
1943
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1944
|
+
}
|
|
1945
|
+
return result;
|
|
1946
|
+
};
|
|
1947
|
+
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
1948
|
+
const result = {
|
|
1949
|
+
action: "active",
|
|
1950
|
+
signal: signal,
|
|
1951
|
+
currentPrice: currentPrice,
|
|
1952
|
+
strategyName: self.params.method.context.strategyName,
|
|
1953
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1954
|
+
symbol: self.params.execution.context.symbol,
|
|
1955
|
+
};
|
|
1956
|
+
if (self.params.callbacks?.onTick) {
|
|
1957
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1958
|
+
}
|
|
1959
|
+
return result;
|
|
1960
|
+
};
|
|
1961
|
+
const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
1962
|
+
if (self.params.callbacks?.onIdle) {
|
|
1963
|
+
self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
|
|
1964
|
+
}
|
|
1965
|
+
const result = {
|
|
1966
|
+
action: "idle",
|
|
1967
|
+
signal: null,
|
|
1968
|
+
strategyName: self.params.method.context.strategyName,
|
|
1969
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1970
|
+
symbol: self.params.execution.context.symbol,
|
|
1971
|
+
currentPrice: currentPrice,
|
|
1972
|
+
};
|
|
1973
|
+
if (self.params.callbacks?.onTick) {
|
|
1974
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1975
|
+
}
|
|
1976
|
+
return result;
|
|
1977
|
+
};
|
|
1978
|
+
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp) => {
|
|
1979
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal cancelled", {
|
|
1980
|
+
symbol: self.params.execution.context.symbol,
|
|
1981
|
+
signalId: scheduled.id,
|
|
1982
|
+
closeTimestamp,
|
|
1983
|
+
averagePrice,
|
|
1984
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1985
|
+
});
|
|
1986
|
+
self._scheduledSignal = null;
|
|
1987
|
+
if (self.params.callbacks?.onCancel) {
|
|
1988
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
1989
|
+
}
|
|
1990
|
+
const result = {
|
|
1991
|
+
action: "cancelled",
|
|
1992
|
+
signal: scheduled,
|
|
1993
|
+
currentPrice: averagePrice,
|
|
1994
|
+
closeTimestamp: closeTimestamp,
|
|
1995
|
+
strategyName: self.params.method.context.strategyName,
|
|
1996
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1997
|
+
symbol: self.params.execution.context.symbol,
|
|
1998
|
+
};
|
|
1999
|
+
if (self.params.callbacks?.onTick) {
|
|
2000
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2001
|
+
}
|
|
2002
|
+
return result;
|
|
2003
|
+
};
|
|
2004
|
+
const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
|
|
2005
|
+
// Check if strategy was stopped
|
|
2006
|
+
if (self._isStopped) {
|
|
2007
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activation cancelled (stopped)", {
|
|
2008
|
+
symbol: self.params.execution.context.symbol,
|
|
2009
|
+
signalId: scheduled.id,
|
|
2010
|
+
});
|
|
2011
|
+
self._scheduledSignal = null;
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
// В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
|
|
2015
|
+
// (timestamp СЛЕДУЮЩЕЙ свечи после достижения priceOpen)
|
|
2016
|
+
// Это обеспечивает точный расчёт minuteEstimatedTime от момента активации
|
|
2017
|
+
const activationTime = activationTimestamp;
|
|
2018
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activated", {
|
|
2019
|
+
symbol: self.params.execution.context.symbol,
|
|
2020
|
+
signalId: scheduled.id,
|
|
2021
|
+
priceOpen: scheduled.priceOpen,
|
|
2022
|
+
scheduledAt: scheduled.scheduledAt,
|
|
2023
|
+
pendingAt: activationTime,
|
|
2024
|
+
});
|
|
2025
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
2026
|
+
symbol: self.params.execution.context.symbol,
|
|
2027
|
+
strategyName: self.params.method.context.strategyName,
|
|
2028
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
2029
|
+
currentPrice: scheduled.priceOpen,
|
|
2030
|
+
timestamp: activationTime,
|
|
2031
|
+
}))) {
|
|
2032
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
|
|
2033
|
+
symbol: self.params.execution.context.symbol,
|
|
2034
|
+
signalId: scheduled.id,
|
|
2035
|
+
});
|
|
2036
|
+
self._scheduledSignal = null;
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
self._scheduledSignal = null;
|
|
2040
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
|
|
2041
|
+
const activatedSignal = {
|
|
2042
|
+
...scheduled,
|
|
2043
|
+
pendingAt: activationTime,
|
|
2044
|
+
_isScheduled: false,
|
|
2045
|
+
};
|
|
2046
|
+
await self.setPendingSignal(activatedSignal);
|
|
2047
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
2048
|
+
strategyName: self.params.method.context.strategyName,
|
|
2049
|
+
riskName: self.params.riskName,
|
|
2050
|
+
});
|
|
2051
|
+
if (self.params.callbacks?.onOpen) {
|
|
2052
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
|
|
2053
|
+
}
|
|
2054
|
+
return true;
|
|
2055
|
+
};
|
|
2056
|
+
const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
|
|
2057
|
+
const pnl = toProfitLossDto(signal, averagePrice);
|
|
2058
|
+
self.params.logger.debug(`ClientStrategy backtest ${closeReason}`, {
|
|
2059
|
+
symbol: self.params.execution.context.symbol,
|
|
2060
|
+
signalId: signal.id,
|
|
2061
|
+
reason: closeReason,
|
|
2062
|
+
priceClose: averagePrice,
|
|
2063
|
+
closeTimestamp,
|
|
2064
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
2065
|
+
});
|
|
2066
|
+
if (closeReason === "stop_loss") {
|
|
2067
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2068
|
+
}
|
|
2069
|
+
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
2070
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2071
|
+
}
|
|
2072
|
+
if (self.params.callbacks?.onClose) {
|
|
2073
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
2074
|
+
}
|
|
2075
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
2076
|
+
strategyName: self.params.method.context.strategyName,
|
|
2077
|
+
riskName: self.params.riskName,
|
|
2078
|
+
});
|
|
2079
|
+
await self.setPendingSignal(null);
|
|
2080
|
+
const result = {
|
|
2081
|
+
action: "closed",
|
|
2082
|
+
signal: signal,
|
|
2083
|
+
currentPrice: averagePrice,
|
|
2084
|
+
closeReason: closeReason,
|
|
2085
|
+
closeTimestamp: closeTimestamp,
|
|
2086
|
+
pnl: pnl,
|
|
2087
|
+
strategyName: self.params.method.context.strategyName,
|
|
2088
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
2089
|
+
symbol: self.params.execution.context.symbol,
|
|
2090
|
+
};
|
|
2091
|
+
if (self.params.callbacks?.onTick) {
|
|
2092
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
2093
|
+
}
|
|
2094
|
+
return result;
|
|
2095
|
+
};
|
|
2096
|
+
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2097
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2098
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2099
|
+
for (let i = 0; i < candles.length; i++) {
|
|
2100
|
+
const candle = candles[i];
|
|
2101
|
+
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2102
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2103
|
+
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
2104
|
+
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
2105
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
2106
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
2107
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
2108
|
+
}
|
|
2109
|
+
let shouldActivate = false;
|
|
2110
|
+
let shouldCancel = false;
|
|
2111
|
+
if (scheduled.position === "long") {
|
|
2112
|
+
// КРИТИЧНО для LONG:
|
|
2113
|
+
// - priceOpen > priceStopLoss (по валидации)
|
|
2114
|
+
// - Активация: low <= priceOpen (цена упала до входа)
|
|
2115
|
+
// - Отмена: low <= priceStopLoss (цена пробила SL)
|
|
2116
|
+
//
|
|
2117
|
+
// EDGE CASE: если low <= priceStopLoss И low <= priceOpen на ОДНОЙ свече:
|
|
2118
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2119
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
2120
|
+
if (candle.low <= scheduled.priceStopLoss) {
|
|
2121
|
+
shouldCancel = true;
|
|
2122
|
+
}
|
|
2123
|
+
else if (candle.low <= scheduled.priceOpen) {
|
|
2124
|
+
shouldActivate = true;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (scheduled.position === "short") {
|
|
2128
|
+
// КРИТИЧНО для SHORT:
|
|
2129
|
+
// - priceOpen < priceStopLoss (по валидации)
|
|
2130
|
+
// - Активация: high >= priceOpen (цена выросла до входа)
|
|
2131
|
+
// - Отмена: high >= priceStopLoss (цена пробила SL)
|
|
2132
|
+
//
|
|
2133
|
+
// EDGE CASE: если high >= priceStopLoss И high >= priceOpen на ОДНОЙ свече:
|
|
2134
|
+
// => Отмена имеет ПРИОРИТЕТ! (SL пробит ДО или ВМЕСТЕ с активацией)
|
|
2135
|
+
// Сигнал НЕ открывается, сразу отменяется
|
|
2136
|
+
if (candle.high >= scheduled.priceStopLoss) {
|
|
2137
|
+
shouldCancel = true;
|
|
2138
|
+
}
|
|
2139
|
+
else if (candle.high >= scheduled.priceOpen) {
|
|
2140
|
+
shouldActivate = true;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (shouldCancel) {
|
|
2144
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
2145
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
2146
|
+
}
|
|
2147
|
+
if (shouldActivate) {
|
|
2148
|
+
await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
|
|
2149
|
+
return {
|
|
2150
|
+
activated: true,
|
|
2151
|
+
cancelled: false,
|
|
2152
|
+
activationIndex: i,
|
|
2153
|
+
result: null,
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
activated: false,
|
|
2159
|
+
cancelled: false,
|
|
2160
|
+
activationIndex: -1,
|
|
2161
|
+
result: null,
|
|
2162
|
+
};
|
|
2163
|
+
};
|
|
2164
|
+
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2165
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2166
|
+
for (let i = candlesCount - 1; i < candles.length; i++) {
|
|
2167
|
+
const recentCandles = candles.slice(i - (candlesCount - 1), i + 1);
|
|
2168
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2169
|
+
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2170
|
+
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2171
|
+
let shouldClose = false;
|
|
2172
|
+
let closeReason;
|
|
2173
|
+
// Check time expiration FIRST (КРИТИЧНО!)
|
|
2174
|
+
const signalTime = signal.pendingAt;
|
|
2175
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
2176
|
+
const elapsedTime = currentCandleTimestamp - signalTime;
|
|
2177
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
2178
|
+
shouldClose = true;
|
|
2179
|
+
closeReason = "time_expired";
|
|
2180
|
+
}
|
|
2181
|
+
// Check TP/SL only if not expired
|
|
2182
|
+
// КРИТИЧНО: используем candle.high/low для точной проверки достижения TP/SL
|
|
2183
|
+
if (!shouldClose && signal.position === "long") {
|
|
2184
|
+
// Для LONG: TP срабатывает если high >= TP, SL если low <= SL
|
|
2185
|
+
if (currentCandle.high >= signal.priceTakeProfit) {
|
|
2186
|
+
shouldClose = true;
|
|
2187
|
+
closeReason = "take_profit";
|
|
2188
|
+
}
|
|
2189
|
+
else if (currentCandle.low <= signal.priceStopLoss) {
|
|
2190
|
+
shouldClose = true;
|
|
2191
|
+
closeReason = "stop_loss";
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
if (!shouldClose && signal.position === "short") {
|
|
2195
|
+
// Для SHORT: TP срабатывает если low <= TP, SL если high >= SL
|
|
2196
|
+
if (currentCandle.low <= signal.priceTakeProfit) {
|
|
2197
|
+
shouldClose = true;
|
|
2198
|
+
closeReason = "take_profit";
|
|
2199
|
+
}
|
|
2200
|
+
else if (currentCandle.high >= signal.priceStopLoss) {
|
|
2201
|
+
shouldClose = true;
|
|
2202
|
+
closeReason = "stop_loss";
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (shouldClose) {
|
|
2206
|
+
// КРИТИЧНО: при закрытии по TP/SL используем точную цену, а не averagePrice
|
|
2207
|
+
let closePrice = averagePrice;
|
|
2208
|
+
if (closeReason === "take_profit") {
|
|
2209
|
+
closePrice = signal.priceTakeProfit;
|
|
2210
|
+
}
|
|
2211
|
+
else if (closeReason === "stop_loss") {
|
|
2212
|
+
closePrice = signal.priceStopLoss;
|
|
2213
|
+
}
|
|
2214
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return null;
|
|
1422
2218
|
};
|
|
1423
2219
|
/**
|
|
1424
2220
|
* Client implementation for trading strategy lifecycle management.
|
|
@@ -1452,6 +2248,7 @@ class ClientStrategy {
|
|
|
1452
2248
|
this.params = params;
|
|
1453
2249
|
this._isStopped = false;
|
|
1454
2250
|
this._pendingSignal = null;
|
|
2251
|
+
this._scheduledSignal = null;
|
|
1455
2252
|
this._lastSignalTimestamp = null;
|
|
1456
2253
|
/**
|
|
1457
2254
|
* Initializes strategy state by loading persisted signal from disk.
|
|
@@ -1478,22 +2275,34 @@ class ClientStrategy {
|
|
|
1478
2275
|
pendingSignal,
|
|
1479
2276
|
});
|
|
1480
2277
|
this._pendingSignal = pendingSignal;
|
|
2278
|
+
// КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
|
|
2279
|
+
// даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
|
|
2280
|
+
if (this.params.callbacks?.onWrite) {
|
|
2281
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, this._pendingSignal, this.params.execution.context.backtest);
|
|
2282
|
+
}
|
|
1481
2283
|
if (this.params.execution.context.backtest) {
|
|
1482
2284
|
return;
|
|
1483
2285
|
}
|
|
1484
|
-
await
|
|
2286
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.strategyName, this.params.execution.context.symbol);
|
|
1485
2287
|
}
|
|
1486
2288
|
/**
|
|
1487
2289
|
* Performs a single tick of strategy execution.
|
|
1488
2290
|
*
|
|
1489
|
-
* Flow:
|
|
1490
|
-
* 1. If
|
|
1491
|
-
* 2. If signal
|
|
1492
|
-
* 3. If
|
|
1493
|
-
* 4. If
|
|
2291
|
+
* Flow (LIVE mode):
|
|
2292
|
+
* 1. If scheduled signal exists: check activation/cancellation
|
|
2293
|
+
* 2. If no pending/scheduled signal: call getSignal with throttling and validation
|
|
2294
|
+
* 3. If signal opened: trigger onOpen callback, persist state
|
|
2295
|
+
* 4. If pending signal exists: check VWAP against TP/SL
|
|
2296
|
+
* 5. If TP/SL/time reached: close signal, trigger onClose, persist state
|
|
2297
|
+
*
|
|
2298
|
+
* Flow (BACKTEST mode):
|
|
2299
|
+
* 1. If no pending/scheduled signal: call getSignal
|
|
2300
|
+
* 2. If scheduled signal created: return "scheduled" (backtest() will handle it)
|
|
2301
|
+
* 3. Otherwise same as LIVE
|
|
1494
2302
|
*
|
|
1495
2303
|
* @returns Promise resolving to discriminated union result:
|
|
1496
2304
|
* - idle: No signal generated
|
|
2305
|
+
* - scheduled: Scheduled signal created (backtest only)
|
|
1497
2306
|
* - opened: New signal just created
|
|
1498
2307
|
* - active: Signal monitoring in progress
|
|
1499
2308
|
* - closed: Signal completed with PNL
|
|
@@ -1508,301 +2317,195 @@ class ClientStrategy {
|
|
|
1508
2317
|
*/
|
|
1509
2318
|
async tick() {
|
|
1510
2319
|
this.params.logger.debug("ClientStrategy tick");
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
// Register signal with risk management
|
|
1516
|
-
await this.params.risk.addSignal(this.params.execution.context.symbol, {
|
|
1517
|
-
strategyName: this.params.method.context.strategyName,
|
|
1518
|
-
riskName: this.params.riskName,
|
|
1519
|
-
});
|
|
1520
|
-
if (this.params.callbacks?.onOpen) {
|
|
1521
|
-
this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
|
|
1522
|
-
}
|
|
1523
|
-
const result = {
|
|
1524
|
-
action: "opened",
|
|
1525
|
-
signal: this._pendingSignal,
|
|
1526
|
-
strategyName: this.params.method.context.strategyName,
|
|
1527
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1528
|
-
symbol: this.params.execution.context.symbol,
|
|
1529
|
-
currentPrice: this._pendingSignal.priceOpen,
|
|
1530
|
-
};
|
|
1531
|
-
if (this.params.callbacks?.onTick) {
|
|
1532
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1533
|
-
}
|
|
1534
|
-
return result;
|
|
1535
|
-
}
|
|
2320
|
+
// Получаем текущее время в начале tick для консистентности
|
|
2321
|
+
const currentTime = this.params.execution.context.when.getTime();
|
|
2322
|
+
// Early return if strategy was stopped
|
|
2323
|
+
if (this._isStopped) {
|
|
1536
2324
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1537
|
-
|
|
1538
|
-
this.params.callbacks.onIdle(this.params.execution.context.symbol, currentPrice, this.params.execution.context.backtest);
|
|
1539
|
-
}
|
|
1540
|
-
const result = {
|
|
1541
|
-
action: "idle",
|
|
1542
|
-
signal: null,
|
|
1543
|
-
strategyName: this.params.method.context.strategyName,
|
|
1544
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1545
|
-
symbol: this.params.execution.context.symbol,
|
|
1546
|
-
currentPrice,
|
|
1547
|
-
};
|
|
1548
|
-
if (this.params.callbacks?.onTick) {
|
|
1549
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1550
|
-
}
|
|
1551
|
-
return result;
|
|
1552
|
-
}
|
|
1553
|
-
const when = this.params.execution.context.when;
|
|
1554
|
-
const signal = this._pendingSignal;
|
|
1555
|
-
// Получаем среднюю цену
|
|
1556
|
-
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1557
|
-
this.params.logger.debug("ClientStrategy tick check", {
|
|
1558
|
-
symbol: this.params.execution.context.symbol,
|
|
1559
|
-
averagePrice,
|
|
1560
|
-
signalId: signal.id,
|
|
1561
|
-
position: signal.position,
|
|
1562
|
-
});
|
|
1563
|
-
let shouldClose = false;
|
|
1564
|
-
let closeReason;
|
|
1565
|
-
// Проверяем истечение времени
|
|
1566
|
-
const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
|
|
1567
|
-
if (when.getTime() >= signalEndTime) {
|
|
1568
|
-
shouldClose = true;
|
|
1569
|
-
closeReason = "time_expired";
|
|
1570
|
-
}
|
|
1571
|
-
// Проверяем достижение TP/SL для long позиции
|
|
1572
|
-
if (signal.position === "long") {
|
|
1573
|
-
if (averagePrice >= signal.priceTakeProfit) {
|
|
1574
|
-
shouldClose = true;
|
|
1575
|
-
closeReason = "take_profit";
|
|
1576
|
-
}
|
|
1577
|
-
else if (averagePrice <= signal.priceStopLoss) {
|
|
1578
|
-
shouldClose = true;
|
|
1579
|
-
closeReason = "stop_loss";
|
|
1580
|
-
}
|
|
2325
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1581
2326
|
}
|
|
1582
|
-
//
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2327
|
+
// Monitor scheduled signal
|
|
2328
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2329
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2330
|
+
// Check timeout
|
|
2331
|
+
const timeoutResult = await CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN(this, this._scheduledSignal, currentPrice);
|
|
2332
|
+
if (timeoutResult)
|
|
2333
|
+
return timeoutResult;
|
|
2334
|
+
// Check price-based activation/cancellation
|
|
2335
|
+
const { shouldActivate, shouldCancel } = CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN(this._scheduledSignal, currentPrice);
|
|
2336
|
+
if (shouldCancel) {
|
|
2337
|
+
return await CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN(this, this._scheduledSignal, currentPrice);
|
|
1587
2338
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2339
|
+
if (shouldActivate) {
|
|
2340
|
+
const activateResult = await ACTIVATE_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal, currentTime);
|
|
2341
|
+
if (activateResult) {
|
|
2342
|
+
return activateResult;
|
|
2343
|
+
}
|
|
2344
|
+
// Risk rejected or stopped - return idle
|
|
2345
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1591
2346
|
}
|
|
2347
|
+
return await RETURN_SCHEDULED_SIGNAL_ACTIVE_FN(this, this._scheduledSignal, currentPrice);
|
|
1592
2348
|
}
|
|
1593
|
-
//
|
|
1594
|
-
if (
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1603
|
-
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1604
|
-
}
|
|
1605
|
-
this.params.logger.debug("ClientStrategy closing", {
|
|
1606
|
-
symbol: this.params.execution.context.symbol,
|
|
1607
|
-
signalId: signal.id,
|
|
1608
|
-
reason: closeReason,
|
|
1609
|
-
priceClose: averagePrice,
|
|
1610
|
-
closeTimestamp,
|
|
1611
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1612
|
-
});
|
|
1613
|
-
if (this.params.callbacks?.onClose) {
|
|
1614
|
-
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
2349
|
+
// Generate new signal if none exists
|
|
2350
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2351
|
+
const signal = await GET_SIGNAL_FN(this);
|
|
2352
|
+
if (signal) {
|
|
2353
|
+
if (signal._isScheduled === true) {
|
|
2354
|
+
this._scheduledSignal = signal;
|
|
2355
|
+
return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
|
|
2356
|
+
}
|
|
2357
|
+
await this.setPendingSignal(signal);
|
|
1615
2358
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
action: "closed",
|
|
1624
|
-
signal: signal,
|
|
1625
|
-
currentPrice: averagePrice,
|
|
1626
|
-
closeReason: closeReason,
|
|
1627
|
-
closeTimestamp: closeTimestamp,
|
|
1628
|
-
pnl: pnl,
|
|
1629
|
-
strategyName: this.params.method.context.strategyName,
|
|
1630
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1631
|
-
symbol: this.params.execution.context.symbol,
|
|
1632
|
-
};
|
|
1633
|
-
if (this.params.callbacks?.onTick) {
|
|
1634
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2359
|
+
if (this._pendingSignal) {
|
|
2360
|
+
const openResult = await OPEN_NEW_PENDING_SIGNAL_FN(this, this._pendingSignal);
|
|
2361
|
+
if (openResult) {
|
|
2362
|
+
return openResult;
|
|
2363
|
+
}
|
|
2364
|
+
// Risk rejected - clear pending signal and return idle
|
|
2365
|
+
await this.setPendingSignal(null);
|
|
1635
2366
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
if (this.params.callbacks?.onActive) {
|
|
1639
|
-
this.params.callbacks.onActive(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
2367
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2368
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1640
2369
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1647
|
-
symbol: this.params.execution.context.symbol,
|
|
1648
|
-
};
|
|
1649
|
-
if (this.params.callbacks?.onTick) {
|
|
1650
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2370
|
+
// Monitor pending signal
|
|
2371
|
+
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2372
|
+
const closedResult = await CHECK_PENDING_SIGNAL_COMPLETION_FN(this, this._pendingSignal, averagePrice);
|
|
2373
|
+
if (closedResult) {
|
|
2374
|
+
return closedResult;
|
|
1651
2375
|
}
|
|
1652
|
-
return
|
|
2376
|
+
return await RETURN_PENDING_SIGNAL_ACTIVE_FN(this, this._pendingSignal, averagePrice);
|
|
1653
2377
|
}
|
|
1654
2378
|
/**
|
|
1655
|
-
* Fast backtests a
|
|
2379
|
+
* Fast backtests a signal using historical candle data.
|
|
2380
|
+
*
|
|
2381
|
+
* For scheduled signals:
|
|
2382
|
+
* 1. Iterates through candles checking for activation (price reaches priceOpen)
|
|
2383
|
+
* 2. Or cancellation (price hits StopLoss before activation)
|
|
2384
|
+
* 3. If activated: converts to pending signal and continues with TP/SL monitoring
|
|
2385
|
+
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
1656
2386
|
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
2387
|
+
* For pending signals:
|
|
2388
|
+
* 1. Iterates through candles checking VWAP against TP/SL on each timeframe
|
|
2389
|
+
* 2. Starts from index 4 (needs 5 candles for VWAP calculation)
|
|
2390
|
+
* 3. Returns closed result (either TP/SL or time_expired)
|
|
1660
2391
|
*
|
|
1661
|
-
* @param candles - Array of candles
|
|
2392
|
+
* @param candles - Array of candles to process
|
|
1662
2393
|
* @returns Promise resolving to closed signal result with PNL
|
|
1663
|
-
* @throws Error if no pending signal or not in backtest mode
|
|
2394
|
+
* @throws Error if no pending/scheduled signal or not in backtest mode
|
|
1664
2395
|
*
|
|
1665
2396
|
* @example
|
|
1666
2397
|
* ```typescript
|
|
1667
2398
|
* // After signal opened in backtest
|
|
1668
2399
|
* const candles = await exchange.getNextCandles("BTCUSDT", "1m", signal.minuteEstimatedTime);
|
|
1669
2400
|
* const result = await strategy.backtest(candles);
|
|
1670
|
-
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
|
|
2401
|
+
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
|
|
1671
2402
|
* ```
|
|
1672
2403
|
*/
|
|
1673
2404
|
async backtest(candles) {
|
|
1674
2405
|
this.params.logger.debug("ClientStrategy backtest", {
|
|
1675
2406
|
symbol: this.params.execution.context.symbol,
|
|
1676
2407
|
candlesCount: candles.length,
|
|
2408
|
+
hasScheduled: !!this._scheduledSignal,
|
|
2409
|
+
hasPending: !!this._pendingSignal,
|
|
1677
2410
|
});
|
|
1678
|
-
const signal = this._pendingSignal;
|
|
1679
|
-
if (!signal) {
|
|
1680
|
-
throw new Error("ClientStrategy backtest: no pending signal");
|
|
1681
|
-
}
|
|
1682
2411
|
if (!this.params.execution.context.backtest) {
|
|
1683
2412
|
throw new Error("ClientStrategy backtest: running in live context");
|
|
1684
2413
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (
|
|
1699
|
-
|
|
1700
|
-
shouldClose = true;
|
|
1701
|
-
closeReason = "take_profit";
|
|
1702
|
-
}
|
|
1703
|
-
else if (averagePrice <= signal.priceStopLoss) {
|
|
1704
|
-
shouldClose = true;
|
|
1705
|
-
closeReason = "stop_loss";
|
|
1706
|
-
}
|
|
2414
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2415
|
+
throw new Error("ClientStrategy backtest: no pending or scheduled signal");
|
|
2416
|
+
}
|
|
2417
|
+
// Process scheduled signal
|
|
2418
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2419
|
+
const scheduled = this._scheduledSignal;
|
|
2420
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal", {
|
|
2421
|
+
symbol: this.params.execution.context.symbol,
|
|
2422
|
+
signalId: scheduled.id,
|
|
2423
|
+
priceOpen: scheduled.priceOpen,
|
|
2424
|
+
position: scheduled.position,
|
|
2425
|
+
});
|
|
2426
|
+
const { activated, cancelled, activationIndex, result } = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
2427
|
+
if (cancelled && result) {
|
|
2428
|
+
return result;
|
|
1707
2429
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
closeReason = "stop_loss";
|
|
2430
|
+
if (activated) {
|
|
2431
|
+
const remainingCandles = candles.slice(activationIndex + 1);
|
|
2432
|
+
if (remainingCandles.length === 0) {
|
|
2433
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2434
|
+
const recentCandles = candles.slice(Math.max(0, activationIndex - (candlesCount - 1)), activationIndex + 1);
|
|
2435
|
+
const lastPrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2436
|
+
const closeTimestamp = candles[activationIndex].timestamp;
|
|
2437
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, "time_expired", closeTimestamp);
|
|
1717
2438
|
}
|
|
2439
|
+
candles = remainingCandles;
|
|
1718
2440
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2441
|
+
if (this._scheduledSignal) {
|
|
2442
|
+
// Check if timeout reached (CC_SCHEDULE_AWAIT_MINUTES from scheduledAt)
|
|
2443
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2444
|
+
const lastCandleTimestamp = candles[candles.length - 1].timestamp;
|
|
2445
|
+
const elapsedTime = lastCandleTimestamp - scheduled.scheduledAt;
|
|
2446
|
+
if (elapsedTime < maxTimeToWait) {
|
|
2447
|
+
// Timeout NOT reached yet - signal is still active (waiting for price)
|
|
2448
|
+
// Return active result to continue monitoring in next backtest() call
|
|
2449
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2450
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2451
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2452
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal still waiting (not expired)", {
|
|
2453
|
+
symbol: this.params.execution.context.symbol,
|
|
2454
|
+
signalId: scheduled.id,
|
|
2455
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2456
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2457
|
+
});
|
|
2458
|
+
// Don't cancel - just return last active state
|
|
2459
|
+
// In real backtest flow this won't happen as we process all candles at once,
|
|
2460
|
+
// but this is correct behavior if someone calls backtest() with partial data
|
|
2461
|
+
const result = {
|
|
2462
|
+
action: "active",
|
|
2463
|
+
signal: scheduled,
|
|
2464
|
+
currentPrice: lastPrice,
|
|
2465
|
+
strategyName: this.params.method.context.strategyName,
|
|
2466
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
2467
|
+
symbol: this.params.execution.context.symbol,
|
|
2468
|
+
};
|
|
2469
|
+
return result; // Cast to IStrategyBacktestResult (which includes Active)
|
|
1737
2470
|
}
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
await this.setPendingSignal(null);
|
|
1744
|
-
const result = {
|
|
1745
|
-
action: "closed",
|
|
1746
|
-
signal: signal,
|
|
1747
|
-
currentPrice: averagePrice,
|
|
1748
|
-
closeReason: closeReason,
|
|
1749
|
-
closeTimestamp: closeTimestamp,
|
|
1750
|
-
pnl: pnl,
|
|
1751
|
-
strategyName: this.params.method.context.strategyName,
|
|
1752
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
2471
|
+
// Timeout reached - cancel the scheduled signal
|
|
2472
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2473
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2474
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2475
|
+
this.params.logger.info("ClientStrategy backtest scheduled signal cancelled by timeout", {
|
|
1753
2476
|
symbol: this.params.execution.context.symbol,
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2477
|
+
signalId: scheduled.id,
|
|
2478
|
+
closeTimestamp: lastCandleTimestamp,
|
|
2479
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2480
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2481
|
+
reason: "timeout - price never reached priceOpen",
|
|
2482
|
+
});
|
|
2483
|
+
return await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, lastCandleTimestamp);
|
|
1759
2484
|
}
|
|
1760
2485
|
}
|
|
1761
|
-
//
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const pnl = toProfitLossDto(signal, lastPrice);
|
|
1766
|
-
this.params.logger.debug("ClientStrategy backtest time_expired", {
|
|
1767
|
-
symbol: this.params.execution.context.symbol,
|
|
1768
|
-
signalId: signal.id,
|
|
1769
|
-
priceClose: lastPrice,
|
|
1770
|
-
closeTimestamp,
|
|
1771
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1772
|
-
});
|
|
1773
|
-
// Предупреждение при убытке от time_expired
|
|
1774
|
-
if (pnl.pnlPercentage < 0) {
|
|
1775
|
-
this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2486
|
+
// Process pending signal
|
|
2487
|
+
const signal = this._pendingSignal;
|
|
2488
|
+
if (!signal) {
|
|
2489
|
+
throw new Error("ClientStrategy backtest: no pending signal after scheduled activation");
|
|
1776
2490
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
2491
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2492
|
+
if (candles.length < candlesCount) {
|
|
2493
|
+
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
1779
2494
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
riskName: this.params.riskName,
|
|
1784
|
-
});
|
|
1785
|
-
await this.setPendingSignal(null);
|
|
1786
|
-
const result = {
|
|
1787
|
-
action: "closed",
|
|
1788
|
-
signal: signal,
|
|
1789
|
-
currentPrice: lastPrice,
|
|
1790
|
-
closeReason: "time_expired",
|
|
1791
|
-
closeTimestamp: closeTimestamp,
|
|
1792
|
-
pnl: pnl,
|
|
1793
|
-
strategyName: this.params.method.context.strategyName,
|
|
1794
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1795
|
-
symbol: this.params.execution.context.symbol,
|
|
1796
|
-
};
|
|
1797
|
-
if (this.params.callbacks?.onTick) {
|
|
1798
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2495
|
+
const closedResult = await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
2496
|
+
if (closedResult) {
|
|
2497
|
+
return closedResult;
|
|
1799
2498
|
}
|
|
1800
|
-
|
|
2499
|
+
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
2500
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2501
|
+
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
2502
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
|
|
1801
2503
|
}
|
|
1802
2504
|
/**
|
|
1803
2505
|
* Stops the strategy from generating new signals.
|
|
1804
2506
|
*
|
|
1805
2507
|
* Sets internal flag to prevent getSignal from being called.
|
|
2508
|
+
* Clears any scheduled signals (not yet activated).
|
|
1806
2509
|
* Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
|
|
1807
2510
|
*
|
|
1808
2511
|
* Use case: Graceful shutdown in live trading without forcing position closure.
|
|
@@ -1816,12 +2519,16 @@ class ClientStrategy {
|
|
|
1816
2519
|
* // Existing signal will continue until natural close
|
|
1817
2520
|
* ```
|
|
1818
2521
|
*/
|
|
1819
|
-
stop() {
|
|
2522
|
+
async stop() {
|
|
1820
2523
|
this.params.logger.debug("ClientStrategy stop", {
|
|
1821
2524
|
hasPendingSignal: this._pendingSignal !== null,
|
|
2525
|
+
hasScheduledSignal: this._scheduledSignal !== null,
|
|
1822
2526
|
});
|
|
1823
2527
|
this._isStopped = true;
|
|
1824
|
-
|
|
2528
|
+
// Clear scheduled signal if exists
|
|
2529
|
+
if (this._scheduledSignal) {
|
|
2530
|
+
this._scheduledSignal = null;
|
|
2531
|
+
}
|
|
1825
2532
|
}
|
|
1826
2533
|
}
|
|
1827
2534
|
|
|
@@ -2094,9 +2801,10 @@ class FrameConnectionService {
|
|
|
2094
2801
|
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2095
2802
|
* @returns Promise resolving to { startDate: Date, endDate: Date }
|
|
2096
2803
|
*/
|
|
2097
|
-
this.getTimeframe = async (symbol) => {
|
|
2804
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
2098
2805
|
this.loggerService.log("frameConnectionService getTimeframe", {
|
|
2099
2806
|
symbol,
|
|
2807
|
+
frameName,
|
|
2100
2808
|
});
|
|
2101
2809
|
return await this.getFrame(this.methodContextService.context.frameName).getTimeframe(symbol);
|
|
2102
2810
|
};
|
|
@@ -2577,9 +3285,21 @@ class RiskConnectionService {
|
|
|
2577
3285
|
});
|
|
2578
3286
|
await this.getRisk(context.riskName).removeSignal(symbol, context);
|
|
2579
3287
|
};
|
|
3288
|
+
/**
|
|
3289
|
+
* Clears the cached ClientRisk instance for the given risk name.
|
|
3290
|
+
*
|
|
3291
|
+
* @param riskName - Name of the risk schema to clear from cache
|
|
3292
|
+
*/
|
|
3293
|
+
this.clear = async (riskName) => {
|
|
3294
|
+
this.loggerService.log("riskConnectionService clear", {
|
|
3295
|
+
riskName,
|
|
3296
|
+
});
|
|
3297
|
+
this.getRisk.clear(riskName);
|
|
3298
|
+
};
|
|
2580
3299
|
}
|
|
2581
3300
|
}
|
|
2582
3301
|
|
|
3302
|
+
const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
|
|
2583
3303
|
/**
|
|
2584
3304
|
* Global service for exchange operations with execution context injection.
|
|
2585
3305
|
*
|
|
@@ -2592,6 +3312,21 @@ class ExchangeGlobalService {
|
|
|
2592
3312
|
constructor() {
|
|
2593
3313
|
this.loggerService = inject(TYPES.loggerService);
|
|
2594
3314
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
3315
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3316
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
3317
|
+
/**
|
|
3318
|
+
* Validates exchange configuration.
|
|
3319
|
+
* Memoized to avoid redundant validations for the same exchange.
|
|
3320
|
+
* Logs validation activity.
|
|
3321
|
+
* @param exchangeName - Name of the exchange to validate
|
|
3322
|
+
* @returns Promise that resolves when validation is complete
|
|
3323
|
+
*/
|
|
3324
|
+
this.validate = functoolsKit.memoize(([exchangeName]) => `${exchangeName}`, async (exchangeName) => {
|
|
3325
|
+
this.loggerService.log(METHOD_NAME_VALIDATE$1, {
|
|
3326
|
+
exchangeName,
|
|
3327
|
+
});
|
|
3328
|
+
this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$1);
|
|
3329
|
+
});
|
|
2595
3330
|
/**
|
|
2596
3331
|
* Fetches historical candles with execution context.
|
|
2597
3332
|
*
|
|
@@ -2610,6 +3345,7 @@ class ExchangeGlobalService {
|
|
|
2610
3345
|
when,
|
|
2611
3346
|
backtest,
|
|
2612
3347
|
});
|
|
3348
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2613
3349
|
return await ExecutionContextService.runInContext(async () => {
|
|
2614
3350
|
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
2615
3351
|
}, {
|
|
@@ -2636,6 +3372,7 @@ class ExchangeGlobalService {
|
|
|
2636
3372
|
when,
|
|
2637
3373
|
backtest,
|
|
2638
3374
|
});
|
|
3375
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2639
3376
|
return await ExecutionContextService.runInContext(async () => {
|
|
2640
3377
|
return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
2641
3378
|
}, {
|
|
@@ -2658,6 +3395,7 @@ class ExchangeGlobalService {
|
|
|
2658
3395
|
when,
|
|
2659
3396
|
backtest,
|
|
2660
3397
|
});
|
|
3398
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2661
3399
|
return await ExecutionContextService.runInContext(async () => {
|
|
2662
3400
|
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
2663
3401
|
}, {
|
|
@@ -2682,6 +3420,7 @@ class ExchangeGlobalService {
|
|
|
2682
3420
|
when,
|
|
2683
3421
|
backtest,
|
|
2684
3422
|
});
|
|
3423
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2685
3424
|
return await ExecutionContextService.runInContext(async () => {
|
|
2686
3425
|
return await this.exchangeConnectionService.formatPrice(symbol, price);
|
|
2687
3426
|
}, {
|
|
@@ -2706,6 +3445,7 @@ class ExchangeGlobalService {
|
|
|
2706
3445
|
when,
|
|
2707
3446
|
backtest,
|
|
2708
3447
|
});
|
|
3448
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2709
3449
|
return await ExecutionContextService.runInContext(async () => {
|
|
2710
3450
|
return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
2711
3451
|
}, {
|
|
@@ -2717,6 +3457,7 @@ class ExchangeGlobalService {
|
|
|
2717
3457
|
}
|
|
2718
3458
|
}
|
|
2719
3459
|
|
|
3460
|
+
const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
|
|
2720
3461
|
/**
|
|
2721
3462
|
* Global service for strategy operations with execution context injection.
|
|
2722
3463
|
*
|
|
@@ -2729,6 +3470,28 @@ class StrategyGlobalService {
|
|
|
2729
3470
|
constructor() {
|
|
2730
3471
|
this.loggerService = inject(TYPES.loggerService);
|
|
2731
3472
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
3473
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
3474
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3475
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3476
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3477
|
+
/**
|
|
3478
|
+
* Validates strategy and associated risk configuration.
|
|
3479
|
+
*
|
|
3480
|
+
* Memoized to avoid redundant validations for the same strategy.
|
|
3481
|
+
* Logs validation activity.
|
|
3482
|
+
* @param strategyName - Name of the strategy to validate
|
|
3483
|
+
* @returns Promise that resolves when validation is complete
|
|
3484
|
+
*/
|
|
3485
|
+
this.validate = functoolsKit.memoize(([strategyName]) => `${strategyName}`, async (strategyName) => {
|
|
3486
|
+
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
3487
|
+
strategyName,
|
|
3488
|
+
});
|
|
3489
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
3490
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_VALIDATE);
|
|
3491
|
+
const riskName = strategySchema.riskName;
|
|
3492
|
+
riskName &&
|
|
3493
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
|
|
3494
|
+
});
|
|
2732
3495
|
/**
|
|
2733
3496
|
* Checks signal status at a specific timestamp.
|
|
2734
3497
|
*
|
|
@@ -2746,6 +3509,7 @@ class StrategyGlobalService {
|
|
|
2746
3509
|
when,
|
|
2747
3510
|
backtest,
|
|
2748
3511
|
});
|
|
3512
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2749
3513
|
return await ExecutionContextService.runInContext(async () => {
|
|
2750
3514
|
return await this.strategyConnectionService.tick();
|
|
2751
3515
|
}, {
|
|
@@ -2773,6 +3537,7 @@ class StrategyGlobalService {
|
|
|
2773
3537
|
when,
|
|
2774
3538
|
backtest,
|
|
2775
3539
|
});
|
|
3540
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2776
3541
|
return await ExecutionContextService.runInContext(async () => {
|
|
2777
3542
|
return await this.strategyConnectionService.backtest(candles);
|
|
2778
3543
|
}, {
|
|
@@ -2794,6 +3559,7 @@ class StrategyGlobalService {
|
|
|
2794
3559
|
this.loggerService.log("strategyGlobalService stop", {
|
|
2795
3560
|
strategyName,
|
|
2796
3561
|
});
|
|
3562
|
+
await this.validate(strategyName);
|
|
2797
3563
|
return await this.strategyConnectionService.stop(strategyName);
|
|
2798
3564
|
};
|
|
2799
3565
|
/**
|
|
@@ -2808,11 +3574,15 @@ class StrategyGlobalService {
|
|
|
2808
3574
|
this.loggerService.log("strategyGlobalService clear", {
|
|
2809
3575
|
strategyName,
|
|
2810
3576
|
});
|
|
3577
|
+
if (strategyName) {
|
|
3578
|
+
await this.validate(strategyName);
|
|
3579
|
+
}
|
|
2811
3580
|
return await this.strategyConnectionService.clear(strategyName);
|
|
2812
3581
|
};
|
|
2813
3582
|
}
|
|
2814
3583
|
}
|
|
2815
3584
|
|
|
3585
|
+
const METHOD_NAME_GET_TIMEFRAME = "frameGlobalService getTimeframe";
|
|
2816
3586
|
/**
|
|
2817
3587
|
* Global service for frame operations.
|
|
2818
3588
|
*
|
|
@@ -2823,21 +3593,25 @@ class FrameGlobalService {
|
|
|
2823
3593
|
constructor() {
|
|
2824
3594
|
this.loggerService = inject(TYPES.loggerService);
|
|
2825
3595
|
this.frameConnectionService = inject(TYPES.frameConnectionService);
|
|
3596
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
2826
3597
|
/**
|
|
2827
3598
|
* Generates timeframe array for backtest iteration.
|
|
2828
3599
|
*
|
|
2829
|
-
* @param
|
|
3600
|
+
* @param frameName - Target frame name (e.g., "1m", "1h")
|
|
2830
3601
|
* @returns Promise resolving to array of Date objects
|
|
2831
3602
|
*/
|
|
2832
|
-
this.getTimeframe = async (symbol) => {
|
|
2833
|
-
this.loggerService.log(
|
|
3603
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
3604
|
+
this.loggerService.log(METHOD_NAME_GET_TIMEFRAME, {
|
|
3605
|
+
frameName,
|
|
2834
3606
|
symbol,
|
|
2835
3607
|
});
|
|
2836
|
-
|
|
3608
|
+
this.frameValidationService.validate(frameName, METHOD_NAME_GET_TIMEFRAME);
|
|
3609
|
+
return await this.frameConnectionService.getTimeframe(symbol, frameName);
|
|
2837
3610
|
};
|
|
2838
3611
|
}
|
|
2839
3612
|
}
|
|
2840
3613
|
|
|
3614
|
+
const METHOD_NAME_CALCULATE = "sizingGlobalService calculate";
|
|
2841
3615
|
/**
|
|
2842
3616
|
* Global service for sizing operations.
|
|
2843
3617
|
*
|
|
@@ -2848,6 +3622,7 @@ class SizingGlobalService {
|
|
|
2848
3622
|
constructor() {
|
|
2849
3623
|
this.loggerService = inject(TYPES.loggerService);
|
|
2850
3624
|
this.sizingConnectionService = inject(TYPES.sizingConnectionService);
|
|
3625
|
+
this.sizingValidationService = inject(TYPES.sizingValidationService);
|
|
2851
3626
|
/**
|
|
2852
3627
|
* Calculates position size based on risk parameters.
|
|
2853
3628
|
*
|
|
@@ -2856,11 +3631,12 @@ class SizingGlobalService {
|
|
|
2856
3631
|
* @returns Promise resolving to calculated position size
|
|
2857
3632
|
*/
|
|
2858
3633
|
this.calculate = async (params, context) => {
|
|
2859
|
-
this.loggerService.log(
|
|
3634
|
+
this.loggerService.log(METHOD_NAME_CALCULATE, {
|
|
2860
3635
|
symbol: params.symbol,
|
|
2861
3636
|
method: params.method,
|
|
2862
3637
|
context,
|
|
2863
3638
|
});
|
|
3639
|
+
this.sizingValidationService.validate(context.sizingName, METHOD_NAME_CALCULATE);
|
|
2864
3640
|
return await this.sizingConnectionService.calculate(params, context);
|
|
2865
3641
|
};
|
|
2866
3642
|
}
|
|
@@ -2876,6 +3652,20 @@ class RiskGlobalService {
|
|
|
2876
3652
|
constructor() {
|
|
2877
3653
|
this.loggerService = inject(TYPES.loggerService);
|
|
2878
3654
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
3655
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3656
|
+
/**
|
|
3657
|
+
* Validates risk configuration.
|
|
3658
|
+
* Memoized to avoid redundant validations for the same risk instance.
|
|
3659
|
+
* Logs validation activity.
|
|
3660
|
+
* @param riskName - Name of the risk instance to validate
|
|
3661
|
+
* @returns Promise that resolves when validation is complete
|
|
3662
|
+
*/
|
|
3663
|
+
this.validate = functoolsKit.memoize(([riskName]) => `${riskName}`, async (riskName) => {
|
|
3664
|
+
this.loggerService.log("riskGlobalService validate", {
|
|
3665
|
+
riskName,
|
|
3666
|
+
});
|
|
3667
|
+
this.riskValidationService.validate(riskName, "riskGlobalService validate");
|
|
3668
|
+
});
|
|
2879
3669
|
/**
|
|
2880
3670
|
* Checks if a signal should be allowed based on risk limits.
|
|
2881
3671
|
*
|
|
@@ -2888,6 +3678,7 @@ class RiskGlobalService {
|
|
|
2888
3678
|
symbol: params.symbol,
|
|
2889
3679
|
context,
|
|
2890
3680
|
});
|
|
3681
|
+
await this.validate(context.riskName);
|
|
2891
3682
|
return await this.riskConnectionService.checkSignal(params, context);
|
|
2892
3683
|
};
|
|
2893
3684
|
/**
|
|
@@ -2901,6 +3692,7 @@ class RiskGlobalService {
|
|
|
2901
3692
|
symbol,
|
|
2902
3693
|
context,
|
|
2903
3694
|
});
|
|
3695
|
+
await this.validate(context.riskName);
|
|
2904
3696
|
await this.riskConnectionService.addSignal(symbol, context);
|
|
2905
3697
|
};
|
|
2906
3698
|
/**
|
|
@@ -2914,8 +3706,24 @@ class RiskGlobalService {
|
|
|
2914
3706
|
symbol,
|
|
2915
3707
|
context,
|
|
2916
3708
|
});
|
|
3709
|
+
await this.validate(context.riskName);
|
|
2917
3710
|
await this.riskConnectionService.removeSignal(symbol, context);
|
|
2918
3711
|
};
|
|
3712
|
+
/**
|
|
3713
|
+
* Clears risk data.
|
|
3714
|
+
* If riskName is provided, clears data for that specific risk instance.
|
|
3715
|
+
* If no riskName is provided, clears all risk data.
|
|
3716
|
+
* @param riskName - Optional name of the risk instance to clear
|
|
3717
|
+
*/
|
|
3718
|
+
this.clear = async (riskName) => {
|
|
3719
|
+
this.loggerService.log("riskGlobalService clear", {
|
|
3720
|
+
riskName,
|
|
3721
|
+
});
|
|
3722
|
+
if (riskName) {
|
|
3723
|
+
await this.validate(riskName);
|
|
3724
|
+
}
|
|
3725
|
+
return await this.riskConnectionService.clear(riskName);
|
|
3726
|
+
};
|
|
2919
3727
|
}
|
|
2920
3728
|
}
|
|
2921
3729
|
|
|
@@ -3426,7 +4234,7 @@ class BacktestLogicPrivateService {
|
|
|
3426
4234
|
symbol,
|
|
3427
4235
|
});
|
|
3428
4236
|
const backtestStartTime = performance.now();
|
|
3429
|
-
const timeframes = await this.frameGlobalService.getTimeframe(symbol);
|
|
4237
|
+
const timeframes = await this.frameGlobalService.getTimeframe(symbol, this.methodContextService.context.frameName);
|
|
3430
4238
|
const totalFrames = timeframes.length;
|
|
3431
4239
|
let i = 0;
|
|
3432
4240
|
let previousEventTimestamp = null;
|
|
@@ -3445,7 +4253,66 @@ class BacktestLogicPrivateService {
|
|
|
3445
4253
|
});
|
|
3446
4254
|
}
|
|
3447
4255
|
const result = await this.strategyGlobalService.tick(symbol, when, true);
|
|
3448
|
-
// Если
|
|
4256
|
+
// Если scheduled signal создан - обрабатываем через backtest()
|
|
4257
|
+
if (result.action === "scheduled") {
|
|
4258
|
+
const signalStartTime = performance.now();
|
|
4259
|
+
const signal = result.signal;
|
|
4260
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
|
|
4261
|
+
symbol,
|
|
4262
|
+
signalId: signal.id,
|
|
4263
|
+
priceOpen: signal.priceOpen,
|
|
4264
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4265
|
+
});
|
|
4266
|
+
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4267
|
+
// КРИТИЧНО: запрашиваем CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4268
|
+
// + minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4269
|
+
// +1 потому что when включается как первая свеча (timestamp начинается с when, а не after when)
|
|
4270
|
+
const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4271
|
+
const candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, when, true);
|
|
4272
|
+
if (!candles.length) {
|
|
4273
|
+
i++;
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4276
|
+
this.loggerService.info("backtestLogicPrivateService candles fetched for scheduled", {
|
|
4277
|
+
symbol,
|
|
4278
|
+
signalId: signal.id,
|
|
4279
|
+
candlesCount: candles.length,
|
|
4280
|
+
candlesNeeded,
|
|
4281
|
+
});
|
|
4282
|
+
// backtest() сам обработает scheduled signal: найдет активацию/отмену
|
|
4283
|
+
// и если активируется - продолжит с TP/SL мониторингом
|
|
4284
|
+
const backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
|
|
4285
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal closed", {
|
|
4286
|
+
symbol,
|
|
4287
|
+
signalId: backtestResult.signal.id,
|
|
4288
|
+
closeTimestamp: backtestResult.closeTimestamp,
|
|
4289
|
+
action: backtestResult.action,
|
|
4290
|
+
closeReason: backtestResult.action === "closed"
|
|
4291
|
+
? backtestResult.closeReason
|
|
4292
|
+
: undefined,
|
|
4293
|
+
});
|
|
4294
|
+
// Track signal processing duration
|
|
4295
|
+
const signalEndTime = performance.now();
|
|
4296
|
+
const currentTimestamp = Date.now();
|
|
4297
|
+
await performanceEmitter.next({
|
|
4298
|
+
timestamp: currentTimestamp,
|
|
4299
|
+
previousTimestamp: previousEventTimestamp,
|
|
4300
|
+
metricType: "backtest_signal",
|
|
4301
|
+
duration: signalEndTime - signalStartTime,
|
|
4302
|
+
strategyName: this.methodContextService.context.strategyName,
|
|
4303
|
+
exchangeName: this.methodContextService.context.exchangeName,
|
|
4304
|
+
symbol,
|
|
4305
|
+
backtest: true,
|
|
4306
|
+
});
|
|
4307
|
+
previousEventTimestamp = currentTimestamp;
|
|
4308
|
+
// Пропускаем timeframes до closeTimestamp
|
|
4309
|
+
while (i < timeframes.length &&
|
|
4310
|
+
timeframes[i].getTime() < backtestResult.closeTimestamp) {
|
|
4311
|
+
i++;
|
|
4312
|
+
}
|
|
4313
|
+
yield backtestResult;
|
|
4314
|
+
}
|
|
4315
|
+
// Если обычный сигнал открыт, вызываем backtest
|
|
3449
4316
|
if (result.action === "opened") {
|
|
3450
4317
|
const signalStartTime = performance.now();
|
|
3451
4318
|
const signal = result.signal;
|
|
@@ -3470,7 +4337,6 @@ class BacktestLogicPrivateService {
|
|
|
3470
4337
|
symbol,
|
|
3471
4338
|
signalId: backtestResult.signal.id,
|
|
3472
4339
|
closeTimestamp: backtestResult.closeTimestamp,
|
|
3473
|
-
closeReason: backtestResult.closeReason,
|
|
3474
4340
|
});
|
|
3475
4341
|
// Track signal processing duration
|
|
3476
4342
|
const signalEndTime = performance.now();
|
|
@@ -3617,6 +4483,11 @@ class LiveLogicPrivateService {
|
|
|
3617
4483
|
await functoolsKit.sleep(TICK_TTL);
|
|
3618
4484
|
continue;
|
|
3619
4485
|
}
|
|
4486
|
+
if (result.action === "scheduled") {
|
|
4487
|
+
await functoolsKit.sleep(TICK_TTL);
|
|
4488
|
+
continue;
|
|
4489
|
+
}
|
|
4490
|
+
// Yield opened, closed, cancelled results
|
|
3620
4491
|
yield result;
|
|
3621
4492
|
await functoolsKit.sleep(TICK_TTL);
|
|
3622
4493
|
}
|
|
@@ -3937,6 +4808,8 @@ class LiveGlobalService {
|
|
|
3937
4808
|
this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
|
|
3938
4809
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3939
4810
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4811
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4812
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3940
4813
|
/**
|
|
3941
4814
|
* Runs live trading for a symbol with context propagation.
|
|
3942
4815
|
*
|
|
@@ -3951,8 +4824,16 @@ class LiveGlobalService {
|
|
|
3951
4824
|
symbol,
|
|
3952
4825
|
context,
|
|
3953
4826
|
});
|
|
3954
|
-
|
|
3955
|
-
|
|
4827
|
+
{
|
|
4828
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
|
|
4829
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
|
|
4830
|
+
}
|
|
4831
|
+
{
|
|
4832
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4833
|
+
const riskName = strategySchema.riskName;
|
|
4834
|
+
riskName &&
|
|
4835
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
|
|
4836
|
+
}
|
|
3956
4837
|
return this.liveLogicPublicService.run(symbol, context);
|
|
3957
4838
|
};
|
|
3958
4839
|
}
|
|
@@ -3968,6 +4849,8 @@ const METHOD_NAME_RUN$1 = "backtestGlobalService run";
|
|
|
3968
4849
|
class BacktestGlobalService {
|
|
3969
4850
|
constructor() {
|
|
3970
4851
|
this.loggerService = inject(TYPES.loggerService);
|
|
4852
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4853
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3971
4854
|
this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
3972
4855
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3973
4856
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
@@ -3984,9 +4867,16 @@ class BacktestGlobalService {
|
|
|
3984
4867
|
symbol,
|
|
3985
4868
|
context,
|
|
3986
4869
|
});
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4870
|
+
{
|
|
4871
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
|
|
4872
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
|
|
4873
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
|
|
4874
|
+
}
|
|
4875
|
+
{
|
|
4876
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4877
|
+
const riskName = strategySchema.riskName;
|
|
4878
|
+
riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
|
|
4879
|
+
}
|
|
3990
4880
|
return this.backtestLogicPublicService.run(symbol, context);
|
|
3991
4881
|
};
|
|
3992
4882
|
}
|
|
@@ -4003,6 +4893,13 @@ class WalkerGlobalService {
|
|
|
4003
4893
|
constructor() {
|
|
4004
4894
|
this.loggerService = inject(TYPES.loggerService);
|
|
4005
4895
|
this.walkerLogicPublicService = inject(TYPES.walkerLogicPublicService);
|
|
4896
|
+
this.walkerSchemaService = inject(TYPES.walkerSchemaService);
|
|
4897
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
4898
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4899
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
4900
|
+
this.walkerValidationService = inject(TYPES.walkerValidationService);
|
|
4901
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4902
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
4006
4903
|
/**
|
|
4007
4904
|
* Runs walker comparison for a symbol with context propagation.
|
|
4008
4905
|
*
|
|
@@ -4014,6 +4911,21 @@ class WalkerGlobalService {
|
|
|
4014
4911
|
symbol,
|
|
4015
4912
|
context,
|
|
4016
4913
|
});
|
|
4914
|
+
{
|
|
4915
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
|
|
4916
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
|
|
4917
|
+
this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
|
|
4918
|
+
}
|
|
4919
|
+
{
|
|
4920
|
+
const walkerSchema = this.walkerSchemaService.get(context.walkerName);
|
|
4921
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
4922
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
4923
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
|
|
4924
|
+
const riskName = strategySchema.riskName;
|
|
4925
|
+
riskName &&
|
|
4926
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4017
4929
|
return this.walkerLogicPublicService.run(symbol, context);
|
|
4018
4930
|
};
|
|
4019
4931
|
}
|
|
@@ -4037,7 +4949,7 @@ function isUnsafe$3(value) {
|
|
|
4037
4949
|
}
|
|
4038
4950
|
return false;
|
|
4039
4951
|
}
|
|
4040
|
-
const columns$
|
|
4952
|
+
const columns$3 = [
|
|
4041
4953
|
{
|
|
4042
4954
|
key: "signalId",
|
|
4043
4955
|
label: "Signal ID",
|
|
@@ -4095,7 +5007,7 @@ const columns$2 = [
|
|
|
4095
5007
|
key: "duration",
|
|
4096
5008
|
label: "Duration (min)",
|
|
4097
5009
|
format: (data) => {
|
|
4098
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
5010
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
4099
5011
|
const durationMin = Math.round(durationMs / 60000);
|
|
4100
5012
|
return `${durationMin}`;
|
|
4101
5013
|
},
|
|
@@ -4103,7 +5015,7 @@ const columns$2 = [
|
|
|
4103
5015
|
{
|
|
4104
5016
|
key: "openTimestamp",
|
|
4105
5017
|
label: "Open Time",
|
|
4106
|
-
format: (data) => new Date(data.signal.
|
|
5018
|
+
format: (data) => new Date(data.signal.pendingAt).toISOString(),
|
|
4107
5019
|
},
|
|
4108
5020
|
{
|
|
4109
5021
|
key: "closeTimestamp",
|
|
@@ -4115,7 +5027,7 @@ const columns$2 = [
|
|
|
4115
5027
|
* Storage class for accumulating closed signals per strategy.
|
|
4116
5028
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
4117
5029
|
*/
|
|
4118
|
-
let ReportStorage$
|
|
5030
|
+
let ReportStorage$3 = class ReportStorage {
|
|
4119
5031
|
constructor() {
|
|
4120
5032
|
/** Internal list of all closed signals for this strategy */
|
|
4121
5033
|
this._signalList = [];
|
|
@@ -4175,7 +5087,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4175
5087
|
: 0;
|
|
4176
5088
|
const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4177
5089
|
// Calculate Expected Yearly Returns
|
|
4178
|
-
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.
|
|
5090
|
+
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / totalSignals;
|
|
4179
5091
|
const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
|
|
4180
5092
|
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4181
5093
|
const expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
@@ -4205,9 +5117,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4205
5117
|
if (stats.totalSignals === 0) {
|
|
4206
5118
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
4207
5119
|
}
|
|
4208
|
-
const header = columns$
|
|
4209
|
-
const separator = columns$
|
|
4210
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
5120
|
+
const header = columns$3.map((col) => col.label);
|
|
5121
|
+
const separator = columns$3.map(() => "---");
|
|
5122
|
+
const rows = this._signalList.map((closedSignal) => columns$3.map((col) => col.format(closedSignal)));
|
|
4211
5123
|
const tableData = [header, separator, ...rows];
|
|
4212
5124
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4213
5125
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
@@ -4268,7 +5180,7 @@ class BacktestMarkdownService {
|
|
|
4268
5180
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
4269
5181
|
* Each strategy gets its own isolated storage instance.
|
|
4270
5182
|
*/
|
|
4271
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
5183
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
|
|
4272
5184
|
/**
|
|
4273
5185
|
* Processes tick events and accumulates closed signals.
|
|
4274
5186
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -4427,7 +5339,7 @@ function isUnsafe$2(value) {
|
|
|
4427
5339
|
}
|
|
4428
5340
|
return false;
|
|
4429
5341
|
}
|
|
4430
|
-
const columns$
|
|
5342
|
+
const columns$2 = [
|
|
4431
5343
|
{
|
|
4432
5344
|
key: "timestamp",
|
|
4433
5345
|
label: "Timestamp",
|
|
@@ -4501,12 +5413,12 @@ const columns$1 = [
|
|
|
4501
5413
|
},
|
|
4502
5414
|
];
|
|
4503
5415
|
/** Maximum number of events to store in live trading reports */
|
|
4504
|
-
const MAX_EVENTS$
|
|
5416
|
+
const MAX_EVENTS$2 = 250;
|
|
4505
5417
|
/**
|
|
4506
5418
|
* Storage class for accumulating all tick events per strategy.
|
|
4507
5419
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
4508
5420
|
*/
|
|
4509
|
-
let ReportStorage$
|
|
5421
|
+
let ReportStorage$2 = class ReportStorage {
|
|
4510
5422
|
constructor() {
|
|
4511
5423
|
/** Internal list of all tick events for this strategy */
|
|
4512
5424
|
this._eventList = [];
|
|
@@ -4534,7 +5446,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4534
5446
|
}
|
|
4535
5447
|
{
|
|
4536
5448
|
this._eventList.push(newEvent);
|
|
4537
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5449
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4538
5450
|
this._eventList.shift();
|
|
4539
5451
|
}
|
|
4540
5452
|
}
|
|
@@ -4546,7 +5458,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4546
5458
|
*/
|
|
4547
5459
|
addOpenedEvent(data) {
|
|
4548
5460
|
this._eventList.push({
|
|
4549
|
-
timestamp: data.signal.
|
|
5461
|
+
timestamp: data.signal.pendingAt,
|
|
4550
5462
|
action: "opened",
|
|
4551
5463
|
symbol: data.signal.symbol,
|
|
4552
5464
|
signalId: data.signal.id,
|
|
@@ -4558,7 +5470,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4558
5470
|
stopLoss: data.signal.priceStopLoss,
|
|
4559
5471
|
});
|
|
4560
5472
|
// Trim queue if exceeded MAX_EVENTS
|
|
4561
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5473
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4562
5474
|
this._eventList.shift();
|
|
4563
5475
|
}
|
|
4564
5476
|
}
|
|
@@ -4590,35 +5502,472 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4590
5502
|
else {
|
|
4591
5503
|
this._eventList.push(newEvent);
|
|
4592
5504
|
// Trim queue if exceeded MAX_EVENTS
|
|
4593
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5505
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
5506
|
+
this._eventList.shift();
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
/**
|
|
5511
|
+
* Updates or adds a closed event to the storage.
|
|
5512
|
+
* Replaces the previous event with the same signalId.
|
|
5513
|
+
*
|
|
5514
|
+
* @param data - Closed tick result
|
|
5515
|
+
*/
|
|
5516
|
+
addClosedEvent(data) {
|
|
5517
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
5518
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
5519
|
+
// Find existing event with the same signalId
|
|
5520
|
+
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
5521
|
+
const newEvent = {
|
|
5522
|
+
timestamp: data.closeTimestamp,
|
|
5523
|
+
action: "closed",
|
|
5524
|
+
symbol: data.signal.symbol,
|
|
5525
|
+
signalId: data.signal.id,
|
|
5526
|
+
position: data.signal.position,
|
|
5527
|
+
note: data.signal.note,
|
|
5528
|
+
currentPrice: data.currentPrice,
|
|
5529
|
+
openPrice: data.signal.priceOpen,
|
|
5530
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5531
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5532
|
+
pnl: data.pnl.pnlPercentage,
|
|
5533
|
+
closeReason: data.closeReason,
|
|
5534
|
+
duration: durationMin,
|
|
5535
|
+
};
|
|
5536
|
+
// Replace existing event or add new one
|
|
5537
|
+
if (existingIndex !== -1) {
|
|
5538
|
+
this._eventList[existingIndex] = newEvent;
|
|
5539
|
+
}
|
|
5540
|
+
else {
|
|
5541
|
+
this._eventList.push(newEvent);
|
|
5542
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5543
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4594
5544
|
this._eventList.shift();
|
|
4595
5545
|
}
|
|
4596
5546
|
}
|
|
4597
5547
|
}
|
|
4598
5548
|
/**
|
|
4599
|
-
*
|
|
5549
|
+
* Calculates statistical data from live trading events (Controller).
|
|
5550
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5551
|
+
*
|
|
5552
|
+
* @returns Statistical data (empty object if no events)
|
|
5553
|
+
*/
|
|
5554
|
+
async getData() {
|
|
5555
|
+
if (this._eventList.length === 0) {
|
|
5556
|
+
return {
|
|
5557
|
+
eventList: [],
|
|
5558
|
+
totalEvents: 0,
|
|
5559
|
+
totalClosed: 0,
|
|
5560
|
+
winCount: 0,
|
|
5561
|
+
lossCount: 0,
|
|
5562
|
+
winRate: null,
|
|
5563
|
+
avgPnl: null,
|
|
5564
|
+
totalPnl: null,
|
|
5565
|
+
stdDev: null,
|
|
5566
|
+
sharpeRatio: null,
|
|
5567
|
+
annualizedSharpeRatio: null,
|
|
5568
|
+
certaintyRatio: null,
|
|
5569
|
+
expectedYearlyReturns: null,
|
|
5570
|
+
};
|
|
5571
|
+
}
|
|
5572
|
+
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
5573
|
+
const totalClosed = closedEvents.length;
|
|
5574
|
+
const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
|
|
5575
|
+
const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
|
|
5576
|
+
// Calculate basic statistics
|
|
5577
|
+
const avgPnl = totalClosed > 0
|
|
5578
|
+
? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
|
|
5579
|
+
: 0;
|
|
5580
|
+
const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
|
|
5581
|
+
const winRate = (winCount / totalClosed) * 100;
|
|
5582
|
+
// Calculate Sharpe Ratio (risk-free rate = 0)
|
|
5583
|
+
let sharpeRatio = 0;
|
|
5584
|
+
let stdDev = 0;
|
|
5585
|
+
if (totalClosed > 0) {
|
|
5586
|
+
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
5587
|
+
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
5588
|
+
stdDev = Math.sqrt(variance);
|
|
5589
|
+
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
5590
|
+
}
|
|
5591
|
+
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
5592
|
+
// Calculate Certainty Ratio
|
|
5593
|
+
let certaintyRatio = 0;
|
|
5594
|
+
if (totalClosed > 0) {
|
|
5595
|
+
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
5596
|
+
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
5597
|
+
const avgWin = wins.length > 0
|
|
5598
|
+
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
5599
|
+
: 0;
|
|
5600
|
+
const avgLoss = losses.length > 0
|
|
5601
|
+
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
5602
|
+
: 0;
|
|
5603
|
+
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
5604
|
+
}
|
|
5605
|
+
// Calculate Expected Yearly Returns
|
|
5606
|
+
let expectedYearlyReturns = 0;
|
|
5607
|
+
if (totalClosed > 0) {
|
|
5608
|
+
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
5609
|
+
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
5610
|
+
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
5611
|
+
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
5612
|
+
}
|
|
5613
|
+
return {
|
|
5614
|
+
eventList: this._eventList,
|
|
5615
|
+
totalEvents: this._eventList.length,
|
|
5616
|
+
totalClosed,
|
|
5617
|
+
winCount,
|
|
5618
|
+
lossCount,
|
|
5619
|
+
winRate: isUnsafe$2(winRate) ? null : winRate,
|
|
5620
|
+
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
5621
|
+
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
5622
|
+
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
5623
|
+
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
5624
|
+
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
5625
|
+
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
5626
|
+
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
/**
|
|
5630
|
+
* Generates markdown report with all tick events for a strategy (View).
|
|
5631
|
+
*
|
|
5632
|
+
* @param strategyName - Strategy name
|
|
5633
|
+
* @returns Markdown formatted report with all events
|
|
5634
|
+
*/
|
|
5635
|
+
async getReport(strategyName) {
|
|
5636
|
+
const stats = await this.getData();
|
|
5637
|
+
if (stats.totalEvents === 0) {
|
|
5638
|
+
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
5639
|
+
}
|
|
5640
|
+
const header = columns$2.map((col) => col.label);
|
|
5641
|
+
const separator = columns$2.map(() => "---");
|
|
5642
|
+
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
5643
|
+
const tableData = [header, separator, ...rows];
|
|
5644
|
+
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5645
|
+
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
5646
|
+
}
|
|
5647
|
+
/**
|
|
5648
|
+
* Saves strategy report to disk.
|
|
5649
|
+
*
|
|
5650
|
+
* @param strategyName - Strategy name
|
|
5651
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5652
|
+
*/
|
|
5653
|
+
async dump(strategyName, path$1 = "./logs/live") {
|
|
5654
|
+
const markdown = await this.getReport(strategyName);
|
|
5655
|
+
try {
|
|
5656
|
+
const dir = path.join(process.cwd(), path$1);
|
|
5657
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5658
|
+
const filename = `${strategyName}.md`;
|
|
5659
|
+
const filepath = path.join(dir, filename);
|
|
5660
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
5661
|
+
console.log(`Live trading report saved: ${filepath}`);
|
|
5662
|
+
}
|
|
5663
|
+
catch (error) {
|
|
5664
|
+
console.error(`Failed to save markdown report:`, error);
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
};
|
|
5668
|
+
/**
|
|
5669
|
+
* Service for generating and saving live trading markdown reports.
|
|
5670
|
+
*
|
|
5671
|
+
* Features:
|
|
5672
|
+
* - Listens to all signal events via onTick callback
|
|
5673
|
+
* - Accumulates all events (idle, opened, active, closed) per strategy
|
|
5674
|
+
* - Generates markdown tables with detailed event information
|
|
5675
|
+
* - Provides trading statistics (win rate, average PNL)
|
|
5676
|
+
* - Saves reports to disk in logs/live/{strategyName}.md
|
|
5677
|
+
*
|
|
5678
|
+
* @example
|
|
5679
|
+
* ```typescript
|
|
5680
|
+
* const service = new LiveMarkdownService();
|
|
5681
|
+
*
|
|
5682
|
+
* // Add to strategy callbacks
|
|
5683
|
+
* addStrategy({
|
|
5684
|
+
* strategyName: "my-strategy",
|
|
5685
|
+
* callbacks: {
|
|
5686
|
+
* onTick: (symbol, result, backtest) => {
|
|
5687
|
+
* if (!backtest) {
|
|
5688
|
+
* service.tick(result);
|
|
5689
|
+
* }
|
|
5690
|
+
* }
|
|
5691
|
+
* }
|
|
5692
|
+
* });
|
|
5693
|
+
*
|
|
5694
|
+
* // Later: generate and save report
|
|
5695
|
+
* await service.dump("my-strategy");
|
|
5696
|
+
* ```
|
|
5697
|
+
*/
|
|
5698
|
+
class LiveMarkdownService {
|
|
5699
|
+
constructor() {
|
|
5700
|
+
/** Logger service for debug output */
|
|
5701
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
5702
|
+
/**
|
|
5703
|
+
* Memoized function to get or create ReportStorage for a strategy.
|
|
5704
|
+
* Each strategy gets its own isolated storage instance.
|
|
5705
|
+
*/
|
|
5706
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
5707
|
+
/**
|
|
5708
|
+
* Processes tick events and accumulates all event types.
|
|
5709
|
+
* Should be called from IStrategyCallbacks.onTick.
|
|
5710
|
+
*
|
|
5711
|
+
* Processes all event types: idle, opened, active, closed.
|
|
5712
|
+
*
|
|
5713
|
+
* @param data - Tick result from strategy execution
|
|
5714
|
+
*
|
|
5715
|
+
* @example
|
|
5716
|
+
* ```typescript
|
|
5717
|
+
* const service = new LiveMarkdownService();
|
|
5718
|
+
*
|
|
5719
|
+
* callbacks: {
|
|
5720
|
+
* onTick: (symbol, result, backtest) => {
|
|
5721
|
+
* if (!backtest) {
|
|
5722
|
+
* service.tick(result);
|
|
5723
|
+
* }
|
|
5724
|
+
* }
|
|
5725
|
+
* }
|
|
5726
|
+
* ```
|
|
5727
|
+
*/
|
|
5728
|
+
this.tick = async (data) => {
|
|
5729
|
+
this.loggerService.log("liveMarkdownService tick", {
|
|
5730
|
+
data,
|
|
5731
|
+
});
|
|
5732
|
+
const storage = this.getStorage(data.strategyName);
|
|
5733
|
+
if (data.action === "idle") {
|
|
5734
|
+
storage.addIdleEvent(data.currentPrice);
|
|
5735
|
+
}
|
|
5736
|
+
else if (data.action === "opened") {
|
|
5737
|
+
storage.addOpenedEvent(data);
|
|
5738
|
+
}
|
|
5739
|
+
else if (data.action === "active") {
|
|
5740
|
+
storage.addActiveEvent(data);
|
|
5741
|
+
}
|
|
5742
|
+
else if (data.action === "closed") {
|
|
5743
|
+
storage.addClosedEvent(data);
|
|
5744
|
+
}
|
|
5745
|
+
};
|
|
5746
|
+
/**
|
|
5747
|
+
* Gets statistical data from all live trading events for a strategy.
|
|
5748
|
+
* Delegates to ReportStorage.getData().
|
|
5749
|
+
*
|
|
5750
|
+
* @param strategyName - Strategy name to get data for
|
|
5751
|
+
* @returns Statistical data object with all metrics
|
|
5752
|
+
*
|
|
5753
|
+
* @example
|
|
5754
|
+
* ```typescript
|
|
5755
|
+
* const service = new LiveMarkdownService();
|
|
5756
|
+
* const stats = await service.getData("my-strategy");
|
|
5757
|
+
* console.log(stats.sharpeRatio, stats.winRate);
|
|
5758
|
+
* ```
|
|
5759
|
+
*/
|
|
5760
|
+
this.getData = async (strategyName) => {
|
|
5761
|
+
this.loggerService.log("liveMarkdownService getData", {
|
|
5762
|
+
strategyName,
|
|
5763
|
+
});
|
|
5764
|
+
const storage = this.getStorage(strategyName);
|
|
5765
|
+
return storage.getData();
|
|
5766
|
+
};
|
|
5767
|
+
/**
|
|
5768
|
+
* Generates markdown report with all events for a strategy.
|
|
5769
|
+
* Delegates to ReportStorage.getReport().
|
|
5770
|
+
*
|
|
5771
|
+
* @param strategyName - Strategy name to generate report for
|
|
5772
|
+
* @returns Markdown formatted report string with table of all events
|
|
5773
|
+
*
|
|
5774
|
+
* @example
|
|
5775
|
+
* ```typescript
|
|
5776
|
+
* const service = new LiveMarkdownService();
|
|
5777
|
+
* const markdown = await service.getReport("my-strategy");
|
|
5778
|
+
* console.log(markdown);
|
|
5779
|
+
* ```
|
|
5780
|
+
*/
|
|
5781
|
+
this.getReport = async (strategyName) => {
|
|
5782
|
+
this.loggerService.log("liveMarkdownService getReport", {
|
|
5783
|
+
strategyName,
|
|
5784
|
+
});
|
|
5785
|
+
const storage = this.getStorage(strategyName);
|
|
5786
|
+
return storage.getReport(strategyName);
|
|
5787
|
+
};
|
|
5788
|
+
/**
|
|
5789
|
+
* Saves strategy report to disk.
|
|
5790
|
+
* Creates directory if it doesn't exist.
|
|
5791
|
+
* Delegates to ReportStorage.dump().
|
|
5792
|
+
*
|
|
5793
|
+
* @param strategyName - Strategy name to save report for
|
|
5794
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5795
|
+
*
|
|
5796
|
+
* @example
|
|
5797
|
+
* ```typescript
|
|
5798
|
+
* const service = new LiveMarkdownService();
|
|
5799
|
+
*
|
|
5800
|
+
* // Save to default path: ./logs/live/my-strategy.md
|
|
5801
|
+
* await service.dump("my-strategy");
|
|
5802
|
+
*
|
|
5803
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5804
|
+
* await service.dump("my-strategy", "./custom/path");
|
|
5805
|
+
* ```
|
|
5806
|
+
*/
|
|
5807
|
+
this.dump = async (strategyName, path = "./logs/live") => {
|
|
5808
|
+
this.loggerService.log("liveMarkdownService dump", {
|
|
5809
|
+
strategyName,
|
|
5810
|
+
path,
|
|
5811
|
+
});
|
|
5812
|
+
const storage = this.getStorage(strategyName);
|
|
5813
|
+
await storage.dump(strategyName, path);
|
|
5814
|
+
};
|
|
5815
|
+
/**
|
|
5816
|
+
* Clears accumulated event data from storage.
|
|
5817
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
5818
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
5819
|
+
*
|
|
5820
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
5821
|
+
*
|
|
5822
|
+
* @example
|
|
5823
|
+
* ```typescript
|
|
5824
|
+
* const service = new LiveMarkdownService();
|
|
5825
|
+
*
|
|
5826
|
+
* // Clear specific strategy data
|
|
5827
|
+
* await service.clear("my-strategy");
|
|
5828
|
+
*
|
|
5829
|
+
* // Clear all strategies' data
|
|
5830
|
+
* await service.clear();
|
|
5831
|
+
* ```
|
|
5832
|
+
*/
|
|
5833
|
+
this.clear = async (strategyName) => {
|
|
5834
|
+
this.loggerService.log("liveMarkdownService clear", {
|
|
5835
|
+
strategyName,
|
|
5836
|
+
});
|
|
5837
|
+
this.getStorage.clear(strategyName);
|
|
5838
|
+
};
|
|
5839
|
+
/**
|
|
5840
|
+
* Initializes the service by subscribing to live signal events.
|
|
5841
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
5842
|
+
* Automatically called on first use.
|
|
5843
|
+
*
|
|
5844
|
+
* @example
|
|
5845
|
+
* ```typescript
|
|
5846
|
+
* const service = new LiveMarkdownService();
|
|
5847
|
+
* await service.init(); // Subscribe to live events
|
|
5848
|
+
* ```
|
|
5849
|
+
*/
|
|
5850
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
5851
|
+
this.loggerService.log("liveMarkdownService init");
|
|
5852
|
+
signalLiveEmitter.subscribe(this.tick);
|
|
5853
|
+
});
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
const columns$1 = [
|
|
5858
|
+
{
|
|
5859
|
+
key: "timestamp",
|
|
5860
|
+
label: "Timestamp",
|
|
5861
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
5862
|
+
},
|
|
5863
|
+
{
|
|
5864
|
+
key: "action",
|
|
5865
|
+
label: "Action",
|
|
5866
|
+
format: (data) => data.action.toUpperCase(),
|
|
5867
|
+
},
|
|
5868
|
+
{
|
|
5869
|
+
key: "symbol",
|
|
5870
|
+
label: "Symbol",
|
|
5871
|
+
format: (data) => data.symbol,
|
|
5872
|
+
},
|
|
5873
|
+
{
|
|
5874
|
+
key: "signalId",
|
|
5875
|
+
label: "Signal ID",
|
|
5876
|
+
format: (data) => data.signalId,
|
|
5877
|
+
},
|
|
5878
|
+
{
|
|
5879
|
+
key: "position",
|
|
5880
|
+
label: "Position",
|
|
5881
|
+
format: (data) => data.position.toUpperCase(),
|
|
5882
|
+
},
|
|
5883
|
+
{
|
|
5884
|
+
key: "note",
|
|
5885
|
+
label: "Note",
|
|
5886
|
+
format: (data) => data.note ?? "N/A",
|
|
5887
|
+
},
|
|
5888
|
+
{
|
|
5889
|
+
key: "currentPrice",
|
|
5890
|
+
label: "Current Price",
|
|
5891
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
5892
|
+
},
|
|
5893
|
+
{
|
|
5894
|
+
key: "priceOpen",
|
|
5895
|
+
label: "Entry Price",
|
|
5896
|
+
format: (data) => `${data.priceOpen.toFixed(8)} USD`,
|
|
5897
|
+
},
|
|
5898
|
+
{
|
|
5899
|
+
key: "takeProfit",
|
|
5900
|
+
label: "Take Profit",
|
|
5901
|
+
format: (data) => `${data.takeProfit.toFixed(8)} USD`,
|
|
5902
|
+
},
|
|
5903
|
+
{
|
|
5904
|
+
key: "stopLoss",
|
|
5905
|
+
label: "Stop Loss",
|
|
5906
|
+
format: (data) => `${data.stopLoss.toFixed(8)} USD`,
|
|
5907
|
+
},
|
|
5908
|
+
{
|
|
5909
|
+
key: "duration",
|
|
5910
|
+
label: "Wait Time (min)",
|
|
5911
|
+
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
5912
|
+
},
|
|
5913
|
+
];
|
|
5914
|
+
/** Maximum number of events to store in schedule reports */
|
|
5915
|
+
const MAX_EVENTS$1 = 250;
|
|
5916
|
+
/**
|
|
5917
|
+
* Storage class for accumulating scheduled signal events per strategy.
|
|
5918
|
+
* Maintains a chronological list of scheduled and cancelled events.
|
|
5919
|
+
*/
|
|
5920
|
+
let ReportStorage$1 = class ReportStorage {
|
|
5921
|
+
constructor() {
|
|
5922
|
+
/** Internal list of all scheduled events for this strategy */
|
|
5923
|
+
this._eventList = [];
|
|
5924
|
+
}
|
|
5925
|
+
/**
|
|
5926
|
+
* Adds a scheduled event to the storage.
|
|
5927
|
+
*
|
|
5928
|
+
* @param data - Scheduled tick result
|
|
5929
|
+
*/
|
|
5930
|
+
addScheduledEvent(data) {
|
|
5931
|
+
this._eventList.push({
|
|
5932
|
+
timestamp: data.signal.scheduledAt,
|
|
5933
|
+
action: "scheduled",
|
|
5934
|
+
symbol: data.signal.symbol,
|
|
5935
|
+
signalId: data.signal.id,
|
|
5936
|
+
position: data.signal.position,
|
|
5937
|
+
note: data.signal.note,
|
|
5938
|
+
currentPrice: data.currentPrice,
|
|
5939
|
+
priceOpen: data.signal.priceOpen,
|
|
5940
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5941
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5942
|
+
});
|
|
5943
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5944
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
5945
|
+
this._eventList.shift();
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
/**
|
|
5949
|
+
* Updates or adds a cancelled event to the storage.
|
|
4600
5950
|
* Replaces the previous event with the same signalId.
|
|
4601
5951
|
*
|
|
4602
|
-
* @param data -
|
|
5952
|
+
* @param data - Cancelled tick result
|
|
4603
5953
|
*/
|
|
4604
|
-
|
|
4605
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
5954
|
+
addCancelledEvent(data) {
|
|
5955
|
+
const durationMs = data.closeTimestamp - data.signal.scheduledAt;
|
|
4606
5956
|
const durationMin = Math.round(durationMs / 60000);
|
|
4607
5957
|
// Find existing event with the same signalId
|
|
4608
5958
|
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
4609
5959
|
const newEvent = {
|
|
4610
5960
|
timestamp: data.closeTimestamp,
|
|
4611
|
-
action: "
|
|
5961
|
+
action: "cancelled",
|
|
4612
5962
|
symbol: data.signal.symbol,
|
|
4613
5963
|
signalId: data.signal.id,
|
|
4614
5964
|
position: data.signal.position,
|
|
4615
5965
|
note: data.signal.note,
|
|
4616
5966
|
currentPrice: data.currentPrice,
|
|
4617
|
-
|
|
5967
|
+
priceOpen: data.signal.priceOpen,
|
|
4618
5968
|
takeProfit: data.signal.priceTakeProfit,
|
|
4619
5969
|
stopLoss: data.signal.priceStopLoss,
|
|
4620
|
-
|
|
4621
|
-
closeReason: data.closeReason,
|
|
5970
|
+
closeTimestamp: data.closeTimestamp,
|
|
4622
5971
|
duration: durationMin,
|
|
4623
5972
|
};
|
|
4624
5973
|
// Replace existing event or add new one
|
|
@@ -4634,8 +5983,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4634
5983
|
}
|
|
4635
5984
|
}
|
|
4636
5985
|
/**
|
|
4637
|
-
* Calculates statistical data from
|
|
4638
|
-
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5986
|
+
* Calculates statistical data from scheduled signal events (Controller).
|
|
4639
5987
|
*
|
|
4640
5988
|
* @returns Statistical data (empty object if no events)
|
|
4641
5989
|
*/
|
|
@@ -4644,78 +5992,34 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4644
5992
|
return {
|
|
4645
5993
|
eventList: [],
|
|
4646
5994
|
totalEvents: 0,
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
avgPnl: null,
|
|
4652
|
-
totalPnl: null,
|
|
4653
|
-
stdDev: null,
|
|
4654
|
-
sharpeRatio: null,
|
|
4655
|
-
annualizedSharpeRatio: null,
|
|
4656
|
-
certaintyRatio: null,
|
|
4657
|
-
expectedYearlyReturns: null,
|
|
5995
|
+
totalScheduled: 0,
|
|
5996
|
+
totalCancelled: 0,
|
|
5997
|
+
cancellationRate: null,
|
|
5998
|
+
avgWaitTime: null,
|
|
4658
5999
|
};
|
|
4659
6000
|
}
|
|
4660
|
-
const
|
|
4661
|
-
const
|
|
4662
|
-
const
|
|
4663
|
-
const
|
|
4664
|
-
// Calculate
|
|
4665
|
-
const
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
let sharpeRatio = 0;
|
|
4672
|
-
let stdDev = 0;
|
|
4673
|
-
if (totalClosed > 0) {
|
|
4674
|
-
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
4675
|
-
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
4676
|
-
stdDev = Math.sqrt(variance);
|
|
4677
|
-
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
4678
|
-
}
|
|
4679
|
-
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
4680
|
-
// Calculate Certainty Ratio
|
|
4681
|
-
let certaintyRatio = 0;
|
|
4682
|
-
if (totalClosed > 0) {
|
|
4683
|
-
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
4684
|
-
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
4685
|
-
const avgWin = wins.length > 0
|
|
4686
|
-
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
4687
|
-
: 0;
|
|
4688
|
-
const avgLoss = losses.length > 0
|
|
4689
|
-
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
4690
|
-
: 0;
|
|
4691
|
-
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4692
|
-
}
|
|
4693
|
-
// Calculate Expected Yearly Returns
|
|
4694
|
-
let expectedYearlyReturns = 0;
|
|
4695
|
-
if (totalClosed > 0) {
|
|
4696
|
-
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
4697
|
-
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
4698
|
-
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4699
|
-
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
4700
|
-
}
|
|
6001
|
+
const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
|
|
6002
|
+
const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
|
|
6003
|
+
const totalScheduled = scheduledEvents.length;
|
|
6004
|
+
const totalCancelled = cancelledEvents.length;
|
|
6005
|
+
// Calculate cancellation rate
|
|
6006
|
+
const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
|
|
6007
|
+
// Calculate average wait time for cancelled signals
|
|
6008
|
+
const avgWaitTime = totalCancelled > 0
|
|
6009
|
+
? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
6010
|
+
totalCancelled
|
|
6011
|
+
: null;
|
|
4701
6012
|
return {
|
|
4702
6013
|
eventList: this._eventList,
|
|
4703
6014
|
totalEvents: this._eventList.length,
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
4709
|
-
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
4710
|
-
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
4711
|
-
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
4712
|
-
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
4713
|
-
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
4714
|
-
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
6015
|
+
totalScheduled,
|
|
6016
|
+
totalCancelled,
|
|
6017
|
+
cancellationRate,
|
|
6018
|
+
avgWaitTime,
|
|
4715
6019
|
};
|
|
4716
6020
|
}
|
|
4717
6021
|
/**
|
|
4718
|
-
* Generates markdown report with all
|
|
6022
|
+
* Generates markdown report with all scheduled events for a strategy (View).
|
|
4719
6023
|
*
|
|
4720
6024
|
* @param strategyName - Strategy name
|
|
4721
6025
|
* @returns Markdown formatted report with all events
|
|
@@ -4723,22 +6027,22 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4723
6027
|
async getReport(strategyName) {
|
|
4724
6028
|
const stats = await this.getData();
|
|
4725
6029
|
if (stats.totalEvents === 0) {
|
|
4726
|
-
return functoolsKit.str.newline(`#
|
|
6030
|
+
return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
|
|
4727
6031
|
}
|
|
4728
6032
|
const header = columns$1.map((col) => col.label);
|
|
4729
6033
|
const separator = columns$1.map(() => "---");
|
|
4730
6034
|
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
4731
6035
|
const tableData = [header, separator, ...rows];
|
|
4732
|
-
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4733
|
-
return functoolsKit.str.newline(`#
|
|
6036
|
+
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
6037
|
+
return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
|
|
4734
6038
|
}
|
|
4735
6039
|
/**
|
|
4736
6040
|
* Saves strategy report to disk.
|
|
4737
6041
|
*
|
|
4738
6042
|
* @param strategyName - Strategy name
|
|
4739
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
6043
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4740
6044
|
*/
|
|
4741
|
-
async dump(strategyName, path$1 = "./logs/
|
|
6045
|
+
async dump(strategyName, path$1 = "./logs/schedule") {
|
|
4742
6046
|
const markdown = await this.getReport(strategyName);
|
|
4743
6047
|
try {
|
|
4744
6048
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -4746,7 +6050,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4746
6050
|
const filename = `${strategyName}.md`;
|
|
4747
6051
|
const filepath = path.join(dir, filename);
|
|
4748
6052
|
await fs.writeFile(filepath, markdown, "utf-8");
|
|
4749
|
-
console.log(`
|
|
6053
|
+
console.log(`Scheduled signals report saved: ${filepath}`);
|
|
4750
6054
|
}
|
|
4751
6055
|
catch (error) {
|
|
4752
6056
|
console.error(`Failed to save markdown report:`, error);
|
|
@@ -4754,36 +6058,27 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4754
6058
|
}
|
|
4755
6059
|
};
|
|
4756
6060
|
/**
|
|
4757
|
-
* Service for generating and saving
|
|
6061
|
+
* Service for generating and saving scheduled signals markdown reports.
|
|
4758
6062
|
*
|
|
4759
6063
|
* Features:
|
|
4760
|
-
* - Listens to
|
|
4761
|
-
* - Accumulates all events (
|
|
6064
|
+
* - Listens to scheduled and cancelled signal events via signalLiveEmitter
|
|
6065
|
+
* - Accumulates all events (scheduled, cancelled) per strategy
|
|
4762
6066
|
* - Generates markdown tables with detailed event information
|
|
4763
|
-
* - Provides
|
|
4764
|
-
* - Saves reports to disk in logs/
|
|
6067
|
+
* - Provides statistics (cancellation rate, average wait time)
|
|
6068
|
+
* - Saves reports to disk in logs/schedule/{strategyName}.md
|
|
4765
6069
|
*
|
|
4766
6070
|
* @example
|
|
4767
6071
|
* ```typescript
|
|
4768
|
-
* const service = new
|
|
6072
|
+
* const service = new ScheduleMarkdownService();
|
|
4769
6073
|
*
|
|
4770
|
-
* //
|
|
4771
|
-
*
|
|
4772
|
-
* strategyName: "my-strategy",
|
|
4773
|
-
* callbacks: {
|
|
4774
|
-
* onTick: (symbol, result, backtest) => {
|
|
4775
|
-
* if (!backtest) {
|
|
4776
|
-
* service.tick(result);
|
|
4777
|
-
* }
|
|
4778
|
-
* }
|
|
4779
|
-
* }
|
|
4780
|
-
* });
|
|
6074
|
+
* // Service automatically subscribes to signalLiveEmitter on init
|
|
6075
|
+
* // No manual callback setup needed
|
|
4781
6076
|
*
|
|
4782
6077
|
* // Later: generate and save report
|
|
4783
6078
|
* await service.dump("my-strategy");
|
|
4784
6079
|
* ```
|
|
4785
6080
|
*/
|
|
4786
|
-
class
|
|
6081
|
+
class ScheduleMarkdownService {
|
|
4787
6082
|
constructor() {
|
|
4788
6083
|
/** Logger service for debug output */
|
|
4789
6084
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -4793,46 +6088,33 @@ class LiveMarkdownService {
|
|
|
4793
6088
|
*/
|
|
4794
6089
|
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
|
|
4795
6090
|
/**
|
|
4796
|
-
* Processes tick events and accumulates
|
|
4797
|
-
* Should be called from
|
|
6091
|
+
* Processes tick events and accumulates scheduled/cancelled events.
|
|
6092
|
+
* Should be called from signalLiveEmitter subscription.
|
|
4798
6093
|
*
|
|
4799
|
-
* Processes
|
|
6094
|
+
* Processes only scheduled and cancelled event types.
|
|
4800
6095
|
*
|
|
4801
6096
|
* @param data - Tick result from strategy execution
|
|
4802
6097
|
*
|
|
4803
6098
|
* @example
|
|
4804
6099
|
* ```typescript
|
|
4805
|
-
* const service = new
|
|
4806
|
-
*
|
|
4807
|
-
* callbacks: {
|
|
4808
|
-
* onTick: (symbol, result, backtest) => {
|
|
4809
|
-
* if (!backtest) {
|
|
4810
|
-
* service.tick(result);
|
|
4811
|
-
* }
|
|
4812
|
-
* }
|
|
4813
|
-
* }
|
|
6100
|
+
* const service = new ScheduleMarkdownService();
|
|
6101
|
+
* // Service automatically subscribes in init()
|
|
4814
6102
|
* ```
|
|
4815
6103
|
*/
|
|
4816
6104
|
this.tick = async (data) => {
|
|
4817
|
-
this.loggerService.log("
|
|
6105
|
+
this.loggerService.log("scheduleMarkdownService tick", {
|
|
4818
6106
|
data,
|
|
4819
6107
|
});
|
|
4820
6108
|
const storage = this.getStorage(data.strategyName);
|
|
4821
|
-
if (data.action === "
|
|
4822
|
-
storage.
|
|
4823
|
-
}
|
|
4824
|
-
else if (data.action === "opened") {
|
|
4825
|
-
storage.addOpenedEvent(data);
|
|
4826
|
-
}
|
|
4827
|
-
else if (data.action === "active") {
|
|
4828
|
-
storage.addActiveEvent(data);
|
|
6109
|
+
if (data.action === "scheduled") {
|
|
6110
|
+
storage.addScheduledEvent(data);
|
|
4829
6111
|
}
|
|
4830
|
-
else if (data.action === "
|
|
4831
|
-
storage.
|
|
6112
|
+
else if (data.action === "cancelled") {
|
|
6113
|
+
storage.addCancelledEvent(data);
|
|
4832
6114
|
}
|
|
4833
6115
|
};
|
|
4834
6116
|
/**
|
|
4835
|
-
* Gets statistical data from all
|
|
6117
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
4836
6118
|
* Delegates to ReportStorage.getData().
|
|
4837
6119
|
*
|
|
4838
6120
|
* @param strategyName - Strategy name to get data for
|
|
@@ -4840,20 +6122,20 @@ class LiveMarkdownService {
|
|
|
4840
6122
|
*
|
|
4841
6123
|
* @example
|
|
4842
6124
|
* ```typescript
|
|
4843
|
-
* const service = new
|
|
6125
|
+
* const service = new ScheduleMarkdownService();
|
|
4844
6126
|
* const stats = await service.getData("my-strategy");
|
|
4845
|
-
* console.log(stats.
|
|
6127
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
4846
6128
|
* ```
|
|
4847
6129
|
*/
|
|
4848
6130
|
this.getData = async (strategyName) => {
|
|
4849
|
-
this.loggerService.log("
|
|
6131
|
+
this.loggerService.log("scheduleMarkdownService getData", {
|
|
4850
6132
|
strategyName,
|
|
4851
6133
|
});
|
|
4852
6134
|
const storage = this.getStorage(strategyName);
|
|
4853
6135
|
return storage.getData();
|
|
4854
6136
|
};
|
|
4855
6137
|
/**
|
|
4856
|
-
* Generates markdown report with all events for a strategy.
|
|
6138
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
4857
6139
|
* Delegates to ReportStorage.getReport().
|
|
4858
6140
|
*
|
|
4859
6141
|
* @param strategyName - Strategy name to generate report for
|
|
@@ -4861,13 +6143,13 @@ class LiveMarkdownService {
|
|
|
4861
6143
|
*
|
|
4862
6144
|
* @example
|
|
4863
6145
|
* ```typescript
|
|
4864
|
-
* const service = new
|
|
6146
|
+
* const service = new ScheduleMarkdownService();
|
|
4865
6147
|
* const markdown = await service.getReport("my-strategy");
|
|
4866
6148
|
* console.log(markdown);
|
|
4867
6149
|
* ```
|
|
4868
6150
|
*/
|
|
4869
6151
|
this.getReport = async (strategyName) => {
|
|
4870
|
-
this.loggerService.log("
|
|
6152
|
+
this.loggerService.log("scheduleMarkdownService getReport", {
|
|
4871
6153
|
strategyName,
|
|
4872
6154
|
});
|
|
4873
6155
|
const storage = this.getStorage(strategyName);
|
|
@@ -4879,21 +6161,21 @@ class LiveMarkdownService {
|
|
|
4879
6161
|
* Delegates to ReportStorage.dump().
|
|
4880
6162
|
*
|
|
4881
6163
|
* @param strategyName - Strategy name to save report for
|
|
4882
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
6164
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4883
6165
|
*
|
|
4884
6166
|
* @example
|
|
4885
6167
|
* ```typescript
|
|
4886
|
-
* const service = new
|
|
6168
|
+
* const service = new ScheduleMarkdownService();
|
|
4887
6169
|
*
|
|
4888
|
-
* // Save to default path: ./logs/
|
|
6170
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
4889
6171
|
* await service.dump("my-strategy");
|
|
4890
6172
|
*
|
|
4891
6173
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
4892
6174
|
* await service.dump("my-strategy", "./custom/path");
|
|
4893
6175
|
* ```
|
|
4894
6176
|
*/
|
|
4895
|
-
this.dump = async (strategyName, path = "./logs/
|
|
4896
|
-
this.loggerService.log("
|
|
6177
|
+
this.dump = async (strategyName, path = "./logs/schedule") => {
|
|
6178
|
+
this.loggerService.log("scheduleMarkdownService dump", {
|
|
4897
6179
|
strategyName,
|
|
4898
6180
|
path,
|
|
4899
6181
|
});
|
|
@@ -4909,7 +6191,7 @@ class LiveMarkdownService {
|
|
|
4909
6191
|
*
|
|
4910
6192
|
* @example
|
|
4911
6193
|
* ```typescript
|
|
4912
|
-
* const service = new
|
|
6194
|
+
* const service = new ScheduleMarkdownService();
|
|
4913
6195
|
*
|
|
4914
6196
|
* // Clear specific strategy data
|
|
4915
6197
|
* await service.clear("my-strategy");
|
|
@@ -4919,7 +6201,7 @@ class LiveMarkdownService {
|
|
|
4919
6201
|
* ```
|
|
4920
6202
|
*/
|
|
4921
6203
|
this.clear = async (strategyName) => {
|
|
4922
|
-
this.loggerService.log("
|
|
6204
|
+
this.loggerService.log("scheduleMarkdownService clear", {
|
|
4923
6205
|
strategyName,
|
|
4924
6206
|
});
|
|
4925
6207
|
this.getStorage.clear(strategyName);
|
|
@@ -4931,13 +6213,13 @@ class LiveMarkdownService {
|
|
|
4931
6213
|
*
|
|
4932
6214
|
* @example
|
|
4933
6215
|
* ```typescript
|
|
4934
|
-
* const service = new
|
|
6216
|
+
* const service = new ScheduleMarkdownService();
|
|
4935
6217
|
* await service.init(); // Subscribe to live events
|
|
4936
6218
|
* ```
|
|
4937
6219
|
*/
|
|
4938
6220
|
this.init = functoolsKit.singleshot(async () => {
|
|
4939
|
-
this.loggerService.log("
|
|
4940
|
-
|
|
6221
|
+
this.loggerService.log("scheduleMarkdownService init");
|
|
6222
|
+
signalEmitter.subscribe(this.tick);
|
|
4941
6223
|
});
|
|
4942
6224
|
}
|
|
4943
6225
|
}
|
|
@@ -6501,6 +7783,7 @@ class RiskValidationService {
|
|
|
6501
7783
|
{
|
|
6502
7784
|
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
|
|
6503
7785
|
provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
|
|
7786
|
+
provide(TYPES.scheduleMarkdownService, () => new ScheduleMarkdownService());
|
|
6504
7787
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
6505
7788
|
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
6506
7789
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
@@ -6559,6 +7842,7 @@ const logicPublicServices = {
|
|
|
6559
7842
|
const markdownServices = {
|
|
6560
7843
|
backtestMarkdownService: inject(TYPES.backtestMarkdownService),
|
|
6561
7844
|
liveMarkdownService: inject(TYPES.liveMarkdownService),
|
|
7845
|
+
scheduleMarkdownService: inject(TYPES.scheduleMarkdownService),
|
|
6562
7846
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
6563
7847
|
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
6564
7848
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
@@ -6605,6 +7889,20 @@ var backtest$1 = backtest;
|
|
|
6605
7889
|
async function setLogger(logger) {
|
|
6606
7890
|
backtest$1.loggerService.setLogger(logger);
|
|
6607
7891
|
}
|
|
7892
|
+
/**
|
|
7893
|
+
* Sets global configuration parameters for the framework.
|
|
7894
|
+
* @param config - Partial configuration object to override default settings
|
|
7895
|
+
*
|
|
7896
|
+
* @example
|
|
7897
|
+
* ```typescript
|
|
7898
|
+
* setConfig({
|
|
7899
|
+
* CC_SCHEDULE_AWAIT_MINUTES: 90,
|
|
7900
|
+
* });
|
|
7901
|
+
* ```
|
|
7902
|
+
*/
|
|
7903
|
+
async function setConfig(config) {
|
|
7904
|
+
Object.assign(GLOBAL_CONFIG, config);
|
|
7905
|
+
}
|
|
6608
7906
|
|
|
6609
7907
|
const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
|
|
6610
7908
|
const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
|
|
@@ -7887,8 +9185,17 @@ class BacktestUtils {
|
|
|
7887
9185
|
symbol,
|
|
7888
9186
|
context,
|
|
7889
9187
|
});
|
|
7890
|
-
|
|
7891
|
-
|
|
9188
|
+
{
|
|
9189
|
+
backtest$1.backtestMarkdownService.clear(context.strategyName);
|
|
9190
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9191
|
+
}
|
|
9192
|
+
{
|
|
9193
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9194
|
+
}
|
|
9195
|
+
{
|
|
9196
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9197
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9198
|
+
}
|
|
7892
9199
|
return backtest$1.backtestGlobalService.run(symbol, context);
|
|
7893
9200
|
};
|
|
7894
9201
|
/**
|
|
@@ -8067,8 +9374,17 @@ class LiveUtils {
|
|
|
8067
9374
|
symbol,
|
|
8068
9375
|
context,
|
|
8069
9376
|
});
|
|
8070
|
-
|
|
8071
|
-
|
|
9377
|
+
{
|
|
9378
|
+
backtest$1.liveMarkdownService.clear(context.strategyName);
|
|
9379
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9380
|
+
}
|
|
9381
|
+
{
|
|
9382
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9383
|
+
}
|
|
9384
|
+
{
|
|
9385
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9386
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9387
|
+
}
|
|
8072
9388
|
return backtest$1.liveGlobalService.run(symbol, context);
|
|
8073
9389
|
};
|
|
8074
9390
|
/**
|
|
@@ -8194,6 +9510,132 @@ class LiveUtils {
|
|
|
8194
9510
|
*/
|
|
8195
9511
|
const Live = new LiveUtils();
|
|
8196
9512
|
|
|
9513
|
+
const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
|
|
9514
|
+
const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
|
|
9515
|
+
const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
|
|
9516
|
+
const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
|
|
9517
|
+
/**
|
|
9518
|
+
* Utility class for scheduled signals reporting operations.
|
|
9519
|
+
*
|
|
9520
|
+
* Provides simplified access to scheduleMarkdownService with logging.
|
|
9521
|
+
* Exported as singleton instance for convenient usage.
|
|
9522
|
+
*
|
|
9523
|
+
* Features:
|
|
9524
|
+
* - Track scheduled signals in queue
|
|
9525
|
+
* - Track cancelled signals
|
|
9526
|
+
* - Calculate cancellation rate and average wait time
|
|
9527
|
+
* - Generate markdown reports
|
|
9528
|
+
*
|
|
9529
|
+
* @example
|
|
9530
|
+
* ```typescript
|
|
9531
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9532
|
+
*
|
|
9533
|
+
* // Get scheduled signals statistics
|
|
9534
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9535
|
+
* console.log(`Cancellation rate: ${stats.cancellationRate}%`);
|
|
9536
|
+
* console.log(`Average wait time: ${stats.avgWaitTime} minutes`);
|
|
9537
|
+
*
|
|
9538
|
+
* // Generate and save report
|
|
9539
|
+
* await Schedule.dump("my-strategy");
|
|
9540
|
+
* ```
|
|
9541
|
+
*/
|
|
9542
|
+
class ScheduleUtils {
|
|
9543
|
+
constructor() {
|
|
9544
|
+
/**
|
|
9545
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
9546
|
+
*
|
|
9547
|
+
* @param strategyName - Strategy name to get data for
|
|
9548
|
+
* @returns Promise resolving to statistical data object
|
|
9549
|
+
*
|
|
9550
|
+
* @example
|
|
9551
|
+
* ```typescript
|
|
9552
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9553
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
9554
|
+
* ```
|
|
9555
|
+
*/
|
|
9556
|
+
this.getData = async (strategyName) => {
|
|
9557
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_DATA, {
|
|
9558
|
+
strategyName,
|
|
9559
|
+
});
|
|
9560
|
+
return await backtest$1.scheduleMarkdownService.getData(strategyName);
|
|
9561
|
+
};
|
|
9562
|
+
/**
|
|
9563
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
9564
|
+
*
|
|
9565
|
+
* @param strategyName - Strategy name to generate report for
|
|
9566
|
+
* @returns Promise resolving to markdown formatted report string
|
|
9567
|
+
*
|
|
9568
|
+
* @example
|
|
9569
|
+
* ```typescript
|
|
9570
|
+
* const markdown = await Schedule.getReport("my-strategy");
|
|
9571
|
+
* console.log(markdown);
|
|
9572
|
+
* ```
|
|
9573
|
+
*/
|
|
9574
|
+
this.getReport = async (strategyName) => {
|
|
9575
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_REPORT, {
|
|
9576
|
+
strategyName,
|
|
9577
|
+
});
|
|
9578
|
+
return await backtest$1.scheduleMarkdownService.getReport(strategyName);
|
|
9579
|
+
};
|
|
9580
|
+
/**
|
|
9581
|
+
* Saves strategy report to disk.
|
|
9582
|
+
*
|
|
9583
|
+
* @param strategyName - Strategy name to save report for
|
|
9584
|
+
* @param path - Optional directory path to save report (default: "./logs/schedule")
|
|
9585
|
+
*
|
|
9586
|
+
* @example
|
|
9587
|
+
* ```typescript
|
|
9588
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
9589
|
+
* await Schedule.dump("my-strategy");
|
|
9590
|
+
*
|
|
9591
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
9592
|
+
* await Schedule.dump("my-strategy", "./custom/path");
|
|
9593
|
+
* ```
|
|
9594
|
+
*/
|
|
9595
|
+
this.dump = async (strategyName, path) => {
|
|
9596
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_DUMP, {
|
|
9597
|
+
strategyName,
|
|
9598
|
+
path,
|
|
9599
|
+
});
|
|
9600
|
+
await backtest$1.scheduleMarkdownService.dump(strategyName, path);
|
|
9601
|
+
};
|
|
9602
|
+
/**
|
|
9603
|
+
* Clears accumulated scheduled signal data from storage.
|
|
9604
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
9605
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
9606
|
+
*
|
|
9607
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
9608
|
+
*
|
|
9609
|
+
* @example
|
|
9610
|
+
* ```typescript
|
|
9611
|
+
* // Clear specific strategy data
|
|
9612
|
+
* await Schedule.clear("my-strategy");
|
|
9613
|
+
*
|
|
9614
|
+
* // Clear all strategies' data
|
|
9615
|
+
* await Schedule.clear();
|
|
9616
|
+
* ```
|
|
9617
|
+
*/
|
|
9618
|
+
this.clear = async (strategyName) => {
|
|
9619
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
|
|
9620
|
+
strategyName,
|
|
9621
|
+
});
|
|
9622
|
+
await backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9623
|
+
};
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
/**
|
|
9627
|
+
* Singleton instance of ScheduleUtils for convenient scheduled signals reporting.
|
|
9628
|
+
*
|
|
9629
|
+
* @example
|
|
9630
|
+
* ```typescript
|
|
9631
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9632
|
+
*
|
|
9633
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9634
|
+
* console.log("Cancellation rate:", stats.cancellationRate);
|
|
9635
|
+
* ```
|
|
9636
|
+
*/
|
|
9637
|
+
const Schedule = new ScheduleUtils();
|
|
9638
|
+
|
|
8197
9639
|
/**
|
|
8198
9640
|
* Performance class provides static methods for performance metrics analysis.
|
|
8199
9641
|
*
|
|
@@ -8369,8 +9811,17 @@ class WalkerUtils {
|
|
|
8369
9811
|
backtest$1.walkerMarkdownService.clear(context.walkerName);
|
|
8370
9812
|
// Clear backtest data for all strategies
|
|
8371
9813
|
for (const strategyName of walkerSchema.strategies) {
|
|
8372
|
-
|
|
8373
|
-
|
|
9814
|
+
{
|
|
9815
|
+
backtest$1.backtestMarkdownService.clear(strategyName);
|
|
9816
|
+
backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9817
|
+
}
|
|
9818
|
+
{
|
|
9819
|
+
backtest$1.strategyGlobalService.clear(strategyName);
|
|
9820
|
+
}
|
|
9821
|
+
{
|
|
9822
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
9823
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9824
|
+
}
|
|
8374
9825
|
}
|
|
8375
9826
|
return backtest$1.walkerGlobalService.run(symbol, {
|
|
8376
9827
|
walkerName: context.walkerName,
|
|
@@ -8420,6 +9871,9 @@ class WalkerUtils {
|
|
|
8420
9871
|
task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
8421
9872
|
return () => {
|
|
8422
9873
|
isStopped = true;
|
|
9874
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
9875
|
+
backtest$1.strategyGlobalService.stop(strategyName);
|
|
9876
|
+
}
|
|
8423
9877
|
};
|
|
8424
9878
|
};
|
|
8425
9879
|
/**
|
|
@@ -8783,8 +10237,9 @@ exports.MethodContextService = MethodContextService;
|
|
|
8783
10237
|
exports.Performance = Performance;
|
|
8784
10238
|
exports.PersistBase = PersistBase;
|
|
8785
10239
|
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
8786
|
-
exports.
|
|
10240
|
+
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
8787
10241
|
exports.PositionSize = PositionSize;
|
|
10242
|
+
exports.Schedule = Schedule;
|
|
8788
10243
|
exports.Walker = Walker;
|
|
8789
10244
|
exports.addExchange = addExchange;
|
|
8790
10245
|
exports.addFrame = addFrame;
|
|
@@ -8825,4 +10280,5 @@ exports.listenValidation = listenValidation;
|
|
|
8825
10280
|
exports.listenWalker = listenWalker;
|
|
8826
10281
|
exports.listenWalkerComplete = listenWalkerComplete;
|
|
8827
10282
|
exports.listenWalkerOnce = listenWalkerOnce;
|
|
10283
|
+
exports.setConfig = setConfig;
|
|
8828
10284
|
exports.setLogger = setLogger;
|