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