backtest-kit 1.1.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -32
- package/build/index.cjs +1759 -440
- package/build/index.mjs +1758 -441
- package/package.json +1 -1
- package/types.d.ts +485 -23
package/build/index.cjs
CHANGED
|
@@ -8,6 +8,37 @@ var path = require('path');
|
|
|
8
8
|
var crypto = require('crypto');
|
|
9
9
|
var os = require('os');
|
|
10
10
|
|
|
11
|
+
const GLOBAL_CONFIG = {
|
|
12
|
+
/**
|
|
13
|
+
* Time to wait for scheduled signal to activate (in minutes)
|
|
14
|
+
* If signal does not activate within this time, it will be cancelled.
|
|
15
|
+
*/
|
|
16
|
+
CC_SCHEDULE_AWAIT_MINUTES: 120,
|
|
17
|
+
/**
|
|
18
|
+
* Number of candles to use for average price calculation (VWAP)
|
|
19
|
+
* Default: 5 candles (last 5 minutes when using 1m interval)
|
|
20
|
+
*/
|
|
21
|
+
CC_AVG_PRICE_CANDLES_COUNT: 5,
|
|
22
|
+
/**
|
|
23
|
+
* Minimum TakeProfit distance from priceOpen (percentage)
|
|
24
|
+
* Must be greater than trading fees to ensure profitable trades
|
|
25
|
+
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
26
|
+
*/
|
|
27
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.1,
|
|
28
|
+
/**
|
|
29
|
+
* Maximum StopLoss distance from priceOpen (percentage)
|
|
30
|
+
* Prevents catastrophic losses from extreme StopLoss values
|
|
31
|
+
* Default: 20% (one signal cannot lose more than 20% of position)
|
|
32
|
+
*/
|
|
33
|
+
CC_MAX_STOPLOSS_DISTANCE_PERCENT: 20,
|
|
34
|
+
/**
|
|
35
|
+
* Maximum signal lifetime in minutes
|
|
36
|
+
* Prevents eternal signals that block risk limits for weeks/months
|
|
37
|
+
* Default: 1440 minutes (1 day)
|
|
38
|
+
*/
|
|
39
|
+
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
40
|
+
};
|
|
41
|
+
|
|
11
42
|
const { init, inject, provide } = diKit.createActivator("backtest");
|
|
12
43
|
|
|
13
44
|
/**
|
|
@@ -81,6 +112,7 @@ const logicPublicServices$1 = {
|
|
|
81
112
|
const markdownServices$1 = {
|
|
82
113
|
backtestMarkdownService: Symbol('backtestMarkdownService'),
|
|
83
114
|
liveMarkdownService: Symbol('liveMarkdownService'),
|
|
115
|
+
scheduleMarkdownService: Symbol('scheduleMarkdownService'),
|
|
84
116
|
performanceMarkdownService: Symbol('performanceMarkdownService'),
|
|
85
117
|
walkerMarkdownService: Symbol('walkerMarkdownService'),
|
|
86
118
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
@@ -340,7 +372,8 @@ class ClientExchange {
|
|
|
340
372
|
return filteredData;
|
|
341
373
|
}
|
|
342
374
|
/**
|
|
343
|
-
* Calculates VWAP (Volume Weighted Average Price) from last
|
|
375
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
376
|
+
* The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
|
|
344
377
|
*
|
|
345
378
|
* Formula:
|
|
346
379
|
* - Typical Price = (high + low + close) / 3
|
|
@@ -356,7 +389,7 @@ class ClientExchange {
|
|
|
356
389
|
this.params.logger.debug(`ClientExchange getAveragePrice`, {
|
|
357
390
|
symbol,
|
|
358
391
|
});
|
|
359
|
-
const candles = await this.getCandles(symbol, "1m",
|
|
392
|
+
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
360
393
|
if (candles.length === 0) {
|
|
361
394
|
throw new Error(`ClientExchange getAveragePrice: no candles data for symbol=${symbol}`);
|
|
362
395
|
}
|
|
@@ -1302,14 +1335,24 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1302
1335
|
};
|
|
1303
1336
|
const VALIDATE_SIGNAL_FN = (signal) => {
|
|
1304
1337
|
const errors = [];
|
|
1305
|
-
//
|
|
1306
|
-
if (signal.priceOpen
|
|
1338
|
+
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
1339
|
+
if (!isFinite(signal.priceOpen)) {
|
|
1340
|
+
errors.push(`priceOpen must be a finite number, got ${signal.priceOpen} (${typeof signal.priceOpen})`);
|
|
1341
|
+
}
|
|
1342
|
+
if (!isFinite(signal.priceTakeProfit)) {
|
|
1343
|
+
errors.push(`priceTakeProfit must be a finite number, got ${signal.priceTakeProfit} (${typeof signal.priceTakeProfit})`);
|
|
1344
|
+
}
|
|
1345
|
+
if (!isFinite(signal.priceStopLoss)) {
|
|
1346
|
+
errors.push(`priceStopLoss must be a finite number, got ${signal.priceStopLoss} (${typeof signal.priceStopLoss})`);
|
|
1347
|
+
}
|
|
1348
|
+
// Валидация цен (только если они конечные)
|
|
1349
|
+
if (isFinite(signal.priceOpen) && signal.priceOpen <= 0) {
|
|
1307
1350
|
errors.push(`priceOpen must be positive, got ${signal.priceOpen}`);
|
|
1308
1351
|
}
|
|
1309
|
-
if (signal.priceTakeProfit <= 0) {
|
|
1352
|
+
if (isFinite(signal.priceTakeProfit) && signal.priceTakeProfit <= 0) {
|
|
1310
1353
|
errors.push(`priceTakeProfit must be positive, got ${signal.priceTakeProfit}`);
|
|
1311
1354
|
}
|
|
1312
|
-
if (signal.priceStopLoss <= 0) {
|
|
1355
|
+
if (isFinite(signal.priceStopLoss) && signal.priceStopLoss <= 0) {
|
|
1313
1356
|
errors.push(`priceStopLoss must be positive, got ${signal.priceStopLoss}`);
|
|
1314
1357
|
}
|
|
1315
1358
|
// Валидация для long позиции
|
|
@@ -1320,6 +1363,24 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1320
1363
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1321
1364
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1322
1365
|
}
|
|
1366
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1367
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1368
|
+
const tpDistancePercent = ((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1369
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1370
|
+
errors.push(`Long: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1371
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1372
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1376
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1377
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1378
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1379
|
+
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1380
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1381
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1323
1384
|
}
|
|
1324
1385
|
// Валидация для short позиции
|
|
1325
1386
|
if (signal.position === "short") {
|
|
@@ -1329,13 +1390,44 @@ const VALIDATE_SIGNAL_FN = (signal) => {
|
|
|
1329
1390
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1330
1391
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1331
1392
|
}
|
|
1393
|
+
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
1394
|
+
if (GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1395
|
+
const tpDistancePercent = ((signal.priceOpen - signal.priceTakeProfit) / signal.priceOpen) * 100;
|
|
1396
|
+
if (tpDistancePercent < GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT) {
|
|
1397
|
+
errors.push(`Short: TakeProfit too close to priceOpen (${tpDistancePercent.toFixed(3)}%). ` +
|
|
1398
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_TAKEPROFIT_DISTANCE_PERCENT}% to cover trading fees. ` +
|
|
1399
|
+
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1403
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1404
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1405
|
+
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1406
|
+
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1407
|
+
`Maximum distance: ${GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT}% to protect capital. ` +
|
|
1408
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1332
1411
|
}
|
|
1333
1412
|
// Валидация временных параметров
|
|
1334
1413
|
if (signal.minuteEstimatedTime <= 0) {
|
|
1335
1414
|
errors.push(`minuteEstimatedTime must be positive, got ${signal.minuteEstimatedTime}`);
|
|
1336
1415
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1416
|
+
// ЗАЩИТА ОТ ВЕЧНЫХ СИГНАЛОВ: ограничиваем максимальное время жизни сигнала
|
|
1417
|
+
if (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES && GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1418
|
+
if (signal.minuteEstimatedTime > GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES) {
|
|
1419
|
+
const days = (signal.minuteEstimatedTime / 60 / 24).toFixed(1);
|
|
1420
|
+
const maxDays = (GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES / 60 / 24).toFixed(0);
|
|
1421
|
+
errors.push(`minuteEstimatedTime too large (${signal.minuteEstimatedTime} minutes = ${days} days). ` +
|
|
1422
|
+
`Maximum: ${GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES} minutes (${maxDays} days) to prevent strategy deadlock. ` +
|
|
1423
|
+
`Eternal signals block risk limits and prevent new trades.`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (signal.scheduledAt <= 0) {
|
|
1427
|
+
errors.push(`scheduledAt must be positive, got ${signal.scheduledAt}`);
|
|
1428
|
+
}
|
|
1429
|
+
if (signal.pendingAt <= 0) {
|
|
1430
|
+
errors.push(`pendingAt must be positive, got ${signal.pendingAt}`);
|
|
1339
1431
|
}
|
|
1340
1432
|
// Кидаем ошибку если есть проблемы
|
|
1341
1433
|
if (errors.length > 0) {
|
|
@@ -1371,6 +1463,27 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1371
1463
|
if (!signal) {
|
|
1372
1464
|
return null;
|
|
1373
1465
|
}
|
|
1466
|
+
// Если priceOpen указан - создаем scheduled signal (risk check при активации)
|
|
1467
|
+
if (signal.priceOpen !== undefined) {
|
|
1468
|
+
const scheduledSignalRow = {
|
|
1469
|
+
id: functoolsKit.randomString(),
|
|
1470
|
+
priceOpen: signal.priceOpen,
|
|
1471
|
+
position: signal.position,
|
|
1472
|
+
note: signal.note,
|
|
1473
|
+
priceTakeProfit: signal.priceTakeProfit,
|
|
1474
|
+
priceStopLoss: signal.priceStopLoss,
|
|
1475
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
1476
|
+
symbol: self.params.execution.context.symbol,
|
|
1477
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1478
|
+
strategyName: self.params.method.context.strategyName,
|
|
1479
|
+
scheduledAt: currentTime,
|
|
1480
|
+
pendingAt: currentTime, // Временно, обновится при активации
|
|
1481
|
+
_isScheduled: true,
|
|
1482
|
+
};
|
|
1483
|
+
// Валидируем сигнал перед возвратом
|
|
1484
|
+
VALIDATE_SIGNAL_FN(scheduledSignalRow);
|
|
1485
|
+
return scheduledSignalRow;
|
|
1486
|
+
}
|
|
1374
1487
|
const signalRow = {
|
|
1375
1488
|
id: functoolsKit.randomString(),
|
|
1376
1489
|
priceOpen: currentPrice,
|
|
@@ -1378,7 +1491,9 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1378
1491
|
symbol: self.params.execution.context.symbol,
|
|
1379
1492
|
exchangeName: self.params.method.context.exchangeName,
|
|
1380
1493
|
strategyName: self.params.method.context.strategyName,
|
|
1381
|
-
|
|
1494
|
+
scheduledAt: currentTime,
|
|
1495
|
+
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
1496
|
+
_isScheduled: false,
|
|
1382
1497
|
};
|
|
1383
1498
|
// Валидируем сигнал перед возвратом
|
|
1384
1499
|
VALIDATE_SIGNAL_FN(signalRow);
|
|
@@ -1419,6 +1534,555 @@ const WAIT_FOR_INIT_FN$1 = async (self) => {
|
|
|
1419
1534
|
return;
|
|
1420
1535
|
}
|
|
1421
1536
|
self._pendingSignal = pendingSignal;
|
|
1537
|
+
// Call onActive callback for restored signal
|
|
1538
|
+
if (self.params.callbacks?.onActive) {
|
|
1539
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1540
|
+
self.params.callbacks.onActive(self.params.execution.context.symbol, pendingSignal, currentPrice, self.params.execution.context.backtest);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice) => {
|
|
1544
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1545
|
+
const signalTime = scheduled.scheduledAt; // Таймаут для scheduled signal считается от scheduledAt
|
|
1546
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
1547
|
+
const elapsedTime = currentTime - signalTime;
|
|
1548
|
+
if (elapsedTime < maxTimeToWait) {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled by timeout", {
|
|
1552
|
+
symbol: self.params.execution.context.symbol,
|
|
1553
|
+
signalId: scheduled.id,
|
|
1554
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
1555
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
1556
|
+
});
|
|
1557
|
+
self._scheduledSignal = null;
|
|
1558
|
+
if (self.params.callbacks?.onCancel) {
|
|
1559
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, currentPrice, self.params.execution.context.backtest);
|
|
1560
|
+
}
|
|
1561
|
+
const result = {
|
|
1562
|
+
action: "cancelled",
|
|
1563
|
+
signal: scheduled,
|
|
1564
|
+
currentPrice: currentPrice,
|
|
1565
|
+
closeTimestamp: currentTime,
|
|
1566
|
+
strategyName: self.params.method.context.strategyName,
|
|
1567
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1568
|
+
symbol: self.params.execution.context.symbol,
|
|
1569
|
+
};
|
|
1570
|
+
if (self.params.callbacks?.onTick) {
|
|
1571
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1572
|
+
}
|
|
1573
|
+
return result;
|
|
1574
|
+
};
|
|
1575
|
+
const CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN = (scheduled, currentPrice) => {
|
|
1576
|
+
let shouldActivate = false;
|
|
1577
|
+
let shouldCancel = false;
|
|
1578
|
+
if (scheduled.position === "long") {
|
|
1579
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1580
|
+
// Отмена если цена упала СЛИШКОМ низко (ниже SL)
|
|
1581
|
+
if (currentPrice <= scheduled.priceStopLoss) {
|
|
1582
|
+
shouldCancel = true;
|
|
1583
|
+
}
|
|
1584
|
+
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1585
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1586
|
+
else if (currentPrice <= scheduled.priceOpen) {
|
|
1587
|
+
shouldActivate = true;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (scheduled.position === "short") {
|
|
1591
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1592
|
+
// Отмена если цена выросла СЛИШКОМ высоко (выше SL)
|
|
1593
|
+
if (currentPrice >= scheduled.priceStopLoss) {
|
|
1594
|
+
shouldCancel = true;
|
|
1595
|
+
}
|
|
1596
|
+
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
1597
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1598
|
+
else if (currentPrice >= scheduled.priceOpen) {
|
|
1599
|
+
shouldActivate = true;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { shouldActivate, shouldCancel };
|
|
1603
|
+
};
|
|
1604
|
+
const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPrice) => {
|
|
1605
|
+
self.params.logger.info("ClientStrategy scheduled signal cancelled", {
|
|
1606
|
+
symbol: self.params.execution.context.symbol,
|
|
1607
|
+
signalId: scheduled.id,
|
|
1608
|
+
position: scheduled.position,
|
|
1609
|
+
averagePrice: currentPrice,
|
|
1610
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1611
|
+
});
|
|
1612
|
+
self._scheduledSignal = null;
|
|
1613
|
+
const result = {
|
|
1614
|
+
action: "idle",
|
|
1615
|
+
signal: null,
|
|
1616
|
+
strategyName: self.params.method.context.strategyName,
|
|
1617
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1618
|
+
symbol: self.params.execution.context.symbol,
|
|
1619
|
+
currentPrice: currentPrice,
|
|
1620
|
+
};
|
|
1621
|
+
if (self.params.callbacks?.onTick) {
|
|
1622
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1623
|
+
}
|
|
1624
|
+
return result;
|
|
1625
|
+
};
|
|
1626
|
+
const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp) => {
|
|
1627
|
+
// Check if strategy was stopped
|
|
1628
|
+
if (self._isStopped) {
|
|
1629
|
+
self.params.logger.info("ClientStrategy scheduled signal activation cancelled (stopped)", {
|
|
1630
|
+
symbol: self.params.execution.context.symbol,
|
|
1631
|
+
signalId: scheduled.id,
|
|
1632
|
+
});
|
|
1633
|
+
self._scheduledSignal = null;
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
// В LIVE режиме activationTimestamp - это текущее время при tick()
|
|
1637
|
+
// В отличие от BACKTEST (где используется candle.timestamp + 60s),
|
|
1638
|
+
// здесь мы не знаем ТОЧНОЕ время достижения priceOpen,
|
|
1639
|
+
// поэтому используем время обнаружения активации
|
|
1640
|
+
const activationTime = activationTimestamp;
|
|
1641
|
+
self.params.logger.info("ClientStrategy scheduled signal activation begin", {
|
|
1642
|
+
symbol: self.params.execution.context.symbol,
|
|
1643
|
+
signalId: scheduled.id,
|
|
1644
|
+
position: scheduled.position,
|
|
1645
|
+
averagePrice: scheduled.priceOpen,
|
|
1646
|
+
priceOpen: scheduled.priceOpen,
|
|
1647
|
+
scheduledAt: scheduled.scheduledAt,
|
|
1648
|
+
pendingAt: activationTime,
|
|
1649
|
+
});
|
|
1650
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1651
|
+
symbol: self.params.execution.context.symbol,
|
|
1652
|
+
strategyName: self.params.method.context.strategyName,
|
|
1653
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1654
|
+
currentPrice: scheduled.priceOpen,
|
|
1655
|
+
timestamp: activationTime,
|
|
1656
|
+
}))) {
|
|
1657
|
+
self.params.logger.info("ClientStrategy scheduled signal rejected by risk", {
|
|
1658
|
+
symbol: self.params.execution.context.symbol,
|
|
1659
|
+
signalId: scheduled.id,
|
|
1660
|
+
});
|
|
1661
|
+
self._scheduledSignal = null;
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
self._scheduledSignal = null;
|
|
1665
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
1666
|
+
const activatedSignal = {
|
|
1667
|
+
...scheduled,
|
|
1668
|
+
pendingAt: activationTime,
|
|
1669
|
+
_isScheduled: false,
|
|
1670
|
+
};
|
|
1671
|
+
await self.setPendingSignal(activatedSignal);
|
|
1672
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1673
|
+
strategyName: self.params.method.context.strategyName,
|
|
1674
|
+
riskName: self.params.riskName,
|
|
1675
|
+
});
|
|
1676
|
+
if (self.params.callbacks?.onOpen) {
|
|
1677
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, self.params.execution.context.backtest);
|
|
1678
|
+
}
|
|
1679
|
+
const result = {
|
|
1680
|
+
action: "opened",
|
|
1681
|
+
signal: self._pendingSignal,
|
|
1682
|
+
strategyName: self.params.method.context.strategyName,
|
|
1683
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1684
|
+
symbol: self.params.execution.context.symbol,
|
|
1685
|
+
currentPrice: self._pendingSignal.priceOpen,
|
|
1686
|
+
};
|
|
1687
|
+
if (self.params.callbacks?.onTick) {
|
|
1688
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1689
|
+
}
|
|
1690
|
+
return result;
|
|
1691
|
+
};
|
|
1692
|
+
const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice) => {
|
|
1693
|
+
const result = {
|
|
1694
|
+
action: "active",
|
|
1695
|
+
signal: scheduled,
|
|
1696
|
+
currentPrice: currentPrice,
|
|
1697
|
+
strategyName: self.params.method.context.strategyName,
|
|
1698
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1699
|
+
symbol: self.params.execution.context.symbol,
|
|
1700
|
+
};
|
|
1701
|
+
if (self.params.callbacks?.onTick) {
|
|
1702
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1703
|
+
}
|
|
1704
|
+
return result;
|
|
1705
|
+
};
|
|
1706
|
+
const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
1707
|
+
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
1708
|
+
self.params.logger.info("ClientStrategy scheduled signal created", {
|
|
1709
|
+
symbol: self.params.execution.context.symbol,
|
|
1710
|
+
signalId: signal.id,
|
|
1711
|
+
position: signal.position,
|
|
1712
|
+
priceOpen: signal.priceOpen,
|
|
1713
|
+
currentPrice: currentPrice,
|
|
1714
|
+
});
|
|
1715
|
+
if (self.params.callbacks?.onSchedule) {
|
|
1716
|
+
self.params.callbacks.onSchedule(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1717
|
+
}
|
|
1718
|
+
const result = {
|
|
1719
|
+
action: "scheduled",
|
|
1720
|
+
signal: signal,
|
|
1721
|
+
strategyName: self.params.method.context.strategyName,
|
|
1722
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1723
|
+
symbol: self.params.execution.context.symbol,
|
|
1724
|
+
currentPrice: currentPrice,
|
|
1725
|
+
};
|
|
1726
|
+
if (self.params.callbacks?.onTick) {
|
|
1727
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1728
|
+
}
|
|
1729
|
+
return result;
|
|
1730
|
+
};
|
|
1731
|
+
const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
1732
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1733
|
+
symbol: self.params.execution.context.symbol,
|
|
1734
|
+
strategyName: self.params.method.context.strategyName,
|
|
1735
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1736
|
+
currentPrice: signal.priceOpen,
|
|
1737
|
+
timestamp: self.params.execution.context.when.getTime(),
|
|
1738
|
+
}))) {
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1742
|
+
strategyName: self.params.method.context.strategyName,
|
|
1743
|
+
riskName: self.params.riskName,
|
|
1744
|
+
});
|
|
1745
|
+
if (self.params.callbacks?.onOpen) {
|
|
1746
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, signal, signal.priceOpen, self.params.execution.context.backtest);
|
|
1747
|
+
}
|
|
1748
|
+
const result = {
|
|
1749
|
+
action: "opened",
|
|
1750
|
+
signal: signal,
|
|
1751
|
+
strategyName: self.params.method.context.strategyName,
|
|
1752
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1753
|
+
symbol: self.params.execution.context.symbol,
|
|
1754
|
+
currentPrice: signal.priceOpen,
|
|
1755
|
+
};
|
|
1756
|
+
if (self.params.callbacks?.onTick) {
|
|
1757
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1758
|
+
}
|
|
1759
|
+
return result;
|
|
1760
|
+
};
|
|
1761
|
+
const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) => {
|
|
1762
|
+
const currentTime = self.params.execution.context.when.getTime();
|
|
1763
|
+
const signalTime = signal.pendingAt; // КРИТИЧНО: используем pendingAt, а не scheduledAt!
|
|
1764
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
1765
|
+
const elapsedTime = currentTime - signalTime;
|
|
1766
|
+
// Check time expiration
|
|
1767
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
1768
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, averagePrice, "time_expired");
|
|
1769
|
+
}
|
|
1770
|
+
// Check take profit
|
|
1771
|
+
if (signal.position === "long" && averagePrice >= signal.priceTakeProfit) {
|
|
1772
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1773
|
+
"take_profit");
|
|
1774
|
+
}
|
|
1775
|
+
if (signal.position === "short" && averagePrice <= signal.priceTakeProfit) {
|
|
1776
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
1777
|
+
"take_profit");
|
|
1778
|
+
}
|
|
1779
|
+
// Check stop loss
|
|
1780
|
+
if (signal.position === "long" && averagePrice <= signal.priceStopLoss) {
|
|
1781
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1782
|
+
"stop_loss");
|
|
1783
|
+
}
|
|
1784
|
+
if (signal.position === "short" && averagePrice >= signal.priceStopLoss) {
|
|
1785
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceStopLoss, // КРИТИЧНО: используем точную цену SL
|
|
1786
|
+
"stop_loss");
|
|
1787
|
+
}
|
|
1788
|
+
return null;
|
|
1789
|
+
};
|
|
1790
|
+
const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason) => {
|
|
1791
|
+
const pnl = toProfitLossDto(signal, currentPrice);
|
|
1792
|
+
self.params.logger.info(`ClientStrategy signal ${closeReason}`, {
|
|
1793
|
+
symbol: self.params.execution.context.symbol,
|
|
1794
|
+
signalId: signal.id,
|
|
1795
|
+
closeReason,
|
|
1796
|
+
priceClose: currentPrice,
|
|
1797
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
1798
|
+
});
|
|
1799
|
+
if (self.params.callbacks?.onClose) {
|
|
1800
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
|
|
1801
|
+
}
|
|
1802
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1803
|
+
strategyName: self.params.method.context.strategyName,
|
|
1804
|
+
riskName: self.params.riskName,
|
|
1805
|
+
});
|
|
1806
|
+
await self.setPendingSignal(null);
|
|
1807
|
+
const result = {
|
|
1808
|
+
action: "closed",
|
|
1809
|
+
signal: signal,
|
|
1810
|
+
currentPrice: currentPrice,
|
|
1811
|
+
closeReason: closeReason,
|
|
1812
|
+
closeTimestamp: self.params.execution.context.when.getTime(),
|
|
1813
|
+
pnl: pnl,
|
|
1814
|
+
strategyName: self.params.method.context.strategyName,
|
|
1815
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1816
|
+
symbol: self.params.execution.context.symbol,
|
|
1817
|
+
};
|
|
1818
|
+
if (self.params.callbacks?.onTick) {
|
|
1819
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1820
|
+
}
|
|
1821
|
+
return result;
|
|
1822
|
+
};
|
|
1823
|
+
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
1824
|
+
const result = {
|
|
1825
|
+
action: "active",
|
|
1826
|
+
signal: signal,
|
|
1827
|
+
currentPrice: currentPrice,
|
|
1828
|
+
strategyName: self.params.method.context.strategyName,
|
|
1829
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1830
|
+
symbol: self.params.execution.context.symbol,
|
|
1831
|
+
};
|
|
1832
|
+
if (self.params.callbacks?.onTick) {
|
|
1833
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1834
|
+
}
|
|
1835
|
+
return result;
|
|
1836
|
+
};
|
|
1837
|
+
const RETURN_IDLE_FN = async (self, currentPrice) => {
|
|
1838
|
+
if (self.params.callbacks?.onIdle) {
|
|
1839
|
+
self.params.callbacks.onIdle(self.params.execution.context.symbol, currentPrice, self.params.execution.context.backtest);
|
|
1840
|
+
}
|
|
1841
|
+
const result = {
|
|
1842
|
+
action: "idle",
|
|
1843
|
+
signal: null,
|
|
1844
|
+
strategyName: self.params.method.context.strategyName,
|
|
1845
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1846
|
+
symbol: self.params.execution.context.symbol,
|
|
1847
|
+
currentPrice: currentPrice,
|
|
1848
|
+
};
|
|
1849
|
+
if (self.params.callbacks?.onTick) {
|
|
1850
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1851
|
+
}
|
|
1852
|
+
return result;
|
|
1853
|
+
};
|
|
1854
|
+
const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePrice, closeTimestamp) => {
|
|
1855
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal cancelled", {
|
|
1856
|
+
symbol: self.params.execution.context.symbol,
|
|
1857
|
+
signalId: scheduled.id,
|
|
1858
|
+
closeTimestamp,
|
|
1859
|
+
averagePrice,
|
|
1860
|
+
priceStopLoss: scheduled.priceStopLoss,
|
|
1861
|
+
});
|
|
1862
|
+
self._scheduledSignal = null;
|
|
1863
|
+
if (self.params.callbacks?.onCancel) {
|
|
1864
|
+
self.params.callbacks.onCancel(self.params.execution.context.symbol, scheduled, averagePrice, self.params.execution.context.backtest);
|
|
1865
|
+
}
|
|
1866
|
+
const result = {
|
|
1867
|
+
action: "cancelled",
|
|
1868
|
+
signal: scheduled,
|
|
1869
|
+
currentPrice: averagePrice,
|
|
1870
|
+
closeTimestamp: closeTimestamp,
|
|
1871
|
+
strategyName: self.params.method.context.strategyName,
|
|
1872
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1873
|
+
symbol: self.params.execution.context.symbol,
|
|
1874
|
+
};
|
|
1875
|
+
if (self.params.callbacks?.onTick) {
|
|
1876
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1877
|
+
}
|
|
1878
|
+
return result;
|
|
1879
|
+
};
|
|
1880
|
+
const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activationTimestamp) => {
|
|
1881
|
+
// Check if strategy was stopped
|
|
1882
|
+
if (self._isStopped) {
|
|
1883
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activation cancelled (stopped)", {
|
|
1884
|
+
symbol: self.params.execution.context.symbol,
|
|
1885
|
+
signalId: scheduled.id,
|
|
1886
|
+
});
|
|
1887
|
+
self._scheduledSignal = null;
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
// В BACKTEST режиме activationTimestamp - это candle.timestamp + 60*1000
|
|
1891
|
+
// (timestamp СЛЕДУЮЩЕЙ свечи после достижения priceOpen)
|
|
1892
|
+
// Это обеспечивает точный расчёт minuteEstimatedTime от момента активации
|
|
1893
|
+
const activationTime = activationTimestamp;
|
|
1894
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal activated", {
|
|
1895
|
+
symbol: self.params.execution.context.symbol,
|
|
1896
|
+
signalId: scheduled.id,
|
|
1897
|
+
priceOpen: scheduled.priceOpen,
|
|
1898
|
+
scheduledAt: scheduled.scheduledAt,
|
|
1899
|
+
pendingAt: activationTime,
|
|
1900
|
+
});
|
|
1901
|
+
if (await functoolsKit.not(self.params.risk.checkSignal({
|
|
1902
|
+
symbol: self.params.execution.context.symbol,
|
|
1903
|
+
strategyName: self.params.method.context.strategyName,
|
|
1904
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1905
|
+
currentPrice: scheduled.priceOpen,
|
|
1906
|
+
timestamp: activationTime,
|
|
1907
|
+
}))) {
|
|
1908
|
+
self.params.logger.info("ClientStrategy backtest scheduled signal rejected by risk", {
|
|
1909
|
+
symbol: self.params.execution.context.symbol,
|
|
1910
|
+
signalId: scheduled.id,
|
|
1911
|
+
});
|
|
1912
|
+
self._scheduledSignal = null;
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
self._scheduledSignal = null;
|
|
1916
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации в backtest
|
|
1917
|
+
const activatedSignal = {
|
|
1918
|
+
...scheduled,
|
|
1919
|
+
pendingAt: activationTime,
|
|
1920
|
+
_isScheduled: false,
|
|
1921
|
+
};
|
|
1922
|
+
await self.setPendingSignal(activatedSignal);
|
|
1923
|
+
await self.params.risk.addSignal(self.params.execution.context.symbol, {
|
|
1924
|
+
strategyName: self.params.method.context.strategyName,
|
|
1925
|
+
riskName: self.params.riskName,
|
|
1926
|
+
});
|
|
1927
|
+
if (self.params.callbacks?.onOpen) {
|
|
1928
|
+
self.params.callbacks.onOpen(self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, self.params.execution.context.backtest);
|
|
1929
|
+
}
|
|
1930
|
+
return true;
|
|
1931
|
+
};
|
|
1932
|
+
const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, closeReason, closeTimestamp) => {
|
|
1933
|
+
const pnl = toProfitLossDto(signal, averagePrice);
|
|
1934
|
+
self.params.logger.debug(`ClientStrategy backtest ${closeReason}`, {
|
|
1935
|
+
symbol: self.params.execution.context.symbol,
|
|
1936
|
+
signalId: signal.id,
|
|
1937
|
+
reason: closeReason,
|
|
1938
|
+
priceClose: averagePrice,
|
|
1939
|
+
closeTimestamp,
|
|
1940
|
+
pnlPercentage: pnl.pnlPercentage,
|
|
1941
|
+
});
|
|
1942
|
+
if (closeReason === "stop_loss") {
|
|
1943
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1944
|
+
}
|
|
1945
|
+
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1946
|
+
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1947
|
+
}
|
|
1948
|
+
if (self.params.callbacks?.onClose) {
|
|
1949
|
+
self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
|
|
1950
|
+
}
|
|
1951
|
+
await self.params.risk.removeSignal(self.params.execution.context.symbol, {
|
|
1952
|
+
strategyName: self.params.method.context.strategyName,
|
|
1953
|
+
riskName: self.params.riskName,
|
|
1954
|
+
});
|
|
1955
|
+
await self.setPendingSignal(null);
|
|
1956
|
+
const result = {
|
|
1957
|
+
action: "closed",
|
|
1958
|
+
signal: signal,
|
|
1959
|
+
currentPrice: averagePrice,
|
|
1960
|
+
closeReason: closeReason,
|
|
1961
|
+
closeTimestamp: closeTimestamp,
|
|
1962
|
+
pnl: pnl,
|
|
1963
|
+
strategyName: self.params.method.context.strategyName,
|
|
1964
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
1965
|
+
symbol: self.params.execution.context.symbol,
|
|
1966
|
+
};
|
|
1967
|
+
if (self.params.callbacks?.onTick) {
|
|
1968
|
+
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
1969
|
+
}
|
|
1970
|
+
return result;
|
|
1971
|
+
};
|
|
1972
|
+
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
1973
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
1974
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
1975
|
+
for (let i = 0; i < candles.length; i++) {
|
|
1976
|
+
const candle = candles[i];
|
|
1977
|
+
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
1978
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
1979
|
+
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
1980
|
+
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
1981
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
1982
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
1983
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
1984
|
+
}
|
|
1985
|
+
let shouldActivate = false;
|
|
1986
|
+
let shouldCancel = false;
|
|
1987
|
+
if (scheduled.position === "long") {
|
|
1988
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
1989
|
+
// Отмена если цена упала СЛИШКОМ низко (ниже SL)
|
|
1990
|
+
if (candle.low <= scheduled.priceStopLoss) {
|
|
1991
|
+
shouldCancel = true;
|
|
1992
|
+
}
|
|
1993
|
+
// Long = покупаем дешевле, ждем падения цены ДО priceOpen
|
|
1994
|
+
// Активируем только если НЕ пробит StopLoss
|
|
1995
|
+
else if (candle.low <= scheduled.priceOpen) {
|
|
1996
|
+
shouldActivate = true;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (scheduled.position === "short") {
|
|
2000
|
+
// КРИТИЧНО: Сначала проверяем StopLoss (отмена приоритетнее активации)
|
|
2001
|
+
// Отмена если цена выросла СЛИШКОМ высоко (выше SL)
|
|
2002
|
+
if (candle.high >= scheduled.priceStopLoss) {
|
|
2003
|
+
shouldCancel = true;
|
|
2004
|
+
}
|
|
2005
|
+
// Short = продаем дороже, ждем роста цены ДО priceOpen
|
|
2006
|
+
// Активируем только если НЕ пробит StopLoss
|
|
2007
|
+
else if (candle.high >= scheduled.priceOpen) {
|
|
2008
|
+
shouldActivate = true;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (shouldCancel) {
|
|
2012
|
+
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp);
|
|
2013
|
+
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
2014
|
+
}
|
|
2015
|
+
if (shouldActivate) {
|
|
2016
|
+
await ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, candle.timestamp);
|
|
2017
|
+
return {
|
|
2018
|
+
activated: true,
|
|
2019
|
+
cancelled: false,
|
|
2020
|
+
activationIndex: i,
|
|
2021
|
+
result: null,
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
activated: false,
|
|
2027
|
+
cancelled: false,
|
|
2028
|
+
activationIndex: -1,
|
|
2029
|
+
result: null,
|
|
2030
|
+
};
|
|
2031
|
+
};
|
|
2032
|
+
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2033
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2034
|
+
for (let i = candlesCount - 1; i < candles.length; i++) {
|
|
2035
|
+
const recentCandles = candles.slice(i - (candlesCount - 1), i + 1);
|
|
2036
|
+
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2037
|
+
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2038
|
+
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2039
|
+
let shouldClose = false;
|
|
2040
|
+
let closeReason;
|
|
2041
|
+
// Check time expiration FIRST (КРИТИЧНО!)
|
|
2042
|
+
const signalTime = signal.pendingAt;
|
|
2043
|
+
const maxTimeToWait = signal.minuteEstimatedTime * 60 * 1000;
|
|
2044
|
+
const elapsedTime = currentCandleTimestamp - signalTime;
|
|
2045
|
+
if (elapsedTime >= maxTimeToWait) {
|
|
2046
|
+
shouldClose = true;
|
|
2047
|
+
closeReason = "time_expired";
|
|
2048
|
+
}
|
|
2049
|
+
// Check TP/SL only if not expired
|
|
2050
|
+
// КРИТИЧНО: используем candle.high/low для точной проверки достижения TP/SL
|
|
2051
|
+
if (!shouldClose && signal.position === "long") {
|
|
2052
|
+
// Для LONG: TP срабатывает если high >= TP, SL если low <= SL
|
|
2053
|
+
if (currentCandle.high >= signal.priceTakeProfit) {
|
|
2054
|
+
shouldClose = true;
|
|
2055
|
+
closeReason = "take_profit";
|
|
2056
|
+
}
|
|
2057
|
+
else if (currentCandle.low <= signal.priceStopLoss) {
|
|
2058
|
+
shouldClose = true;
|
|
2059
|
+
closeReason = "stop_loss";
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (!shouldClose && signal.position === "short") {
|
|
2063
|
+
// Для SHORT: TP срабатывает если low <= TP, SL если high >= SL
|
|
2064
|
+
if (currentCandle.low <= signal.priceTakeProfit) {
|
|
2065
|
+
shouldClose = true;
|
|
2066
|
+
closeReason = "take_profit";
|
|
2067
|
+
}
|
|
2068
|
+
else if (currentCandle.high >= signal.priceStopLoss) {
|
|
2069
|
+
shouldClose = true;
|
|
2070
|
+
closeReason = "stop_loss";
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
if (shouldClose) {
|
|
2074
|
+
// КРИТИЧНО: при закрытии по TP/SL используем точную цену, а не averagePrice
|
|
2075
|
+
let closePrice = averagePrice;
|
|
2076
|
+
if (closeReason === "take_profit") {
|
|
2077
|
+
closePrice = signal.priceTakeProfit;
|
|
2078
|
+
}
|
|
2079
|
+
else if (closeReason === "stop_loss") {
|
|
2080
|
+
closePrice = signal.priceStopLoss;
|
|
2081
|
+
}
|
|
2082
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return null;
|
|
1422
2086
|
};
|
|
1423
2087
|
/**
|
|
1424
2088
|
* Client implementation for trading strategy lifecycle management.
|
|
@@ -1452,6 +2116,7 @@ class ClientStrategy {
|
|
|
1452
2116
|
this.params = params;
|
|
1453
2117
|
this._isStopped = false;
|
|
1454
2118
|
this._pendingSignal = null;
|
|
2119
|
+
this._scheduledSignal = null;
|
|
1455
2120
|
this._lastSignalTimestamp = null;
|
|
1456
2121
|
/**
|
|
1457
2122
|
* Initializes strategy state by loading persisted signal from disk.
|
|
@@ -1486,14 +2151,21 @@ class ClientStrategy {
|
|
|
1486
2151
|
/**
|
|
1487
2152
|
* Performs a single tick of strategy execution.
|
|
1488
2153
|
*
|
|
1489
|
-
* Flow:
|
|
1490
|
-
* 1. If
|
|
1491
|
-
* 2. If signal
|
|
1492
|
-
* 3. If
|
|
1493
|
-
* 4. If
|
|
2154
|
+
* Flow (LIVE mode):
|
|
2155
|
+
* 1. If scheduled signal exists: check activation/cancellation
|
|
2156
|
+
* 2. If no pending/scheduled signal: call getSignal with throttling and validation
|
|
2157
|
+
* 3. If signal opened: trigger onOpen callback, persist state
|
|
2158
|
+
* 4. If pending signal exists: check VWAP against TP/SL
|
|
2159
|
+
* 5. If TP/SL/time reached: close signal, trigger onClose, persist state
|
|
2160
|
+
*
|
|
2161
|
+
* Flow (BACKTEST mode):
|
|
2162
|
+
* 1. If no pending/scheduled signal: call getSignal
|
|
2163
|
+
* 2. If scheduled signal created: return "scheduled" (backtest() will handle it)
|
|
2164
|
+
* 3. Otherwise same as LIVE
|
|
1494
2165
|
*
|
|
1495
2166
|
* @returns Promise resolving to discriminated union result:
|
|
1496
2167
|
* - idle: No signal generated
|
|
2168
|
+
* - scheduled: Scheduled signal created (backtest only)
|
|
1497
2169
|
* - opened: New signal just created
|
|
1498
2170
|
* - active: Signal monitoring in progress
|
|
1499
2171
|
* - closed: Signal completed with PNL
|
|
@@ -1508,301 +2180,195 @@ class ClientStrategy {
|
|
|
1508
2180
|
*/
|
|
1509
2181
|
async tick() {
|
|
1510
2182
|
this.params.logger.debug("ClientStrategy tick");
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
// Register signal with risk management
|
|
1516
|
-
await this.params.risk.addSignal(this.params.execution.context.symbol, {
|
|
1517
|
-
strategyName: this.params.method.context.strategyName,
|
|
1518
|
-
riskName: this.params.riskName,
|
|
1519
|
-
});
|
|
1520
|
-
if (this.params.callbacks?.onOpen) {
|
|
1521
|
-
this.params.callbacks.onOpen(this.params.execution.context.symbol, this._pendingSignal, this._pendingSignal.priceOpen, this.params.execution.context.backtest);
|
|
1522
|
-
}
|
|
1523
|
-
const result = {
|
|
1524
|
-
action: "opened",
|
|
1525
|
-
signal: this._pendingSignal,
|
|
1526
|
-
strategyName: this.params.method.context.strategyName,
|
|
1527
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1528
|
-
symbol: this.params.execution.context.symbol,
|
|
1529
|
-
currentPrice: this._pendingSignal.priceOpen,
|
|
1530
|
-
};
|
|
1531
|
-
if (this.params.callbacks?.onTick) {
|
|
1532
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1533
|
-
}
|
|
1534
|
-
return result;
|
|
1535
|
-
}
|
|
2183
|
+
// Получаем текущее время в начале tick для консистентности
|
|
2184
|
+
const currentTime = this.params.execution.context.when.getTime();
|
|
2185
|
+
// Early return if strategy was stopped
|
|
2186
|
+
if (this._isStopped) {
|
|
1536
2187
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1537
|
-
|
|
1538
|
-
this.params.callbacks.onIdle(this.params.execution.context.symbol, currentPrice, this.params.execution.context.backtest);
|
|
1539
|
-
}
|
|
1540
|
-
const result = {
|
|
1541
|
-
action: "idle",
|
|
1542
|
-
signal: null,
|
|
1543
|
-
strategyName: this.params.method.context.strategyName,
|
|
1544
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1545
|
-
symbol: this.params.execution.context.symbol,
|
|
1546
|
-
currentPrice,
|
|
1547
|
-
};
|
|
1548
|
-
if (this.params.callbacks?.onTick) {
|
|
1549
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1550
|
-
}
|
|
1551
|
-
return result;
|
|
1552
|
-
}
|
|
1553
|
-
const when = this.params.execution.context.when;
|
|
1554
|
-
const signal = this._pendingSignal;
|
|
1555
|
-
// Получаем среднюю цену
|
|
1556
|
-
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
1557
|
-
this.params.logger.debug("ClientStrategy tick check", {
|
|
1558
|
-
symbol: this.params.execution.context.symbol,
|
|
1559
|
-
averagePrice,
|
|
1560
|
-
signalId: signal.id,
|
|
1561
|
-
position: signal.position,
|
|
1562
|
-
});
|
|
1563
|
-
let shouldClose = false;
|
|
1564
|
-
let closeReason;
|
|
1565
|
-
// Проверяем истечение времени
|
|
1566
|
-
const signalEndTime = signal.timestamp + signal.minuteEstimatedTime * 60 * 1000;
|
|
1567
|
-
if (when.getTime() >= signalEndTime) {
|
|
1568
|
-
shouldClose = true;
|
|
1569
|
-
closeReason = "time_expired";
|
|
2188
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1570
2189
|
}
|
|
1571
|
-
//
|
|
1572
|
-
if (
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2190
|
+
// Monitor scheduled signal
|
|
2191
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2192
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2193
|
+
// Check timeout
|
|
2194
|
+
const timeoutResult = await CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN(this, this._scheduledSignal, currentPrice);
|
|
2195
|
+
if (timeoutResult)
|
|
2196
|
+
return timeoutResult;
|
|
2197
|
+
// Check price-based activation/cancellation
|
|
2198
|
+
const { shouldActivate, shouldCancel } = CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN(this._scheduledSignal, currentPrice);
|
|
2199
|
+
if (shouldCancel) {
|
|
2200
|
+
return await CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN(this, this._scheduledSignal, currentPrice);
|
|
1576
2201
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2202
|
+
if (shouldActivate) {
|
|
2203
|
+
const activateResult = await ACTIVATE_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal, currentTime);
|
|
2204
|
+
if (activateResult) {
|
|
2205
|
+
return activateResult;
|
|
2206
|
+
}
|
|
2207
|
+
// Risk rejected or stopped - return idle
|
|
2208
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1580
2209
|
}
|
|
2210
|
+
return await RETURN_SCHEDULED_SIGNAL_ACTIVE_FN(this, this._scheduledSignal, currentPrice);
|
|
1581
2211
|
}
|
|
1582
|
-
//
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2212
|
+
// Generate new signal if none exists
|
|
2213
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2214
|
+
const signal = await GET_SIGNAL_FN(this);
|
|
2215
|
+
if (signal) {
|
|
2216
|
+
if (signal._isScheduled === true) {
|
|
2217
|
+
this._scheduledSignal = signal;
|
|
2218
|
+
return await OPEN_NEW_SCHEDULED_SIGNAL_FN(this, this._scheduledSignal);
|
|
2219
|
+
}
|
|
2220
|
+
await this.setPendingSignal(signal);
|
|
1587
2221
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2222
|
+
if (this._pendingSignal) {
|
|
2223
|
+
const openResult = await OPEN_NEW_PENDING_SIGNAL_FN(this, this._pendingSignal);
|
|
2224
|
+
if (openResult) {
|
|
2225
|
+
return openResult;
|
|
2226
|
+
}
|
|
2227
|
+
// Risk rejected - clear pending signal and return idle
|
|
2228
|
+
await this.setPendingSignal(null);
|
|
1591
2229
|
}
|
|
2230
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2231
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
1592
2232
|
}
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
if (closeReason === "stop_loss") {
|
|
1599
|
-
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (stop_loss), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1600
|
-
}
|
|
1601
|
-
// Предупреждение о закрытии сигнала в убыток
|
|
1602
|
-
if (closeReason === "time_expired" && pnl.pnlPercentage < 0) {
|
|
1603
|
-
this.params.logger.warn(`ClientStrategy tick: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
1604
|
-
}
|
|
1605
|
-
this.params.logger.debug("ClientStrategy closing", {
|
|
1606
|
-
symbol: this.params.execution.context.symbol,
|
|
1607
|
-
signalId: signal.id,
|
|
1608
|
-
reason: closeReason,
|
|
1609
|
-
priceClose: averagePrice,
|
|
1610
|
-
closeTimestamp,
|
|
1611
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1612
|
-
});
|
|
1613
|
-
if (this.params.callbacks?.onClose) {
|
|
1614
|
-
this.params.callbacks.onClose(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1615
|
-
}
|
|
1616
|
-
// Remove signal from risk management
|
|
1617
|
-
await this.params.risk.removeSignal(this.params.execution.context.symbol, {
|
|
1618
|
-
strategyName: this.params.method.context.strategyName,
|
|
1619
|
-
riskName: this.params.riskName,
|
|
1620
|
-
});
|
|
1621
|
-
await this.setPendingSignal(null);
|
|
1622
|
-
const result = {
|
|
1623
|
-
action: "closed",
|
|
1624
|
-
signal: signal,
|
|
1625
|
-
currentPrice: averagePrice,
|
|
1626
|
-
closeReason: closeReason,
|
|
1627
|
-
closeTimestamp: closeTimestamp,
|
|
1628
|
-
pnl: pnl,
|
|
1629
|
-
strategyName: this.params.method.context.strategyName,
|
|
1630
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1631
|
-
symbol: this.params.execution.context.symbol,
|
|
1632
|
-
};
|
|
1633
|
-
if (this.params.callbacks?.onTick) {
|
|
1634
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
1635
|
-
}
|
|
1636
|
-
return result;
|
|
1637
|
-
}
|
|
1638
|
-
if (this.params.callbacks?.onActive) {
|
|
1639
|
-
this.params.callbacks.onActive(this.params.execution.context.symbol, signal, averagePrice, this.params.execution.context.backtest);
|
|
1640
|
-
}
|
|
1641
|
-
const result = {
|
|
1642
|
-
action: "active",
|
|
1643
|
-
signal: signal,
|
|
1644
|
-
currentPrice: averagePrice,
|
|
1645
|
-
strategyName: this.params.method.context.strategyName,
|
|
1646
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1647
|
-
symbol: this.params.execution.context.symbol,
|
|
1648
|
-
};
|
|
1649
|
-
if (this.params.callbacks?.onTick) {
|
|
1650
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2233
|
+
// Monitor pending signal
|
|
2234
|
+
const averagePrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
2235
|
+
const closedResult = await CHECK_PENDING_SIGNAL_COMPLETION_FN(this, this._pendingSignal, averagePrice);
|
|
2236
|
+
if (closedResult) {
|
|
2237
|
+
return closedResult;
|
|
1651
2238
|
}
|
|
1652
|
-
return
|
|
2239
|
+
return await RETURN_PENDING_SIGNAL_ACTIVE_FN(this, this._pendingSignal, averagePrice);
|
|
1653
2240
|
}
|
|
1654
2241
|
/**
|
|
1655
|
-
* Fast backtests a
|
|
2242
|
+
* Fast backtests a signal using historical candle data.
|
|
1656
2243
|
*
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
*
|
|
2244
|
+
* For scheduled signals:
|
|
2245
|
+
* 1. Iterates through candles checking for activation (price reaches priceOpen)
|
|
2246
|
+
* 2. Or cancellation (price hits StopLoss before activation)
|
|
2247
|
+
* 3. If activated: converts to pending signal and continues with TP/SL monitoring
|
|
2248
|
+
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
1660
2249
|
*
|
|
1661
|
-
*
|
|
2250
|
+
* For pending signals:
|
|
2251
|
+
* 1. Iterates through candles checking VWAP against TP/SL on each timeframe
|
|
2252
|
+
* 2. Starts from index 4 (needs 5 candles for VWAP calculation)
|
|
2253
|
+
* 3. Returns closed result (either TP/SL or time_expired)
|
|
2254
|
+
*
|
|
2255
|
+
* @param candles - Array of candles to process
|
|
1662
2256
|
* @returns Promise resolving to closed signal result with PNL
|
|
1663
|
-
* @throws Error if no pending signal or not in backtest mode
|
|
2257
|
+
* @throws Error if no pending/scheduled signal or not in backtest mode
|
|
1664
2258
|
*
|
|
1665
2259
|
* @example
|
|
1666
2260
|
* ```typescript
|
|
1667
2261
|
* // After signal opened in backtest
|
|
1668
2262
|
* const candles = await exchange.getNextCandles("BTCUSDT", "1m", signal.minuteEstimatedTime);
|
|
1669
2263
|
* const result = await strategy.backtest(candles);
|
|
1670
|
-
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired"
|
|
2264
|
+
* console.log(result.closeReason); // "take_profit" | "stop_loss" | "time_expired" | "cancelled"
|
|
1671
2265
|
* ```
|
|
1672
2266
|
*/
|
|
1673
2267
|
async backtest(candles) {
|
|
1674
2268
|
this.params.logger.debug("ClientStrategy backtest", {
|
|
1675
2269
|
symbol: this.params.execution.context.symbol,
|
|
1676
2270
|
candlesCount: candles.length,
|
|
2271
|
+
hasScheduled: !!this._scheduledSignal,
|
|
2272
|
+
hasPending: !!this._pendingSignal,
|
|
1677
2273
|
});
|
|
1678
|
-
const signal = this._pendingSignal;
|
|
1679
|
-
if (!signal) {
|
|
1680
|
-
throw new Error("ClientStrategy backtest: no pending signal");
|
|
1681
|
-
}
|
|
1682
2274
|
if (!this.params.execution.context.backtest) {
|
|
1683
2275
|
throw new Error("ClientStrategy backtest: running in live context");
|
|
1684
2276
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (
|
|
1699
|
-
|
|
1700
|
-
shouldClose = true;
|
|
1701
|
-
closeReason = "take_profit";
|
|
1702
|
-
}
|
|
1703
|
-
else if (averagePrice <= signal.priceStopLoss) {
|
|
1704
|
-
shouldClose = true;
|
|
1705
|
-
closeReason = "stop_loss";
|
|
1706
|
-
}
|
|
2277
|
+
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
2278
|
+
throw new Error("ClientStrategy backtest: no pending or scheduled signal");
|
|
2279
|
+
}
|
|
2280
|
+
// Process scheduled signal
|
|
2281
|
+
if (this._scheduledSignal && !this._pendingSignal) {
|
|
2282
|
+
const scheduled = this._scheduledSignal;
|
|
2283
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal", {
|
|
2284
|
+
symbol: this.params.execution.context.symbol,
|
|
2285
|
+
signalId: scheduled.id,
|
|
2286
|
+
priceOpen: scheduled.priceOpen,
|
|
2287
|
+
position: scheduled.position,
|
|
2288
|
+
});
|
|
2289
|
+
const { activated, cancelled, activationIndex, result } = await PROCESS_SCHEDULED_SIGNAL_CANDLES_FN(this, scheduled, candles);
|
|
2290
|
+
if (cancelled && result) {
|
|
2291
|
+
return result;
|
|
1707
2292
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
closeReason = "stop_loss";
|
|
2293
|
+
if (activated) {
|
|
2294
|
+
const remainingCandles = candles.slice(activationIndex + 1);
|
|
2295
|
+
if (remainingCandles.length === 0) {
|
|
2296
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2297
|
+
const recentCandles = candles.slice(Math.max(0, activationIndex - (candlesCount - 1)), activationIndex + 1);
|
|
2298
|
+
const lastPrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2299
|
+
const closeTimestamp = candles[activationIndex].timestamp;
|
|
2300
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, "time_expired", closeTimestamp);
|
|
1717
2301
|
}
|
|
2302
|
+
candles = remainingCandles;
|
|
1718
2303
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
const
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2304
|
+
if (this._scheduledSignal) {
|
|
2305
|
+
// Check if timeout reached (CC_SCHEDULE_AWAIT_MINUTES from scheduledAt)
|
|
2306
|
+
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2307
|
+
const lastCandleTimestamp = candles[candles.length - 1].timestamp;
|
|
2308
|
+
const elapsedTime = lastCandleTimestamp - scheduled.scheduledAt;
|
|
2309
|
+
if (elapsedTime < maxTimeToWait) {
|
|
2310
|
+
// Timeout NOT reached yet - signal is still active (waiting for price)
|
|
2311
|
+
// Return active result to continue monitoring in next backtest() call
|
|
2312
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2313
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2314
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2315
|
+
this.params.logger.debug("ClientStrategy backtest scheduled signal still waiting (not expired)", {
|
|
2316
|
+
symbol: this.params.execution.context.symbol,
|
|
2317
|
+
signalId: scheduled.id,
|
|
2318
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2319
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2320
|
+
});
|
|
2321
|
+
// Don't cancel - just return last active state
|
|
2322
|
+
// In real backtest flow this won't happen as we process all candles at once,
|
|
2323
|
+
// but this is correct behavior if someone calls backtest() with partial data
|
|
2324
|
+
const result = {
|
|
2325
|
+
action: "active",
|
|
2326
|
+
signal: scheduled,
|
|
2327
|
+
currentPrice: lastPrice,
|
|
2328
|
+
strategyName: this.params.method.context.strategyName,
|
|
2329
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
2330
|
+
symbol: this.params.execution.context.symbol,
|
|
2331
|
+
};
|
|
2332
|
+
return result; // Cast to IStrategyBacktestResult (which includes Active)
|
|
1734
2333
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
strategyName: this.params.method.context.strategyName,
|
|
1741
|
-
riskName: this.params.riskName,
|
|
1742
|
-
});
|
|
1743
|
-
await this.setPendingSignal(null);
|
|
1744
|
-
const result = {
|
|
1745
|
-
action: "closed",
|
|
1746
|
-
signal: signal,
|
|
1747
|
-
currentPrice: averagePrice,
|
|
1748
|
-
closeReason: closeReason,
|
|
1749
|
-
closeTimestamp: closeTimestamp,
|
|
1750
|
-
pnl: pnl,
|
|
1751
|
-
strategyName: this.params.method.context.strategyName,
|
|
1752
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
2334
|
+
// Timeout reached - cancel the scheduled signal
|
|
2335
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2336
|
+
const lastCandles = candles.slice(-candlesCount);
|
|
2337
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2338
|
+
this.params.logger.info("ClientStrategy backtest scheduled signal cancelled by timeout", {
|
|
1753
2339
|
symbol: this.params.execution.context.symbol,
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2340
|
+
signalId: scheduled.id,
|
|
2341
|
+
closeTimestamp: lastCandleTimestamp,
|
|
2342
|
+
elapsedMinutes: Math.floor(elapsedTime / 60000),
|
|
2343
|
+
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
2344
|
+
reason: "timeout - price never reached priceOpen",
|
|
2345
|
+
});
|
|
2346
|
+
return await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(this, scheduled, lastPrice, lastCandleTimestamp);
|
|
1759
2347
|
}
|
|
1760
2348
|
}
|
|
1761
|
-
//
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const pnl = toProfitLossDto(signal, lastPrice);
|
|
1766
|
-
this.params.logger.debug("ClientStrategy backtest time_expired", {
|
|
1767
|
-
symbol: this.params.execution.context.symbol,
|
|
1768
|
-
signalId: signal.id,
|
|
1769
|
-
priceClose: lastPrice,
|
|
1770
|
-
closeTimestamp,
|
|
1771
|
-
pnlPercentage: pnl.pnlPercentage,
|
|
1772
|
-
});
|
|
1773
|
-
// Предупреждение при убытке от time_expired
|
|
1774
|
-
if (pnl.pnlPercentage < 0) {
|
|
1775
|
-
this.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${pnl.pnlPercentage.toFixed(2)}%`);
|
|
2349
|
+
// Process pending signal
|
|
2350
|
+
const signal = this._pendingSignal;
|
|
2351
|
+
if (!signal) {
|
|
2352
|
+
throw new Error("ClientStrategy backtest: no pending signal after scheduled activation");
|
|
1776
2353
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
2354
|
+
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2355
|
+
if (candles.length < candlesCount) {
|
|
2356
|
+
this.params.logger.warn(`ClientStrategy backtest: Expected at least ${candlesCount} candles for VWAP, got ${candles.length}`);
|
|
1779
2357
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
riskName: this.params.riskName,
|
|
1784
|
-
});
|
|
1785
|
-
await this.setPendingSignal(null);
|
|
1786
|
-
const result = {
|
|
1787
|
-
action: "closed",
|
|
1788
|
-
signal: signal,
|
|
1789
|
-
currentPrice: lastPrice,
|
|
1790
|
-
closeReason: "time_expired",
|
|
1791
|
-
closeTimestamp: closeTimestamp,
|
|
1792
|
-
pnl: pnl,
|
|
1793
|
-
strategyName: this.params.method.context.strategyName,
|
|
1794
|
-
exchangeName: this.params.method.context.exchangeName,
|
|
1795
|
-
symbol: this.params.execution.context.symbol,
|
|
1796
|
-
};
|
|
1797
|
-
if (this.params.callbacks?.onTick) {
|
|
1798
|
-
this.params.callbacks.onTick(this.params.execution.context.symbol, result, this.params.execution.context.backtest);
|
|
2358
|
+
const closedResult = await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, signal, candles);
|
|
2359
|
+
if (closedResult) {
|
|
2360
|
+
return closedResult;
|
|
1799
2361
|
}
|
|
1800
|
-
|
|
2362
|
+
const lastCandles = candles.slice(-GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
2363
|
+
const lastPrice = GET_AVG_PRICE_FN(lastCandles);
|
|
2364
|
+
const closeTimestamp = lastCandles[lastCandles.length - 1].timestamp;
|
|
2365
|
+
return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(this, signal, lastPrice, "time_expired", closeTimestamp);
|
|
1801
2366
|
}
|
|
1802
2367
|
/**
|
|
1803
2368
|
* Stops the strategy from generating new signals.
|
|
1804
2369
|
*
|
|
1805
2370
|
* Sets internal flag to prevent getSignal from being called.
|
|
2371
|
+
* Clears any scheduled signals (not yet activated).
|
|
1806
2372
|
* Does NOT close active pending signals - they continue monitoring until TP/SL/time_expired.
|
|
1807
2373
|
*
|
|
1808
2374
|
* Use case: Graceful shutdown in live trading without forcing position closure.
|
|
@@ -1816,12 +2382,16 @@ class ClientStrategy {
|
|
|
1816
2382
|
* // Existing signal will continue until natural close
|
|
1817
2383
|
* ```
|
|
1818
2384
|
*/
|
|
1819
|
-
stop() {
|
|
2385
|
+
async stop() {
|
|
1820
2386
|
this.params.logger.debug("ClientStrategy stop", {
|
|
1821
2387
|
hasPendingSignal: this._pendingSignal !== null,
|
|
2388
|
+
hasScheduledSignal: this._scheduledSignal !== null,
|
|
1822
2389
|
});
|
|
1823
2390
|
this._isStopped = true;
|
|
1824
|
-
|
|
2391
|
+
// Clear scheduled signal if exists
|
|
2392
|
+
if (this._scheduledSignal) {
|
|
2393
|
+
this._scheduledSignal = null;
|
|
2394
|
+
}
|
|
1825
2395
|
}
|
|
1826
2396
|
}
|
|
1827
2397
|
|
|
@@ -2094,9 +2664,10 @@ class FrameConnectionService {
|
|
|
2094
2664
|
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
2095
2665
|
* @returns Promise resolving to { startDate: Date, endDate: Date }
|
|
2096
2666
|
*/
|
|
2097
|
-
this.getTimeframe = async (symbol) => {
|
|
2667
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
2098
2668
|
this.loggerService.log("frameConnectionService getTimeframe", {
|
|
2099
2669
|
symbol,
|
|
2670
|
+
frameName,
|
|
2100
2671
|
});
|
|
2101
2672
|
return await this.getFrame(this.methodContextService.context.frameName).getTimeframe(symbol);
|
|
2102
2673
|
};
|
|
@@ -2577,9 +3148,21 @@ class RiskConnectionService {
|
|
|
2577
3148
|
});
|
|
2578
3149
|
await this.getRisk(context.riskName).removeSignal(symbol, context);
|
|
2579
3150
|
};
|
|
3151
|
+
/**
|
|
3152
|
+
* Clears the cached ClientRisk instance for the given risk name.
|
|
3153
|
+
*
|
|
3154
|
+
* @param riskName - Name of the risk schema to clear from cache
|
|
3155
|
+
*/
|
|
3156
|
+
this.clear = async (riskName) => {
|
|
3157
|
+
this.loggerService.log("riskConnectionService clear", {
|
|
3158
|
+
riskName,
|
|
3159
|
+
});
|
|
3160
|
+
this.getRisk.clear(riskName);
|
|
3161
|
+
};
|
|
2580
3162
|
}
|
|
2581
3163
|
}
|
|
2582
3164
|
|
|
3165
|
+
const METHOD_NAME_VALIDATE$1 = "exchangeGlobalService validate";
|
|
2583
3166
|
/**
|
|
2584
3167
|
* Global service for exchange operations with execution context injection.
|
|
2585
3168
|
*
|
|
@@ -2592,6 +3175,21 @@ class ExchangeGlobalService {
|
|
|
2592
3175
|
constructor() {
|
|
2593
3176
|
this.loggerService = inject(TYPES.loggerService);
|
|
2594
3177
|
this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
|
|
3178
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3179
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
3180
|
+
/**
|
|
3181
|
+
* Validates exchange configuration.
|
|
3182
|
+
* Memoized to avoid redundant validations for the same exchange.
|
|
3183
|
+
* Logs validation activity.
|
|
3184
|
+
* @param exchangeName - Name of the exchange to validate
|
|
3185
|
+
* @returns Promise that resolves when validation is complete
|
|
3186
|
+
*/
|
|
3187
|
+
this.validate = functoolsKit.memoize(([exchangeName]) => `${exchangeName}`, async (exchangeName) => {
|
|
3188
|
+
this.loggerService.log(METHOD_NAME_VALIDATE$1, {
|
|
3189
|
+
exchangeName,
|
|
3190
|
+
});
|
|
3191
|
+
this.exchangeValidationService.validate(exchangeName, METHOD_NAME_VALIDATE$1);
|
|
3192
|
+
});
|
|
2595
3193
|
/**
|
|
2596
3194
|
* Fetches historical candles with execution context.
|
|
2597
3195
|
*
|
|
@@ -2610,6 +3208,7 @@ class ExchangeGlobalService {
|
|
|
2610
3208
|
when,
|
|
2611
3209
|
backtest,
|
|
2612
3210
|
});
|
|
3211
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2613
3212
|
return await ExecutionContextService.runInContext(async () => {
|
|
2614
3213
|
return await this.exchangeConnectionService.getCandles(symbol, interval, limit);
|
|
2615
3214
|
}, {
|
|
@@ -2636,6 +3235,7 @@ class ExchangeGlobalService {
|
|
|
2636
3235
|
when,
|
|
2637
3236
|
backtest,
|
|
2638
3237
|
});
|
|
3238
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2639
3239
|
return await ExecutionContextService.runInContext(async () => {
|
|
2640
3240
|
return await this.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
2641
3241
|
}, {
|
|
@@ -2658,6 +3258,7 @@ class ExchangeGlobalService {
|
|
|
2658
3258
|
when,
|
|
2659
3259
|
backtest,
|
|
2660
3260
|
});
|
|
3261
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2661
3262
|
return await ExecutionContextService.runInContext(async () => {
|
|
2662
3263
|
return await this.exchangeConnectionService.getAveragePrice(symbol);
|
|
2663
3264
|
}, {
|
|
@@ -2682,6 +3283,7 @@ class ExchangeGlobalService {
|
|
|
2682
3283
|
when,
|
|
2683
3284
|
backtest,
|
|
2684
3285
|
});
|
|
3286
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2685
3287
|
return await ExecutionContextService.runInContext(async () => {
|
|
2686
3288
|
return await this.exchangeConnectionService.formatPrice(symbol, price);
|
|
2687
3289
|
}, {
|
|
@@ -2706,6 +3308,7 @@ class ExchangeGlobalService {
|
|
|
2706
3308
|
when,
|
|
2707
3309
|
backtest,
|
|
2708
3310
|
});
|
|
3311
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
2709
3312
|
return await ExecutionContextService.runInContext(async () => {
|
|
2710
3313
|
return await this.exchangeConnectionService.formatQuantity(symbol, quantity);
|
|
2711
3314
|
}, {
|
|
@@ -2717,6 +3320,7 @@ class ExchangeGlobalService {
|
|
|
2717
3320
|
}
|
|
2718
3321
|
}
|
|
2719
3322
|
|
|
3323
|
+
const METHOD_NAME_VALIDATE = "strategyGlobalService validate";
|
|
2720
3324
|
/**
|
|
2721
3325
|
* Global service for strategy operations with execution context injection.
|
|
2722
3326
|
*
|
|
@@ -2729,6 +3333,28 @@ class StrategyGlobalService {
|
|
|
2729
3333
|
constructor() {
|
|
2730
3334
|
this.loggerService = inject(TYPES.loggerService);
|
|
2731
3335
|
this.strategyConnectionService = inject(TYPES.strategyConnectionService);
|
|
3336
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
3337
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3338
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3339
|
+
this.methodContextService = inject(TYPES.methodContextService);
|
|
3340
|
+
/**
|
|
3341
|
+
* Validates strategy and associated risk configuration.
|
|
3342
|
+
*
|
|
3343
|
+
* Memoized to avoid redundant validations for the same strategy.
|
|
3344
|
+
* Logs validation activity.
|
|
3345
|
+
* @param strategyName - Name of the strategy to validate
|
|
3346
|
+
* @returns Promise that resolves when validation is complete
|
|
3347
|
+
*/
|
|
3348
|
+
this.validate = functoolsKit.memoize(([strategyName]) => `${strategyName}`, async (strategyName) => {
|
|
3349
|
+
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
3350
|
+
strategyName,
|
|
3351
|
+
});
|
|
3352
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
3353
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_VALIDATE);
|
|
3354
|
+
const riskName = strategySchema.riskName;
|
|
3355
|
+
riskName &&
|
|
3356
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
|
|
3357
|
+
});
|
|
2732
3358
|
/**
|
|
2733
3359
|
* Checks signal status at a specific timestamp.
|
|
2734
3360
|
*
|
|
@@ -2746,6 +3372,7 @@ class StrategyGlobalService {
|
|
|
2746
3372
|
when,
|
|
2747
3373
|
backtest,
|
|
2748
3374
|
});
|
|
3375
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2749
3376
|
return await ExecutionContextService.runInContext(async () => {
|
|
2750
3377
|
return await this.strategyConnectionService.tick();
|
|
2751
3378
|
}, {
|
|
@@ -2773,6 +3400,7 @@ class StrategyGlobalService {
|
|
|
2773
3400
|
when,
|
|
2774
3401
|
backtest,
|
|
2775
3402
|
});
|
|
3403
|
+
await this.validate(this.methodContextService.context.strategyName);
|
|
2776
3404
|
return await ExecutionContextService.runInContext(async () => {
|
|
2777
3405
|
return await this.strategyConnectionService.backtest(candles);
|
|
2778
3406
|
}, {
|
|
@@ -2794,6 +3422,7 @@ class StrategyGlobalService {
|
|
|
2794
3422
|
this.loggerService.log("strategyGlobalService stop", {
|
|
2795
3423
|
strategyName,
|
|
2796
3424
|
});
|
|
3425
|
+
await this.validate(strategyName);
|
|
2797
3426
|
return await this.strategyConnectionService.stop(strategyName);
|
|
2798
3427
|
};
|
|
2799
3428
|
/**
|
|
@@ -2808,11 +3437,15 @@ class StrategyGlobalService {
|
|
|
2808
3437
|
this.loggerService.log("strategyGlobalService clear", {
|
|
2809
3438
|
strategyName,
|
|
2810
3439
|
});
|
|
3440
|
+
if (strategyName) {
|
|
3441
|
+
await this.validate(strategyName);
|
|
3442
|
+
}
|
|
2811
3443
|
return await this.strategyConnectionService.clear(strategyName);
|
|
2812
3444
|
};
|
|
2813
3445
|
}
|
|
2814
3446
|
}
|
|
2815
3447
|
|
|
3448
|
+
const METHOD_NAME_GET_TIMEFRAME = "frameGlobalService getTimeframe";
|
|
2816
3449
|
/**
|
|
2817
3450
|
* Global service for frame operations.
|
|
2818
3451
|
*
|
|
@@ -2823,21 +3456,25 @@ class FrameGlobalService {
|
|
|
2823
3456
|
constructor() {
|
|
2824
3457
|
this.loggerService = inject(TYPES.loggerService);
|
|
2825
3458
|
this.frameConnectionService = inject(TYPES.frameConnectionService);
|
|
3459
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
2826
3460
|
/**
|
|
2827
3461
|
* Generates timeframe array for backtest iteration.
|
|
2828
3462
|
*
|
|
2829
|
-
* @param
|
|
3463
|
+
* @param frameName - Target frame name (e.g., "1m", "1h")
|
|
2830
3464
|
* @returns Promise resolving to array of Date objects
|
|
2831
3465
|
*/
|
|
2832
|
-
this.getTimeframe = async (symbol) => {
|
|
2833
|
-
this.loggerService.log(
|
|
3466
|
+
this.getTimeframe = async (symbol, frameName) => {
|
|
3467
|
+
this.loggerService.log(METHOD_NAME_GET_TIMEFRAME, {
|
|
3468
|
+
frameName,
|
|
2834
3469
|
symbol,
|
|
2835
3470
|
});
|
|
2836
|
-
|
|
3471
|
+
this.frameValidationService.validate(frameName, METHOD_NAME_GET_TIMEFRAME);
|
|
3472
|
+
return await this.frameConnectionService.getTimeframe(symbol, frameName);
|
|
2837
3473
|
};
|
|
2838
3474
|
}
|
|
2839
3475
|
}
|
|
2840
3476
|
|
|
3477
|
+
const METHOD_NAME_CALCULATE = "sizingGlobalService calculate";
|
|
2841
3478
|
/**
|
|
2842
3479
|
* Global service for sizing operations.
|
|
2843
3480
|
*
|
|
@@ -2848,6 +3485,7 @@ class SizingGlobalService {
|
|
|
2848
3485
|
constructor() {
|
|
2849
3486
|
this.loggerService = inject(TYPES.loggerService);
|
|
2850
3487
|
this.sizingConnectionService = inject(TYPES.sizingConnectionService);
|
|
3488
|
+
this.sizingValidationService = inject(TYPES.sizingValidationService);
|
|
2851
3489
|
/**
|
|
2852
3490
|
* Calculates position size based on risk parameters.
|
|
2853
3491
|
*
|
|
@@ -2856,11 +3494,12 @@ class SizingGlobalService {
|
|
|
2856
3494
|
* @returns Promise resolving to calculated position size
|
|
2857
3495
|
*/
|
|
2858
3496
|
this.calculate = async (params, context) => {
|
|
2859
|
-
this.loggerService.log(
|
|
3497
|
+
this.loggerService.log(METHOD_NAME_CALCULATE, {
|
|
2860
3498
|
symbol: params.symbol,
|
|
2861
3499
|
method: params.method,
|
|
2862
3500
|
context,
|
|
2863
3501
|
});
|
|
3502
|
+
this.sizingValidationService.validate(context.sizingName, METHOD_NAME_CALCULATE);
|
|
2864
3503
|
return await this.sizingConnectionService.calculate(params, context);
|
|
2865
3504
|
};
|
|
2866
3505
|
}
|
|
@@ -2876,6 +3515,20 @@ class RiskGlobalService {
|
|
|
2876
3515
|
constructor() {
|
|
2877
3516
|
this.loggerService = inject(TYPES.loggerService);
|
|
2878
3517
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
3518
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3519
|
+
/**
|
|
3520
|
+
* Validates risk configuration.
|
|
3521
|
+
* Memoized to avoid redundant validations for the same risk instance.
|
|
3522
|
+
* Logs validation activity.
|
|
3523
|
+
* @param riskName - Name of the risk instance to validate
|
|
3524
|
+
* @returns Promise that resolves when validation is complete
|
|
3525
|
+
*/
|
|
3526
|
+
this.validate = functoolsKit.memoize(([riskName]) => `${riskName}`, async (riskName) => {
|
|
3527
|
+
this.loggerService.log("riskGlobalService validate", {
|
|
3528
|
+
riskName,
|
|
3529
|
+
});
|
|
3530
|
+
this.riskValidationService.validate(riskName, "riskGlobalService validate");
|
|
3531
|
+
});
|
|
2879
3532
|
/**
|
|
2880
3533
|
* Checks if a signal should be allowed based on risk limits.
|
|
2881
3534
|
*
|
|
@@ -2888,6 +3541,7 @@ class RiskGlobalService {
|
|
|
2888
3541
|
symbol: params.symbol,
|
|
2889
3542
|
context,
|
|
2890
3543
|
});
|
|
3544
|
+
await this.validate(context.riskName);
|
|
2891
3545
|
return await this.riskConnectionService.checkSignal(params, context);
|
|
2892
3546
|
};
|
|
2893
3547
|
/**
|
|
@@ -2901,6 +3555,7 @@ class RiskGlobalService {
|
|
|
2901
3555
|
symbol,
|
|
2902
3556
|
context,
|
|
2903
3557
|
});
|
|
3558
|
+
await this.validate(context.riskName);
|
|
2904
3559
|
await this.riskConnectionService.addSignal(symbol, context);
|
|
2905
3560
|
};
|
|
2906
3561
|
/**
|
|
@@ -2914,8 +3569,24 @@ class RiskGlobalService {
|
|
|
2914
3569
|
symbol,
|
|
2915
3570
|
context,
|
|
2916
3571
|
});
|
|
3572
|
+
await this.validate(context.riskName);
|
|
2917
3573
|
await this.riskConnectionService.removeSignal(symbol, context);
|
|
2918
3574
|
};
|
|
3575
|
+
/**
|
|
3576
|
+
* Clears risk data.
|
|
3577
|
+
* If riskName is provided, clears data for that specific risk instance.
|
|
3578
|
+
* If no riskName is provided, clears all risk data.
|
|
3579
|
+
* @param riskName - Optional name of the risk instance to clear
|
|
3580
|
+
*/
|
|
3581
|
+
this.clear = async (riskName) => {
|
|
3582
|
+
this.loggerService.log("riskGlobalService clear", {
|
|
3583
|
+
riskName,
|
|
3584
|
+
});
|
|
3585
|
+
if (riskName) {
|
|
3586
|
+
await this.validate(riskName);
|
|
3587
|
+
}
|
|
3588
|
+
return await this.riskConnectionService.clear(riskName);
|
|
3589
|
+
};
|
|
2919
3590
|
}
|
|
2920
3591
|
}
|
|
2921
3592
|
|
|
@@ -3426,7 +4097,7 @@ class BacktestLogicPrivateService {
|
|
|
3426
4097
|
symbol,
|
|
3427
4098
|
});
|
|
3428
4099
|
const backtestStartTime = performance.now();
|
|
3429
|
-
const timeframes = await this.frameGlobalService.getTimeframe(symbol);
|
|
4100
|
+
const timeframes = await this.frameGlobalService.getTimeframe(symbol, this.methodContextService.context.frameName);
|
|
3430
4101
|
const totalFrames = timeframes.length;
|
|
3431
4102
|
let i = 0;
|
|
3432
4103
|
let previousEventTimestamp = null;
|
|
@@ -3445,7 +4116,66 @@ class BacktestLogicPrivateService {
|
|
|
3445
4116
|
});
|
|
3446
4117
|
}
|
|
3447
4118
|
const result = await this.strategyGlobalService.tick(symbol, when, true);
|
|
3448
|
-
// Если
|
|
4119
|
+
// Если scheduled signal создан - обрабатываем через backtest()
|
|
4120
|
+
if (result.action === "scheduled") {
|
|
4121
|
+
const signalStartTime = performance.now();
|
|
4122
|
+
const signal = result.signal;
|
|
4123
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal detected", {
|
|
4124
|
+
symbol,
|
|
4125
|
+
signalId: signal.id,
|
|
4126
|
+
priceOpen: signal.priceOpen,
|
|
4127
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4128
|
+
});
|
|
4129
|
+
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4130
|
+
// КРИТИЧНО: запрашиваем CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4131
|
+
// + minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4132
|
+
// +1 потому что when включается как первая свеча (timestamp начинается с when, а не after when)
|
|
4133
|
+
const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4134
|
+
const candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, when, true);
|
|
4135
|
+
if (!candles.length) {
|
|
4136
|
+
i++;
|
|
4137
|
+
continue;
|
|
4138
|
+
}
|
|
4139
|
+
this.loggerService.info("backtestLogicPrivateService candles fetched for scheduled", {
|
|
4140
|
+
symbol,
|
|
4141
|
+
signalId: signal.id,
|
|
4142
|
+
candlesCount: candles.length,
|
|
4143
|
+
candlesNeeded,
|
|
4144
|
+
});
|
|
4145
|
+
// backtest() сам обработает scheduled signal: найдет активацию/отмену
|
|
4146
|
+
// и если активируется - продолжит с TP/SL мониторингом
|
|
4147
|
+
const backtestResult = await this.strategyGlobalService.backtest(symbol, candles, when, true);
|
|
4148
|
+
this.loggerService.info("backtestLogicPrivateService scheduled signal closed", {
|
|
4149
|
+
symbol,
|
|
4150
|
+
signalId: backtestResult.signal.id,
|
|
4151
|
+
closeTimestamp: backtestResult.closeTimestamp,
|
|
4152
|
+
action: backtestResult.action,
|
|
4153
|
+
closeReason: backtestResult.action === "closed"
|
|
4154
|
+
? backtestResult.closeReason
|
|
4155
|
+
: undefined,
|
|
4156
|
+
});
|
|
4157
|
+
// Track signal processing duration
|
|
4158
|
+
const signalEndTime = performance.now();
|
|
4159
|
+
const currentTimestamp = Date.now();
|
|
4160
|
+
await performanceEmitter.next({
|
|
4161
|
+
timestamp: currentTimestamp,
|
|
4162
|
+
previousTimestamp: previousEventTimestamp,
|
|
4163
|
+
metricType: "backtest_signal",
|
|
4164
|
+
duration: signalEndTime - signalStartTime,
|
|
4165
|
+
strategyName: this.methodContextService.context.strategyName,
|
|
4166
|
+
exchangeName: this.methodContextService.context.exchangeName,
|
|
4167
|
+
symbol,
|
|
4168
|
+
backtest: true,
|
|
4169
|
+
});
|
|
4170
|
+
previousEventTimestamp = currentTimestamp;
|
|
4171
|
+
// Пропускаем timeframes до closeTimestamp
|
|
4172
|
+
while (i < timeframes.length &&
|
|
4173
|
+
timeframes[i].getTime() < backtestResult.closeTimestamp) {
|
|
4174
|
+
i++;
|
|
4175
|
+
}
|
|
4176
|
+
yield backtestResult;
|
|
4177
|
+
}
|
|
4178
|
+
// Если обычный сигнал открыт, вызываем backtest
|
|
3449
4179
|
if (result.action === "opened") {
|
|
3450
4180
|
const signalStartTime = performance.now();
|
|
3451
4181
|
const signal = result.signal;
|
|
@@ -3470,7 +4200,6 @@ class BacktestLogicPrivateService {
|
|
|
3470
4200
|
symbol,
|
|
3471
4201
|
signalId: backtestResult.signal.id,
|
|
3472
4202
|
closeTimestamp: backtestResult.closeTimestamp,
|
|
3473
|
-
closeReason: backtestResult.closeReason,
|
|
3474
4203
|
});
|
|
3475
4204
|
// Track signal processing duration
|
|
3476
4205
|
const signalEndTime = performance.now();
|
|
@@ -3617,6 +4346,11 @@ class LiveLogicPrivateService {
|
|
|
3617
4346
|
await functoolsKit.sleep(TICK_TTL);
|
|
3618
4347
|
continue;
|
|
3619
4348
|
}
|
|
4349
|
+
if (result.action === "scheduled") {
|
|
4350
|
+
await functoolsKit.sleep(TICK_TTL);
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
// Yield opened, closed, cancelled results
|
|
3620
4354
|
yield result;
|
|
3621
4355
|
await functoolsKit.sleep(TICK_TTL);
|
|
3622
4356
|
}
|
|
@@ -3937,6 +4671,8 @@ class LiveGlobalService {
|
|
|
3937
4671
|
this.liveLogicPublicService = inject(TYPES.liveLogicPublicService);
|
|
3938
4672
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3939
4673
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4674
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4675
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3940
4676
|
/**
|
|
3941
4677
|
* Runs live trading for a symbol with context propagation.
|
|
3942
4678
|
*
|
|
@@ -3951,8 +4687,16 @@ class LiveGlobalService {
|
|
|
3951
4687
|
symbol,
|
|
3952
4688
|
context,
|
|
3953
4689
|
});
|
|
3954
|
-
|
|
3955
|
-
|
|
4690
|
+
{
|
|
4691
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$2);
|
|
4692
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$2);
|
|
4693
|
+
}
|
|
4694
|
+
{
|
|
4695
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4696
|
+
const riskName = strategySchema.riskName;
|
|
4697
|
+
riskName &&
|
|
4698
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN$2);
|
|
4699
|
+
}
|
|
3956
4700
|
return this.liveLogicPublicService.run(symbol, context);
|
|
3957
4701
|
};
|
|
3958
4702
|
}
|
|
@@ -3968,6 +4712,8 @@ const METHOD_NAME_RUN$1 = "backtestGlobalService run";
|
|
|
3968
4712
|
class BacktestGlobalService {
|
|
3969
4713
|
constructor() {
|
|
3970
4714
|
this.loggerService = inject(TYPES.loggerService);
|
|
4715
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4716
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
3971
4717
|
this.backtestLogicPublicService = inject(TYPES.backtestLogicPublicService);
|
|
3972
4718
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
3973
4719
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
@@ -3984,9 +4730,16 @@ class BacktestGlobalService {
|
|
|
3984
4730
|
symbol,
|
|
3985
4731
|
context,
|
|
3986
4732
|
});
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4733
|
+
{
|
|
4734
|
+
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_RUN$1);
|
|
4735
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN$1);
|
|
4736
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN$1);
|
|
4737
|
+
}
|
|
4738
|
+
{
|
|
4739
|
+
const strategySchema = this.strategySchemaService.get(context.strategyName);
|
|
4740
|
+
const riskName = strategySchema.riskName;
|
|
4741
|
+
riskName && this.riskValidationService.validate(riskName, METHOD_NAME_RUN$1);
|
|
4742
|
+
}
|
|
3990
4743
|
return this.backtestLogicPublicService.run(symbol, context);
|
|
3991
4744
|
};
|
|
3992
4745
|
}
|
|
@@ -4003,6 +4756,13 @@ class WalkerGlobalService {
|
|
|
4003
4756
|
constructor() {
|
|
4004
4757
|
this.loggerService = inject(TYPES.loggerService);
|
|
4005
4758
|
this.walkerLogicPublicService = inject(TYPES.walkerLogicPublicService);
|
|
4759
|
+
this.walkerSchemaService = inject(TYPES.walkerSchemaService);
|
|
4760
|
+
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
4761
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
4762
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
4763
|
+
this.walkerValidationService = inject(TYPES.walkerValidationService);
|
|
4764
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
4765
|
+
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
4006
4766
|
/**
|
|
4007
4767
|
* Runs walker comparison for a symbol with context propagation.
|
|
4008
4768
|
*
|
|
@@ -4014,6 +4774,21 @@ class WalkerGlobalService {
|
|
|
4014
4774
|
symbol,
|
|
4015
4775
|
context,
|
|
4016
4776
|
});
|
|
4777
|
+
{
|
|
4778
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_RUN);
|
|
4779
|
+
this.frameValidationService.validate(context.frameName, METHOD_NAME_RUN);
|
|
4780
|
+
this.walkerValidationService.validate(context.walkerName, METHOD_NAME_RUN);
|
|
4781
|
+
}
|
|
4782
|
+
{
|
|
4783
|
+
const walkerSchema = this.walkerSchemaService.get(context.walkerName);
|
|
4784
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
4785
|
+
const strategySchema = this.strategySchemaService.get(strategyName);
|
|
4786
|
+
this.strategyValidationService.validate(strategyName, METHOD_NAME_RUN);
|
|
4787
|
+
const riskName = strategySchema.riskName;
|
|
4788
|
+
riskName &&
|
|
4789
|
+
this.riskValidationService.validate(riskName, METHOD_NAME_RUN);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4017
4792
|
return this.walkerLogicPublicService.run(symbol, context);
|
|
4018
4793
|
};
|
|
4019
4794
|
}
|
|
@@ -4037,7 +4812,7 @@ function isUnsafe$3(value) {
|
|
|
4037
4812
|
}
|
|
4038
4813
|
return false;
|
|
4039
4814
|
}
|
|
4040
|
-
const columns$
|
|
4815
|
+
const columns$3 = [
|
|
4041
4816
|
{
|
|
4042
4817
|
key: "signalId",
|
|
4043
4818
|
label: "Signal ID",
|
|
@@ -4095,7 +4870,7 @@ const columns$2 = [
|
|
|
4095
4870
|
key: "duration",
|
|
4096
4871
|
label: "Duration (min)",
|
|
4097
4872
|
format: (data) => {
|
|
4098
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
4873
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
4099
4874
|
const durationMin = Math.round(durationMs / 60000);
|
|
4100
4875
|
return `${durationMin}`;
|
|
4101
4876
|
},
|
|
@@ -4103,7 +4878,7 @@ const columns$2 = [
|
|
|
4103
4878
|
{
|
|
4104
4879
|
key: "openTimestamp",
|
|
4105
4880
|
label: "Open Time",
|
|
4106
|
-
format: (data) => new Date(data.signal.
|
|
4881
|
+
format: (data) => new Date(data.signal.pendingAt).toISOString(),
|
|
4107
4882
|
},
|
|
4108
4883
|
{
|
|
4109
4884
|
key: "closeTimestamp",
|
|
@@ -4115,7 +4890,7 @@ const columns$2 = [
|
|
|
4115
4890
|
* Storage class for accumulating closed signals per strategy.
|
|
4116
4891
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
4117
4892
|
*/
|
|
4118
|
-
let ReportStorage$
|
|
4893
|
+
let ReportStorage$3 = class ReportStorage {
|
|
4119
4894
|
constructor() {
|
|
4120
4895
|
/** Internal list of all closed signals for this strategy */
|
|
4121
4896
|
this._signalList = [];
|
|
@@ -4175,7 +4950,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4175
4950
|
: 0;
|
|
4176
4951
|
const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4177
4952
|
// Calculate Expected Yearly Returns
|
|
4178
|
-
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.
|
|
4953
|
+
const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.pendingAt), 0) / totalSignals;
|
|
4179
4954
|
const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
|
|
4180
4955
|
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4181
4956
|
const expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
@@ -4205,9 +4980,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
4205
4980
|
if (stats.totalSignals === 0) {
|
|
4206
4981
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
4207
4982
|
}
|
|
4208
|
-
const header = columns$
|
|
4209
|
-
const separator = columns$
|
|
4210
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
4983
|
+
const header = columns$3.map((col) => col.label);
|
|
4984
|
+
const separator = columns$3.map(() => "---");
|
|
4985
|
+
const rows = this._signalList.map((closedSignal) => columns$3.map((col) => col.format(closedSignal)));
|
|
4211
4986
|
const tableData = [header, separator, ...rows];
|
|
4212
4987
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4213
4988
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
@@ -4268,7 +5043,7 @@ class BacktestMarkdownService {
|
|
|
4268
5043
|
* Memoized function to get or create ReportStorage for a strategy.
|
|
4269
5044
|
* Each strategy gets its own isolated storage instance.
|
|
4270
5045
|
*/
|
|
4271
|
-
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$
|
|
5046
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$3());
|
|
4272
5047
|
/**
|
|
4273
5048
|
* Processes tick events and accumulates closed signals.
|
|
4274
5049
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -4427,7 +5202,7 @@ function isUnsafe$2(value) {
|
|
|
4427
5202
|
}
|
|
4428
5203
|
return false;
|
|
4429
5204
|
}
|
|
4430
|
-
const columns$
|
|
5205
|
+
const columns$2 = [
|
|
4431
5206
|
{
|
|
4432
5207
|
key: "timestamp",
|
|
4433
5208
|
label: "Timestamp",
|
|
@@ -4501,12 +5276,12 @@ const columns$1 = [
|
|
|
4501
5276
|
},
|
|
4502
5277
|
];
|
|
4503
5278
|
/** Maximum number of events to store in live trading reports */
|
|
4504
|
-
const MAX_EVENTS$
|
|
5279
|
+
const MAX_EVENTS$2 = 250;
|
|
4505
5280
|
/**
|
|
4506
5281
|
* Storage class for accumulating all tick events per strategy.
|
|
4507
5282
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
4508
5283
|
*/
|
|
4509
|
-
let ReportStorage$
|
|
5284
|
+
let ReportStorage$2 = class ReportStorage {
|
|
4510
5285
|
constructor() {
|
|
4511
5286
|
/** Internal list of all tick events for this strategy */
|
|
4512
5287
|
this._eventList = [];
|
|
@@ -4534,7 +5309,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4534
5309
|
}
|
|
4535
5310
|
{
|
|
4536
5311
|
this._eventList.push(newEvent);
|
|
4537
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5312
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4538
5313
|
this._eventList.shift();
|
|
4539
5314
|
}
|
|
4540
5315
|
}
|
|
@@ -4546,7 +5321,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4546
5321
|
*/
|
|
4547
5322
|
addOpenedEvent(data) {
|
|
4548
5323
|
this._eventList.push({
|
|
4549
|
-
timestamp: data.signal.
|
|
5324
|
+
timestamp: data.signal.pendingAt,
|
|
4550
5325
|
action: "opened",
|
|
4551
5326
|
symbol: data.signal.symbol,
|
|
4552
5327
|
signalId: data.signal.id,
|
|
@@ -4558,7 +5333,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4558
5333
|
stopLoss: data.signal.priceStopLoss,
|
|
4559
5334
|
});
|
|
4560
5335
|
// Trim queue if exceeded MAX_EVENTS
|
|
4561
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5336
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4562
5337
|
this._eventList.shift();
|
|
4563
5338
|
}
|
|
4564
5339
|
}
|
|
@@ -4590,35 +5365,472 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4590
5365
|
else {
|
|
4591
5366
|
this._eventList.push(newEvent);
|
|
4592
5367
|
// Trim queue if exceeded MAX_EVENTS
|
|
4593
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
5368
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
5369
|
+
this._eventList.shift();
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
/**
|
|
5374
|
+
* Updates or adds a closed event to the storage.
|
|
5375
|
+
* Replaces the previous event with the same signalId.
|
|
5376
|
+
*
|
|
5377
|
+
* @param data - Closed tick result
|
|
5378
|
+
*/
|
|
5379
|
+
addClosedEvent(data) {
|
|
5380
|
+
const durationMs = data.closeTimestamp - data.signal.pendingAt;
|
|
5381
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
5382
|
+
// Find existing event with the same signalId
|
|
5383
|
+
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
5384
|
+
const newEvent = {
|
|
5385
|
+
timestamp: data.closeTimestamp,
|
|
5386
|
+
action: "closed",
|
|
5387
|
+
symbol: data.signal.symbol,
|
|
5388
|
+
signalId: data.signal.id,
|
|
5389
|
+
position: data.signal.position,
|
|
5390
|
+
note: data.signal.note,
|
|
5391
|
+
currentPrice: data.currentPrice,
|
|
5392
|
+
openPrice: data.signal.priceOpen,
|
|
5393
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5394
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5395
|
+
pnl: data.pnl.pnlPercentage,
|
|
5396
|
+
closeReason: data.closeReason,
|
|
5397
|
+
duration: durationMin,
|
|
5398
|
+
};
|
|
5399
|
+
// Replace existing event or add new one
|
|
5400
|
+
if (existingIndex !== -1) {
|
|
5401
|
+
this._eventList[existingIndex] = newEvent;
|
|
5402
|
+
}
|
|
5403
|
+
else {
|
|
5404
|
+
this._eventList.push(newEvent);
|
|
5405
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5406
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
4594
5407
|
this._eventList.shift();
|
|
4595
5408
|
}
|
|
4596
5409
|
}
|
|
4597
5410
|
}
|
|
4598
5411
|
/**
|
|
4599
|
-
*
|
|
5412
|
+
* Calculates statistical data from live trading events (Controller).
|
|
5413
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5414
|
+
*
|
|
5415
|
+
* @returns Statistical data (empty object if no events)
|
|
5416
|
+
*/
|
|
5417
|
+
async getData() {
|
|
5418
|
+
if (this._eventList.length === 0) {
|
|
5419
|
+
return {
|
|
5420
|
+
eventList: [],
|
|
5421
|
+
totalEvents: 0,
|
|
5422
|
+
totalClosed: 0,
|
|
5423
|
+
winCount: 0,
|
|
5424
|
+
lossCount: 0,
|
|
5425
|
+
winRate: null,
|
|
5426
|
+
avgPnl: null,
|
|
5427
|
+
totalPnl: null,
|
|
5428
|
+
stdDev: null,
|
|
5429
|
+
sharpeRatio: null,
|
|
5430
|
+
annualizedSharpeRatio: null,
|
|
5431
|
+
certaintyRatio: null,
|
|
5432
|
+
expectedYearlyReturns: null,
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
5436
|
+
const totalClosed = closedEvents.length;
|
|
5437
|
+
const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
|
|
5438
|
+
const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
|
|
5439
|
+
// Calculate basic statistics
|
|
5440
|
+
const avgPnl = totalClosed > 0
|
|
5441
|
+
? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
|
|
5442
|
+
: 0;
|
|
5443
|
+
const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
|
|
5444
|
+
const winRate = (winCount / totalClosed) * 100;
|
|
5445
|
+
// Calculate Sharpe Ratio (risk-free rate = 0)
|
|
5446
|
+
let sharpeRatio = 0;
|
|
5447
|
+
let stdDev = 0;
|
|
5448
|
+
if (totalClosed > 0) {
|
|
5449
|
+
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
5450
|
+
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
5451
|
+
stdDev = Math.sqrt(variance);
|
|
5452
|
+
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
5453
|
+
}
|
|
5454
|
+
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
5455
|
+
// Calculate Certainty Ratio
|
|
5456
|
+
let certaintyRatio = 0;
|
|
5457
|
+
if (totalClosed > 0) {
|
|
5458
|
+
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
5459
|
+
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
5460
|
+
const avgWin = wins.length > 0
|
|
5461
|
+
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
5462
|
+
: 0;
|
|
5463
|
+
const avgLoss = losses.length > 0
|
|
5464
|
+
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
5465
|
+
: 0;
|
|
5466
|
+
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
5467
|
+
}
|
|
5468
|
+
// Calculate Expected Yearly Returns
|
|
5469
|
+
let expectedYearlyReturns = 0;
|
|
5470
|
+
if (totalClosed > 0) {
|
|
5471
|
+
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
5472
|
+
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
5473
|
+
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
5474
|
+
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
5475
|
+
}
|
|
5476
|
+
return {
|
|
5477
|
+
eventList: this._eventList,
|
|
5478
|
+
totalEvents: this._eventList.length,
|
|
5479
|
+
totalClosed,
|
|
5480
|
+
winCount,
|
|
5481
|
+
lossCount,
|
|
5482
|
+
winRate: isUnsafe$2(winRate) ? null : winRate,
|
|
5483
|
+
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
5484
|
+
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
5485
|
+
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
5486
|
+
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
5487
|
+
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
5488
|
+
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
5489
|
+
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Generates markdown report with all tick events for a strategy (View).
|
|
5494
|
+
*
|
|
5495
|
+
* @param strategyName - Strategy name
|
|
5496
|
+
* @returns Markdown formatted report with all events
|
|
5497
|
+
*/
|
|
5498
|
+
async getReport(strategyName) {
|
|
5499
|
+
const stats = await this.getData();
|
|
5500
|
+
if (stats.totalEvents === 0) {
|
|
5501
|
+
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
|
|
5502
|
+
}
|
|
5503
|
+
const header = columns$2.map((col) => col.label);
|
|
5504
|
+
const separator = columns$2.map(() => "---");
|
|
5505
|
+
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
5506
|
+
const tableData = [header, separator, ...rows];
|
|
5507
|
+
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
5508
|
+
return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
|
|
5509
|
+
}
|
|
5510
|
+
/**
|
|
5511
|
+
* Saves strategy report to disk.
|
|
5512
|
+
*
|
|
5513
|
+
* @param strategyName - Strategy name
|
|
5514
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5515
|
+
*/
|
|
5516
|
+
async dump(strategyName, path$1 = "./logs/live") {
|
|
5517
|
+
const markdown = await this.getReport(strategyName);
|
|
5518
|
+
try {
|
|
5519
|
+
const dir = path.join(process.cwd(), path$1);
|
|
5520
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5521
|
+
const filename = `${strategyName}.md`;
|
|
5522
|
+
const filepath = path.join(dir, filename);
|
|
5523
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
5524
|
+
console.log(`Live trading report saved: ${filepath}`);
|
|
5525
|
+
}
|
|
5526
|
+
catch (error) {
|
|
5527
|
+
console.error(`Failed to save markdown report:`, error);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
};
|
|
5531
|
+
/**
|
|
5532
|
+
* Service for generating and saving live trading markdown reports.
|
|
5533
|
+
*
|
|
5534
|
+
* Features:
|
|
5535
|
+
* - Listens to all signal events via onTick callback
|
|
5536
|
+
* - Accumulates all events (idle, opened, active, closed) per strategy
|
|
5537
|
+
* - Generates markdown tables with detailed event information
|
|
5538
|
+
* - Provides trading statistics (win rate, average PNL)
|
|
5539
|
+
* - Saves reports to disk in logs/live/{strategyName}.md
|
|
5540
|
+
*
|
|
5541
|
+
* @example
|
|
5542
|
+
* ```typescript
|
|
5543
|
+
* const service = new LiveMarkdownService();
|
|
5544
|
+
*
|
|
5545
|
+
* // Add to strategy callbacks
|
|
5546
|
+
* addStrategy({
|
|
5547
|
+
* strategyName: "my-strategy",
|
|
5548
|
+
* callbacks: {
|
|
5549
|
+
* onTick: (symbol, result, backtest) => {
|
|
5550
|
+
* if (!backtest) {
|
|
5551
|
+
* service.tick(result);
|
|
5552
|
+
* }
|
|
5553
|
+
* }
|
|
5554
|
+
* }
|
|
5555
|
+
* });
|
|
5556
|
+
*
|
|
5557
|
+
* // Later: generate and save report
|
|
5558
|
+
* await service.dump("my-strategy");
|
|
5559
|
+
* ```
|
|
5560
|
+
*/
|
|
5561
|
+
class LiveMarkdownService {
|
|
5562
|
+
constructor() {
|
|
5563
|
+
/** Logger service for debug output */
|
|
5564
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
5565
|
+
/**
|
|
5566
|
+
* Memoized function to get or create ReportStorage for a strategy.
|
|
5567
|
+
* Each strategy gets its own isolated storage instance.
|
|
5568
|
+
*/
|
|
5569
|
+
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$2());
|
|
5570
|
+
/**
|
|
5571
|
+
* Processes tick events and accumulates all event types.
|
|
5572
|
+
* Should be called from IStrategyCallbacks.onTick.
|
|
5573
|
+
*
|
|
5574
|
+
* Processes all event types: idle, opened, active, closed.
|
|
5575
|
+
*
|
|
5576
|
+
* @param data - Tick result from strategy execution
|
|
5577
|
+
*
|
|
5578
|
+
* @example
|
|
5579
|
+
* ```typescript
|
|
5580
|
+
* const service = new LiveMarkdownService();
|
|
5581
|
+
*
|
|
5582
|
+
* callbacks: {
|
|
5583
|
+
* onTick: (symbol, result, backtest) => {
|
|
5584
|
+
* if (!backtest) {
|
|
5585
|
+
* service.tick(result);
|
|
5586
|
+
* }
|
|
5587
|
+
* }
|
|
5588
|
+
* }
|
|
5589
|
+
* ```
|
|
5590
|
+
*/
|
|
5591
|
+
this.tick = async (data) => {
|
|
5592
|
+
this.loggerService.log("liveMarkdownService tick", {
|
|
5593
|
+
data,
|
|
5594
|
+
});
|
|
5595
|
+
const storage = this.getStorage(data.strategyName);
|
|
5596
|
+
if (data.action === "idle") {
|
|
5597
|
+
storage.addIdleEvent(data.currentPrice);
|
|
5598
|
+
}
|
|
5599
|
+
else if (data.action === "opened") {
|
|
5600
|
+
storage.addOpenedEvent(data);
|
|
5601
|
+
}
|
|
5602
|
+
else if (data.action === "active") {
|
|
5603
|
+
storage.addActiveEvent(data);
|
|
5604
|
+
}
|
|
5605
|
+
else if (data.action === "closed") {
|
|
5606
|
+
storage.addClosedEvent(data);
|
|
5607
|
+
}
|
|
5608
|
+
};
|
|
5609
|
+
/**
|
|
5610
|
+
* Gets statistical data from all live trading events for a strategy.
|
|
5611
|
+
* Delegates to ReportStorage.getData().
|
|
5612
|
+
*
|
|
5613
|
+
* @param strategyName - Strategy name to get data for
|
|
5614
|
+
* @returns Statistical data object with all metrics
|
|
5615
|
+
*
|
|
5616
|
+
* @example
|
|
5617
|
+
* ```typescript
|
|
5618
|
+
* const service = new LiveMarkdownService();
|
|
5619
|
+
* const stats = await service.getData("my-strategy");
|
|
5620
|
+
* console.log(stats.sharpeRatio, stats.winRate);
|
|
5621
|
+
* ```
|
|
5622
|
+
*/
|
|
5623
|
+
this.getData = async (strategyName) => {
|
|
5624
|
+
this.loggerService.log("liveMarkdownService getData", {
|
|
5625
|
+
strategyName,
|
|
5626
|
+
});
|
|
5627
|
+
const storage = this.getStorage(strategyName);
|
|
5628
|
+
return storage.getData();
|
|
5629
|
+
};
|
|
5630
|
+
/**
|
|
5631
|
+
* Generates markdown report with all events for a strategy.
|
|
5632
|
+
* Delegates to ReportStorage.getReport().
|
|
5633
|
+
*
|
|
5634
|
+
* @param strategyName - Strategy name to generate report for
|
|
5635
|
+
* @returns Markdown formatted report string with table of all events
|
|
5636
|
+
*
|
|
5637
|
+
* @example
|
|
5638
|
+
* ```typescript
|
|
5639
|
+
* const service = new LiveMarkdownService();
|
|
5640
|
+
* const markdown = await service.getReport("my-strategy");
|
|
5641
|
+
* console.log(markdown);
|
|
5642
|
+
* ```
|
|
5643
|
+
*/
|
|
5644
|
+
this.getReport = async (strategyName) => {
|
|
5645
|
+
this.loggerService.log("liveMarkdownService getReport", {
|
|
5646
|
+
strategyName,
|
|
5647
|
+
});
|
|
5648
|
+
const storage = this.getStorage(strategyName);
|
|
5649
|
+
return storage.getReport(strategyName);
|
|
5650
|
+
};
|
|
5651
|
+
/**
|
|
5652
|
+
* Saves strategy report to disk.
|
|
5653
|
+
* Creates directory if it doesn't exist.
|
|
5654
|
+
* Delegates to ReportStorage.dump().
|
|
5655
|
+
*
|
|
5656
|
+
* @param strategyName - Strategy name to save report for
|
|
5657
|
+
* @param path - Directory path to save report (default: "./logs/live")
|
|
5658
|
+
*
|
|
5659
|
+
* @example
|
|
5660
|
+
* ```typescript
|
|
5661
|
+
* const service = new LiveMarkdownService();
|
|
5662
|
+
*
|
|
5663
|
+
* // Save to default path: ./logs/live/my-strategy.md
|
|
5664
|
+
* await service.dump("my-strategy");
|
|
5665
|
+
*
|
|
5666
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
5667
|
+
* await service.dump("my-strategy", "./custom/path");
|
|
5668
|
+
* ```
|
|
5669
|
+
*/
|
|
5670
|
+
this.dump = async (strategyName, path = "./logs/live") => {
|
|
5671
|
+
this.loggerService.log("liveMarkdownService dump", {
|
|
5672
|
+
strategyName,
|
|
5673
|
+
path,
|
|
5674
|
+
});
|
|
5675
|
+
const storage = this.getStorage(strategyName);
|
|
5676
|
+
await storage.dump(strategyName, path);
|
|
5677
|
+
};
|
|
5678
|
+
/**
|
|
5679
|
+
* Clears accumulated event data from storage.
|
|
5680
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
5681
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
5682
|
+
*
|
|
5683
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
5684
|
+
*
|
|
5685
|
+
* @example
|
|
5686
|
+
* ```typescript
|
|
5687
|
+
* const service = new LiveMarkdownService();
|
|
5688
|
+
*
|
|
5689
|
+
* // Clear specific strategy data
|
|
5690
|
+
* await service.clear("my-strategy");
|
|
5691
|
+
*
|
|
5692
|
+
* // Clear all strategies' data
|
|
5693
|
+
* await service.clear();
|
|
5694
|
+
* ```
|
|
5695
|
+
*/
|
|
5696
|
+
this.clear = async (strategyName) => {
|
|
5697
|
+
this.loggerService.log("liveMarkdownService clear", {
|
|
5698
|
+
strategyName,
|
|
5699
|
+
});
|
|
5700
|
+
this.getStorage.clear(strategyName);
|
|
5701
|
+
};
|
|
5702
|
+
/**
|
|
5703
|
+
* Initializes the service by subscribing to live signal events.
|
|
5704
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
5705
|
+
* Automatically called on first use.
|
|
5706
|
+
*
|
|
5707
|
+
* @example
|
|
5708
|
+
* ```typescript
|
|
5709
|
+
* const service = new LiveMarkdownService();
|
|
5710
|
+
* await service.init(); // Subscribe to live events
|
|
5711
|
+
* ```
|
|
5712
|
+
*/
|
|
5713
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
5714
|
+
this.loggerService.log("liveMarkdownService init");
|
|
5715
|
+
signalLiveEmitter.subscribe(this.tick);
|
|
5716
|
+
});
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
const columns$1 = [
|
|
5721
|
+
{
|
|
5722
|
+
key: "timestamp",
|
|
5723
|
+
label: "Timestamp",
|
|
5724
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
5725
|
+
},
|
|
5726
|
+
{
|
|
5727
|
+
key: "action",
|
|
5728
|
+
label: "Action",
|
|
5729
|
+
format: (data) => data.action.toUpperCase(),
|
|
5730
|
+
},
|
|
5731
|
+
{
|
|
5732
|
+
key: "symbol",
|
|
5733
|
+
label: "Symbol",
|
|
5734
|
+
format: (data) => data.symbol,
|
|
5735
|
+
},
|
|
5736
|
+
{
|
|
5737
|
+
key: "signalId",
|
|
5738
|
+
label: "Signal ID",
|
|
5739
|
+
format: (data) => data.signalId,
|
|
5740
|
+
},
|
|
5741
|
+
{
|
|
5742
|
+
key: "position",
|
|
5743
|
+
label: "Position",
|
|
5744
|
+
format: (data) => data.position.toUpperCase(),
|
|
5745
|
+
},
|
|
5746
|
+
{
|
|
5747
|
+
key: "note",
|
|
5748
|
+
label: "Note",
|
|
5749
|
+
format: (data) => data.note ?? "N/A",
|
|
5750
|
+
},
|
|
5751
|
+
{
|
|
5752
|
+
key: "currentPrice",
|
|
5753
|
+
label: "Current Price",
|
|
5754
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
5755
|
+
},
|
|
5756
|
+
{
|
|
5757
|
+
key: "priceOpen",
|
|
5758
|
+
label: "Entry Price",
|
|
5759
|
+
format: (data) => `${data.priceOpen.toFixed(8)} USD`,
|
|
5760
|
+
},
|
|
5761
|
+
{
|
|
5762
|
+
key: "takeProfit",
|
|
5763
|
+
label: "Take Profit",
|
|
5764
|
+
format: (data) => `${data.takeProfit.toFixed(8)} USD`,
|
|
5765
|
+
},
|
|
5766
|
+
{
|
|
5767
|
+
key: "stopLoss",
|
|
5768
|
+
label: "Stop Loss",
|
|
5769
|
+
format: (data) => `${data.stopLoss.toFixed(8)} USD`,
|
|
5770
|
+
},
|
|
5771
|
+
{
|
|
5772
|
+
key: "duration",
|
|
5773
|
+
label: "Wait Time (min)",
|
|
5774
|
+
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
5775
|
+
},
|
|
5776
|
+
];
|
|
5777
|
+
/** Maximum number of events to store in schedule reports */
|
|
5778
|
+
const MAX_EVENTS$1 = 250;
|
|
5779
|
+
/**
|
|
5780
|
+
* Storage class for accumulating scheduled signal events per strategy.
|
|
5781
|
+
* Maintains a chronological list of scheduled and cancelled events.
|
|
5782
|
+
*/
|
|
5783
|
+
let ReportStorage$1 = class ReportStorage {
|
|
5784
|
+
constructor() {
|
|
5785
|
+
/** Internal list of all scheduled events for this strategy */
|
|
5786
|
+
this._eventList = [];
|
|
5787
|
+
}
|
|
5788
|
+
/**
|
|
5789
|
+
* Adds a scheduled event to the storage.
|
|
5790
|
+
*
|
|
5791
|
+
* @param data - Scheduled tick result
|
|
5792
|
+
*/
|
|
5793
|
+
addScheduledEvent(data) {
|
|
5794
|
+
this._eventList.push({
|
|
5795
|
+
timestamp: data.signal.scheduledAt,
|
|
5796
|
+
action: "scheduled",
|
|
5797
|
+
symbol: data.signal.symbol,
|
|
5798
|
+
signalId: data.signal.id,
|
|
5799
|
+
position: data.signal.position,
|
|
5800
|
+
note: data.signal.note,
|
|
5801
|
+
currentPrice: data.currentPrice,
|
|
5802
|
+
priceOpen: data.signal.priceOpen,
|
|
5803
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
5804
|
+
stopLoss: data.signal.priceStopLoss,
|
|
5805
|
+
});
|
|
5806
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
5807
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
5808
|
+
this._eventList.shift();
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Updates or adds a cancelled event to the storage.
|
|
4600
5813
|
* Replaces the previous event with the same signalId.
|
|
4601
5814
|
*
|
|
4602
|
-
* @param data -
|
|
5815
|
+
* @param data - Cancelled tick result
|
|
4603
5816
|
*/
|
|
4604
|
-
|
|
4605
|
-
const durationMs = data.closeTimestamp - data.signal.
|
|
5817
|
+
addCancelledEvent(data) {
|
|
5818
|
+
const durationMs = data.closeTimestamp - data.signal.scheduledAt;
|
|
4606
5819
|
const durationMin = Math.round(durationMs / 60000);
|
|
4607
5820
|
// Find existing event with the same signalId
|
|
4608
5821
|
const existingIndex = this._eventList.findIndex((event) => event.signalId === data.signal.id);
|
|
4609
5822
|
const newEvent = {
|
|
4610
5823
|
timestamp: data.closeTimestamp,
|
|
4611
|
-
action: "
|
|
5824
|
+
action: "cancelled",
|
|
4612
5825
|
symbol: data.signal.symbol,
|
|
4613
5826
|
signalId: data.signal.id,
|
|
4614
5827
|
position: data.signal.position,
|
|
4615
5828
|
note: data.signal.note,
|
|
4616
5829
|
currentPrice: data.currentPrice,
|
|
4617
|
-
|
|
5830
|
+
priceOpen: data.signal.priceOpen,
|
|
4618
5831
|
takeProfit: data.signal.priceTakeProfit,
|
|
4619
5832
|
stopLoss: data.signal.priceStopLoss,
|
|
4620
|
-
|
|
4621
|
-
closeReason: data.closeReason,
|
|
5833
|
+
closeTimestamp: data.closeTimestamp,
|
|
4622
5834
|
duration: durationMin,
|
|
4623
5835
|
};
|
|
4624
5836
|
// Replace existing event or add new one
|
|
@@ -4634,8 +5846,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4634
5846
|
}
|
|
4635
5847
|
}
|
|
4636
5848
|
/**
|
|
4637
|
-
* Calculates statistical data from
|
|
4638
|
-
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
5849
|
+
* Calculates statistical data from scheduled signal events (Controller).
|
|
4639
5850
|
*
|
|
4640
5851
|
* @returns Statistical data (empty object if no events)
|
|
4641
5852
|
*/
|
|
@@ -4644,78 +5855,34 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4644
5855
|
return {
|
|
4645
5856
|
eventList: [],
|
|
4646
5857
|
totalEvents: 0,
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
avgPnl: null,
|
|
4652
|
-
totalPnl: null,
|
|
4653
|
-
stdDev: null,
|
|
4654
|
-
sharpeRatio: null,
|
|
4655
|
-
annualizedSharpeRatio: null,
|
|
4656
|
-
certaintyRatio: null,
|
|
4657
|
-
expectedYearlyReturns: null,
|
|
5858
|
+
totalScheduled: 0,
|
|
5859
|
+
totalCancelled: 0,
|
|
5860
|
+
cancellationRate: null,
|
|
5861
|
+
avgWaitTime: null,
|
|
4658
5862
|
};
|
|
4659
5863
|
}
|
|
4660
|
-
const
|
|
4661
|
-
const
|
|
4662
|
-
const
|
|
4663
|
-
const
|
|
4664
|
-
// Calculate
|
|
4665
|
-
const
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
let sharpeRatio = 0;
|
|
4672
|
-
let stdDev = 0;
|
|
4673
|
-
if (totalClosed > 0) {
|
|
4674
|
-
const returns = closedEvents.map((e) => e.pnl || 0);
|
|
4675
|
-
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
|
|
4676
|
-
stdDev = Math.sqrt(variance);
|
|
4677
|
-
sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
|
|
4678
|
-
}
|
|
4679
|
-
const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
|
|
4680
|
-
// Calculate Certainty Ratio
|
|
4681
|
-
let certaintyRatio = 0;
|
|
4682
|
-
if (totalClosed > 0) {
|
|
4683
|
-
const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
|
|
4684
|
-
const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
|
|
4685
|
-
const avgWin = wins.length > 0
|
|
4686
|
-
? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
|
|
4687
|
-
: 0;
|
|
4688
|
-
const avgLoss = losses.length > 0
|
|
4689
|
-
? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
|
|
4690
|
-
: 0;
|
|
4691
|
-
certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
|
|
4692
|
-
}
|
|
4693
|
-
// Calculate Expected Yearly Returns
|
|
4694
|
-
let expectedYearlyReturns = 0;
|
|
4695
|
-
if (totalClosed > 0) {
|
|
4696
|
-
const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
|
|
4697
|
-
const avgDurationDays = avgDurationMin / (60 * 24);
|
|
4698
|
-
const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
|
|
4699
|
-
expectedYearlyReturns = avgPnl * tradesPerYear;
|
|
4700
|
-
}
|
|
5864
|
+
const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
|
|
5865
|
+
const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
|
|
5866
|
+
const totalScheduled = scheduledEvents.length;
|
|
5867
|
+
const totalCancelled = cancelledEvents.length;
|
|
5868
|
+
// Calculate cancellation rate
|
|
5869
|
+
const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
|
|
5870
|
+
// Calculate average wait time for cancelled signals
|
|
5871
|
+
const avgWaitTime = totalCancelled > 0
|
|
5872
|
+
? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
5873
|
+
totalCancelled
|
|
5874
|
+
: null;
|
|
4701
5875
|
return {
|
|
4702
5876
|
eventList: this._eventList,
|
|
4703
5877
|
totalEvents: this._eventList.length,
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
avgPnl: isUnsafe$2(avgPnl) ? null : avgPnl,
|
|
4709
|
-
totalPnl: isUnsafe$2(totalPnl) ? null : totalPnl,
|
|
4710
|
-
stdDev: isUnsafe$2(stdDev) ? null : stdDev,
|
|
4711
|
-
sharpeRatio: isUnsafe$2(sharpeRatio) ? null : sharpeRatio,
|
|
4712
|
-
annualizedSharpeRatio: isUnsafe$2(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
|
|
4713
|
-
certaintyRatio: isUnsafe$2(certaintyRatio) ? null : certaintyRatio,
|
|
4714
|
-
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
5878
|
+
totalScheduled,
|
|
5879
|
+
totalCancelled,
|
|
5880
|
+
cancellationRate,
|
|
5881
|
+
avgWaitTime,
|
|
4715
5882
|
};
|
|
4716
5883
|
}
|
|
4717
5884
|
/**
|
|
4718
|
-
* Generates markdown report with all
|
|
5885
|
+
* Generates markdown report with all scheduled events for a strategy (View).
|
|
4719
5886
|
*
|
|
4720
5887
|
* @param strategyName - Strategy name
|
|
4721
5888
|
* @returns Markdown formatted report with all events
|
|
@@ -4723,22 +5890,22 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4723
5890
|
async getReport(strategyName) {
|
|
4724
5891
|
const stats = await this.getData();
|
|
4725
5892
|
if (stats.totalEvents === 0) {
|
|
4726
|
-
return functoolsKit.str.newline(`#
|
|
5893
|
+
return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
|
|
4727
5894
|
}
|
|
4728
5895
|
const header = columns$1.map((col) => col.label);
|
|
4729
5896
|
const separator = columns$1.map(() => "---");
|
|
4730
5897
|
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
4731
5898
|
const tableData = [header, separator, ...rows];
|
|
4732
|
-
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
4733
|
-
return functoolsKit.str.newline(`#
|
|
5899
|
+
const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
|
|
5900
|
+
return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
|
|
4734
5901
|
}
|
|
4735
5902
|
/**
|
|
4736
5903
|
* Saves strategy report to disk.
|
|
4737
5904
|
*
|
|
4738
5905
|
* @param strategyName - Strategy name
|
|
4739
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
5906
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4740
5907
|
*/
|
|
4741
|
-
async dump(strategyName, path$1 = "./logs/
|
|
5908
|
+
async dump(strategyName, path$1 = "./logs/schedule") {
|
|
4742
5909
|
const markdown = await this.getReport(strategyName);
|
|
4743
5910
|
try {
|
|
4744
5911
|
const dir = path.join(process.cwd(), path$1);
|
|
@@ -4746,7 +5913,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4746
5913
|
const filename = `${strategyName}.md`;
|
|
4747
5914
|
const filepath = path.join(dir, filename);
|
|
4748
5915
|
await fs.writeFile(filepath, markdown, "utf-8");
|
|
4749
|
-
console.log(`
|
|
5916
|
+
console.log(`Scheduled signals report saved: ${filepath}`);
|
|
4750
5917
|
}
|
|
4751
5918
|
catch (error) {
|
|
4752
5919
|
console.error(`Failed to save markdown report:`, error);
|
|
@@ -4754,36 +5921,27 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
4754
5921
|
}
|
|
4755
5922
|
};
|
|
4756
5923
|
/**
|
|
4757
|
-
* Service for generating and saving
|
|
5924
|
+
* Service for generating and saving scheduled signals markdown reports.
|
|
4758
5925
|
*
|
|
4759
5926
|
* Features:
|
|
4760
|
-
* - Listens to
|
|
4761
|
-
* - Accumulates all events (
|
|
5927
|
+
* - Listens to scheduled and cancelled signal events via signalLiveEmitter
|
|
5928
|
+
* - Accumulates all events (scheduled, cancelled) per strategy
|
|
4762
5929
|
* - Generates markdown tables with detailed event information
|
|
4763
|
-
* - Provides
|
|
4764
|
-
* - Saves reports to disk in logs/
|
|
5930
|
+
* - Provides statistics (cancellation rate, average wait time)
|
|
5931
|
+
* - Saves reports to disk in logs/schedule/{strategyName}.md
|
|
4765
5932
|
*
|
|
4766
5933
|
* @example
|
|
4767
5934
|
* ```typescript
|
|
4768
|
-
* const service = new
|
|
5935
|
+
* const service = new ScheduleMarkdownService();
|
|
4769
5936
|
*
|
|
4770
|
-
* //
|
|
4771
|
-
*
|
|
4772
|
-
* strategyName: "my-strategy",
|
|
4773
|
-
* callbacks: {
|
|
4774
|
-
* onTick: (symbol, result, backtest) => {
|
|
4775
|
-
* if (!backtest) {
|
|
4776
|
-
* service.tick(result);
|
|
4777
|
-
* }
|
|
4778
|
-
* }
|
|
4779
|
-
* }
|
|
4780
|
-
* });
|
|
5937
|
+
* // Service automatically subscribes to signalLiveEmitter on init
|
|
5938
|
+
* // No manual callback setup needed
|
|
4781
5939
|
*
|
|
4782
5940
|
* // Later: generate and save report
|
|
4783
5941
|
* await service.dump("my-strategy");
|
|
4784
5942
|
* ```
|
|
4785
5943
|
*/
|
|
4786
|
-
class
|
|
5944
|
+
class ScheduleMarkdownService {
|
|
4787
5945
|
constructor() {
|
|
4788
5946
|
/** Logger service for debug output */
|
|
4789
5947
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -4793,46 +5951,33 @@ class LiveMarkdownService {
|
|
|
4793
5951
|
*/
|
|
4794
5952
|
this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new ReportStorage$1());
|
|
4795
5953
|
/**
|
|
4796
|
-
* Processes tick events and accumulates
|
|
4797
|
-
* Should be called from
|
|
5954
|
+
* Processes tick events and accumulates scheduled/cancelled events.
|
|
5955
|
+
* Should be called from signalLiveEmitter subscription.
|
|
4798
5956
|
*
|
|
4799
|
-
* Processes
|
|
5957
|
+
* Processes only scheduled and cancelled event types.
|
|
4800
5958
|
*
|
|
4801
5959
|
* @param data - Tick result from strategy execution
|
|
4802
5960
|
*
|
|
4803
5961
|
* @example
|
|
4804
5962
|
* ```typescript
|
|
4805
|
-
* const service = new
|
|
4806
|
-
*
|
|
4807
|
-
* callbacks: {
|
|
4808
|
-
* onTick: (symbol, result, backtest) => {
|
|
4809
|
-
* if (!backtest) {
|
|
4810
|
-
* service.tick(result);
|
|
4811
|
-
* }
|
|
4812
|
-
* }
|
|
4813
|
-
* }
|
|
5963
|
+
* const service = new ScheduleMarkdownService();
|
|
5964
|
+
* // Service automatically subscribes in init()
|
|
4814
5965
|
* ```
|
|
4815
5966
|
*/
|
|
4816
5967
|
this.tick = async (data) => {
|
|
4817
|
-
this.loggerService.log("
|
|
5968
|
+
this.loggerService.log("scheduleMarkdownService tick", {
|
|
4818
5969
|
data,
|
|
4819
5970
|
});
|
|
4820
5971
|
const storage = this.getStorage(data.strategyName);
|
|
4821
|
-
if (data.action === "
|
|
4822
|
-
storage.
|
|
4823
|
-
}
|
|
4824
|
-
else if (data.action === "opened") {
|
|
4825
|
-
storage.addOpenedEvent(data);
|
|
4826
|
-
}
|
|
4827
|
-
else if (data.action === "active") {
|
|
4828
|
-
storage.addActiveEvent(data);
|
|
5972
|
+
if (data.action === "scheduled") {
|
|
5973
|
+
storage.addScheduledEvent(data);
|
|
4829
5974
|
}
|
|
4830
|
-
else if (data.action === "
|
|
4831
|
-
storage.
|
|
5975
|
+
else if (data.action === "cancelled") {
|
|
5976
|
+
storage.addCancelledEvent(data);
|
|
4832
5977
|
}
|
|
4833
5978
|
};
|
|
4834
5979
|
/**
|
|
4835
|
-
* Gets statistical data from all
|
|
5980
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
4836
5981
|
* Delegates to ReportStorage.getData().
|
|
4837
5982
|
*
|
|
4838
5983
|
* @param strategyName - Strategy name to get data for
|
|
@@ -4840,20 +5985,20 @@ class LiveMarkdownService {
|
|
|
4840
5985
|
*
|
|
4841
5986
|
* @example
|
|
4842
5987
|
* ```typescript
|
|
4843
|
-
* const service = new
|
|
5988
|
+
* const service = new ScheduleMarkdownService();
|
|
4844
5989
|
* const stats = await service.getData("my-strategy");
|
|
4845
|
-
* console.log(stats.
|
|
5990
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
4846
5991
|
* ```
|
|
4847
5992
|
*/
|
|
4848
5993
|
this.getData = async (strategyName) => {
|
|
4849
|
-
this.loggerService.log("
|
|
5994
|
+
this.loggerService.log("scheduleMarkdownService getData", {
|
|
4850
5995
|
strategyName,
|
|
4851
5996
|
});
|
|
4852
5997
|
const storage = this.getStorage(strategyName);
|
|
4853
5998
|
return storage.getData();
|
|
4854
5999
|
};
|
|
4855
6000
|
/**
|
|
4856
|
-
* Generates markdown report with all events for a strategy.
|
|
6001
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
4857
6002
|
* Delegates to ReportStorage.getReport().
|
|
4858
6003
|
*
|
|
4859
6004
|
* @param strategyName - Strategy name to generate report for
|
|
@@ -4861,13 +6006,13 @@ class LiveMarkdownService {
|
|
|
4861
6006
|
*
|
|
4862
6007
|
* @example
|
|
4863
6008
|
* ```typescript
|
|
4864
|
-
* const service = new
|
|
6009
|
+
* const service = new ScheduleMarkdownService();
|
|
4865
6010
|
* const markdown = await service.getReport("my-strategy");
|
|
4866
6011
|
* console.log(markdown);
|
|
4867
6012
|
* ```
|
|
4868
6013
|
*/
|
|
4869
6014
|
this.getReport = async (strategyName) => {
|
|
4870
|
-
this.loggerService.log("
|
|
6015
|
+
this.loggerService.log("scheduleMarkdownService getReport", {
|
|
4871
6016
|
strategyName,
|
|
4872
6017
|
});
|
|
4873
6018
|
const storage = this.getStorage(strategyName);
|
|
@@ -4879,21 +6024,21 @@ class LiveMarkdownService {
|
|
|
4879
6024
|
* Delegates to ReportStorage.dump().
|
|
4880
6025
|
*
|
|
4881
6026
|
* @param strategyName - Strategy name to save report for
|
|
4882
|
-
* @param path - Directory path to save report (default: "./logs/
|
|
6027
|
+
* @param path - Directory path to save report (default: "./logs/schedule")
|
|
4883
6028
|
*
|
|
4884
6029
|
* @example
|
|
4885
6030
|
* ```typescript
|
|
4886
|
-
* const service = new
|
|
6031
|
+
* const service = new ScheduleMarkdownService();
|
|
4887
6032
|
*
|
|
4888
|
-
* // Save to default path: ./logs/
|
|
6033
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
4889
6034
|
* await service.dump("my-strategy");
|
|
4890
6035
|
*
|
|
4891
6036
|
* // Save to custom path: ./custom/path/my-strategy.md
|
|
4892
6037
|
* await service.dump("my-strategy", "./custom/path");
|
|
4893
6038
|
* ```
|
|
4894
6039
|
*/
|
|
4895
|
-
this.dump = async (strategyName, path = "./logs/
|
|
4896
|
-
this.loggerService.log("
|
|
6040
|
+
this.dump = async (strategyName, path = "./logs/schedule") => {
|
|
6041
|
+
this.loggerService.log("scheduleMarkdownService dump", {
|
|
4897
6042
|
strategyName,
|
|
4898
6043
|
path,
|
|
4899
6044
|
});
|
|
@@ -4909,7 +6054,7 @@ class LiveMarkdownService {
|
|
|
4909
6054
|
*
|
|
4910
6055
|
* @example
|
|
4911
6056
|
* ```typescript
|
|
4912
|
-
* const service = new
|
|
6057
|
+
* const service = new ScheduleMarkdownService();
|
|
4913
6058
|
*
|
|
4914
6059
|
* // Clear specific strategy data
|
|
4915
6060
|
* await service.clear("my-strategy");
|
|
@@ -4919,7 +6064,7 @@ class LiveMarkdownService {
|
|
|
4919
6064
|
* ```
|
|
4920
6065
|
*/
|
|
4921
6066
|
this.clear = async (strategyName) => {
|
|
4922
|
-
this.loggerService.log("
|
|
6067
|
+
this.loggerService.log("scheduleMarkdownService clear", {
|
|
4923
6068
|
strategyName,
|
|
4924
6069
|
});
|
|
4925
6070
|
this.getStorage.clear(strategyName);
|
|
@@ -4931,13 +6076,13 @@ class LiveMarkdownService {
|
|
|
4931
6076
|
*
|
|
4932
6077
|
* @example
|
|
4933
6078
|
* ```typescript
|
|
4934
|
-
* const service = new
|
|
6079
|
+
* const service = new ScheduleMarkdownService();
|
|
4935
6080
|
* await service.init(); // Subscribe to live events
|
|
4936
6081
|
* ```
|
|
4937
6082
|
*/
|
|
4938
6083
|
this.init = functoolsKit.singleshot(async () => {
|
|
4939
|
-
this.loggerService.log("
|
|
4940
|
-
|
|
6084
|
+
this.loggerService.log("scheduleMarkdownService init");
|
|
6085
|
+
signalEmitter.subscribe(this.tick);
|
|
4941
6086
|
});
|
|
4942
6087
|
}
|
|
4943
6088
|
}
|
|
@@ -6501,6 +7646,7 @@ class RiskValidationService {
|
|
|
6501
7646
|
{
|
|
6502
7647
|
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
|
|
6503
7648
|
provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
|
|
7649
|
+
provide(TYPES.scheduleMarkdownService, () => new ScheduleMarkdownService());
|
|
6504
7650
|
provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
|
|
6505
7651
|
provide(TYPES.walkerMarkdownService, () => new WalkerMarkdownService());
|
|
6506
7652
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
@@ -6559,6 +7705,7 @@ const logicPublicServices = {
|
|
|
6559
7705
|
const markdownServices = {
|
|
6560
7706
|
backtestMarkdownService: inject(TYPES.backtestMarkdownService),
|
|
6561
7707
|
liveMarkdownService: inject(TYPES.liveMarkdownService),
|
|
7708
|
+
scheduleMarkdownService: inject(TYPES.scheduleMarkdownService),
|
|
6562
7709
|
performanceMarkdownService: inject(TYPES.performanceMarkdownService),
|
|
6563
7710
|
walkerMarkdownService: inject(TYPES.walkerMarkdownService),
|
|
6564
7711
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
@@ -6605,6 +7752,20 @@ var backtest$1 = backtest;
|
|
|
6605
7752
|
async function setLogger(logger) {
|
|
6606
7753
|
backtest$1.loggerService.setLogger(logger);
|
|
6607
7754
|
}
|
|
7755
|
+
/**
|
|
7756
|
+
* Sets global configuration parameters for the framework.
|
|
7757
|
+
* @param config - Partial configuration object to override default settings
|
|
7758
|
+
*
|
|
7759
|
+
* @example
|
|
7760
|
+
* ```typescript
|
|
7761
|
+
* setConfig({
|
|
7762
|
+
* CC_SCHEDULE_AWAIT_MINUTES: 90,
|
|
7763
|
+
* });
|
|
7764
|
+
* ```
|
|
7765
|
+
*/
|
|
7766
|
+
async function setConfig(config) {
|
|
7767
|
+
Object.assign(GLOBAL_CONFIG, config);
|
|
7768
|
+
}
|
|
6608
7769
|
|
|
6609
7770
|
const ADD_STRATEGY_METHOD_NAME = "add.addStrategy";
|
|
6610
7771
|
const ADD_EXCHANGE_METHOD_NAME = "add.addExchange";
|
|
@@ -7887,8 +9048,17 @@ class BacktestUtils {
|
|
|
7887
9048
|
symbol,
|
|
7888
9049
|
context,
|
|
7889
9050
|
});
|
|
7890
|
-
|
|
7891
|
-
|
|
9051
|
+
{
|
|
9052
|
+
backtest$1.backtestMarkdownService.clear(context.strategyName);
|
|
9053
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9054
|
+
}
|
|
9055
|
+
{
|
|
9056
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9057
|
+
}
|
|
9058
|
+
{
|
|
9059
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9060
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9061
|
+
}
|
|
7892
9062
|
return backtest$1.backtestGlobalService.run(symbol, context);
|
|
7893
9063
|
};
|
|
7894
9064
|
/**
|
|
@@ -8067,8 +9237,17 @@ class LiveUtils {
|
|
|
8067
9237
|
symbol,
|
|
8068
9238
|
context,
|
|
8069
9239
|
});
|
|
8070
|
-
|
|
8071
|
-
|
|
9240
|
+
{
|
|
9241
|
+
backtest$1.liveMarkdownService.clear(context.strategyName);
|
|
9242
|
+
backtest$1.scheduleMarkdownService.clear(context.strategyName);
|
|
9243
|
+
}
|
|
9244
|
+
{
|
|
9245
|
+
backtest$1.strategyGlobalService.clear(context.strategyName);
|
|
9246
|
+
}
|
|
9247
|
+
{
|
|
9248
|
+
const { riskName } = backtest$1.strategySchemaService.get(context.strategyName);
|
|
9249
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9250
|
+
}
|
|
8072
9251
|
return backtest$1.liveGlobalService.run(symbol, context);
|
|
8073
9252
|
};
|
|
8074
9253
|
/**
|
|
@@ -8194,6 +9373,132 @@ class LiveUtils {
|
|
|
8194
9373
|
*/
|
|
8195
9374
|
const Live = new LiveUtils();
|
|
8196
9375
|
|
|
9376
|
+
const SCHEDULE_METHOD_NAME_GET_DATA = "ScheduleUtils.getData";
|
|
9377
|
+
const SCHEDULE_METHOD_NAME_GET_REPORT = "ScheduleUtils.getReport";
|
|
9378
|
+
const SCHEDULE_METHOD_NAME_DUMP = "ScheduleUtils.dump";
|
|
9379
|
+
const SCHEDULE_METHOD_NAME_CLEAR = "ScheduleUtils.clear";
|
|
9380
|
+
/**
|
|
9381
|
+
* Utility class for scheduled signals reporting operations.
|
|
9382
|
+
*
|
|
9383
|
+
* Provides simplified access to scheduleMarkdownService with logging.
|
|
9384
|
+
* Exported as singleton instance for convenient usage.
|
|
9385
|
+
*
|
|
9386
|
+
* Features:
|
|
9387
|
+
* - Track scheduled signals in queue
|
|
9388
|
+
* - Track cancelled signals
|
|
9389
|
+
* - Calculate cancellation rate and average wait time
|
|
9390
|
+
* - Generate markdown reports
|
|
9391
|
+
*
|
|
9392
|
+
* @example
|
|
9393
|
+
* ```typescript
|
|
9394
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9395
|
+
*
|
|
9396
|
+
* // Get scheduled signals statistics
|
|
9397
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9398
|
+
* console.log(`Cancellation rate: ${stats.cancellationRate}%`);
|
|
9399
|
+
* console.log(`Average wait time: ${stats.avgWaitTime} minutes`);
|
|
9400
|
+
*
|
|
9401
|
+
* // Generate and save report
|
|
9402
|
+
* await Schedule.dump("my-strategy");
|
|
9403
|
+
* ```
|
|
9404
|
+
*/
|
|
9405
|
+
class ScheduleUtils {
|
|
9406
|
+
constructor() {
|
|
9407
|
+
/**
|
|
9408
|
+
* Gets statistical data from all scheduled signal events for a strategy.
|
|
9409
|
+
*
|
|
9410
|
+
* @param strategyName - Strategy name to get data for
|
|
9411
|
+
* @returns Promise resolving to statistical data object
|
|
9412
|
+
*
|
|
9413
|
+
* @example
|
|
9414
|
+
* ```typescript
|
|
9415
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9416
|
+
* console.log(stats.cancellationRate, stats.avgWaitTime);
|
|
9417
|
+
* ```
|
|
9418
|
+
*/
|
|
9419
|
+
this.getData = async (strategyName) => {
|
|
9420
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_DATA, {
|
|
9421
|
+
strategyName,
|
|
9422
|
+
});
|
|
9423
|
+
return await backtest$1.scheduleMarkdownService.getData(strategyName);
|
|
9424
|
+
};
|
|
9425
|
+
/**
|
|
9426
|
+
* Generates markdown report with all scheduled events for a strategy.
|
|
9427
|
+
*
|
|
9428
|
+
* @param strategyName - Strategy name to generate report for
|
|
9429
|
+
* @returns Promise resolving to markdown formatted report string
|
|
9430
|
+
*
|
|
9431
|
+
* @example
|
|
9432
|
+
* ```typescript
|
|
9433
|
+
* const markdown = await Schedule.getReport("my-strategy");
|
|
9434
|
+
* console.log(markdown);
|
|
9435
|
+
* ```
|
|
9436
|
+
*/
|
|
9437
|
+
this.getReport = async (strategyName) => {
|
|
9438
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_GET_REPORT, {
|
|
9439
|
+
strategyName,
|
|
9440
|
+
});
|
|
9441
|
+
return await backtest$1.scheduleMarkdownService.getReport(strategyName);
|
|
9442
|
+
};
|
|
9443
|
+
/**
|
|
9444
|
+
* Saves strategy report to disk.
|
|
9445
|
+
*
|
|
9446
|
+
* @param strategyName - Strategy name to save report for
|
|
9447
|
+
* @param path - Optional directory path to save report (default: "./logs/schedule")
|
|
9448
|
+
*
|
|
9449
|
+
* @example
|
|
9450
|
+
* ```typescript
|
|
9451
|
+
* // Save to default path: ./logs/schedule/my-strategy.md
|
|
9452
|
+
* await Schedule.dump("my-strategy");
|
|
9453
|
+
*
|
|
9454
|
+
* // Save to custom path: ./custom/path/my-strategy.md
|
|
9455
|
+
* await Schedule.dump("my-strategy", "./custom/path");
|
|
9456
|
+
* ```
|
|
9457
|
+
*/
|
|
9458
|
+
this.dump = async (strategyName, path) => {
|
|
9459
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_DUMP, {
|
|
9460
|
+
strategyName,
|
|
9461
|
+
path,
|
|
9462
|
+
});
|
|
9463
|
+
await backtest$1.scheduleMarkdownService.dump(strategyName, path);
|
|
9464
|
+
};
|
|
9465
|
+
/**
|
|
9466
|
+
* Clears accumulated scheduled signal data from storage.
|
|
9467
|
+
* If strategyName is provided, clears only that strategy's data.
|
|
9468
|
+
* If strategyName is omitted, clears all strategies' data.
|
|
9469
|
+
*
|
|
9470
|
+
* @param strategyName - Optional strategy name to clear specific strategy data
|
|
9471
|
+
*
|
|
9472
|
+
* @example
|
|
9473
|
+
* ```typescript
|
|
9474
|
+
* // Clear specific strategy data
|
|
9475
|
+
* await Schedule.clear("my-strategy");
|
|
9476
|
+
*
|
|
9477
|
+
* // Clear all strategies' data
|
|
9478
|
+
* await Schedule.clear();
|
|
9479
|
+
* ```
|
|
9480
|
+
*/
|
|
9481
|
+
this.clear = async (strategyName) => {
|
|
9482
|
+
backtest$1.loggerService.info(SCHEDULE_METHOD_NAME_CLEAR, {
|
|
9483
|
+
strategyName,
|
|
9484
|
+
});
|
|
9485
|
+
await backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9486
|
+
};
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
/**
|
|
9490
|
+
* Singleton instance of ScheduleUtils for convenient scheduled signals reporting.
|
|
9491
|
+
*
|
|
9492
|
+
* @example
|
|
9493
|
+
* ```typescript
|
|
9494
|
+
* import { Schedule } from "./classes/Schedule";
|
|
9495
|
+
*
|
|
9496
|
+
* const stats = await Schedule.getData("my-strategy");
|
|
9497
|
+
* console.log("Cancellation rate:", stats.cancellationRate);
|
|
9498
|
+
* ```
|
|
9499
|
+
*/
|
|
9500
|
+
const Schedule = new ScheduleUtils();
|
|
9501
|
+
|
|
8197
9502
|
/**
|
|
8198
9503
|
* Performance class provides static methods for performance metrics analysis.
|
|
8199
9504
|
*
|
|
@@ -8369,8 +9674,17 @@ class WalkerUtils {
|
|
|
8369
9674
|
backtest$1.walkerMarkdownService.clear(context.walkerName);
|
|
8370
9675
|
// Clear backtest data for all strategies
|
|
8371
9676
|
for (const strategyName of walkerSchema.strategies) {
|
|
8372
|
-
|
|
8373
|
-
|
|
9677
|
+
{
|
|
9678
|
+
backtest$1.backtestMarkdownService.clear(strategyName);
|
|
9679
|
+
backtest$1.scheduleMarkdownService.clear(strategyName);
|
|
9680
|
+
}
|
|
9681
|
+
{
|
|
9682
|
+
backtest$1.strategyGlobalService.clear(strategyName);
|
|
9683
|
+
}
|
|
9684
|
+
{
|
|
9685
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
9686
|
+
riskName && backtest$1.riskGlobalService.clear(riskName);
|
|
9687
|
+
}
|
|
8374
9688
|
}
|
|
8375
9689
|
return backtest$1.walkerGlobalService.run(symbol, {
|
|
8376
9690
|
walkerName: context.walkerName,
|
|
@@ -8420,6 +9734,9 @@ class WalkerUtils {
|
|
|
8420
9734
|
task().catch((error) => errorEmitter.next(new Error(functoolsKit.getErrorMessage(error))));
|
|
8421
9735
|
return () => {
|
|
8422
9736
|
isStopped = true;
|
|
9737
|
+
for (const strategyName of walkerSchema.strategies) {
|
|
9738
|
+
backtest$1.strategyGlobalService.stop(strategyName);
|
|
9739
|
+
}
|
|
8423
9740
|
};
|
|
8424
9741
|
};
|
|
8425
9742
|
/**
|
|
@@ -8785,6 +10102,7 @@ exports.PersistBase = PersistBase;
|
|
|
8785
10102
|
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
8786
10103
|
exports.PersistSignalAdaper = PersistSignalAdaper;
|
|
8787
10104
|
exports.PositionSize = PositionSize;
|
|
10105
|
+
exports.Schedule = Schedule;
|
|
8788
10106
|
exports.Walker = Walker;
|
|
8789
10107
|
exports.addExchange = addExchange;
|
|
8790
10108
|
exports.addFrame = addFrame;
|
|
@@ -8825,4 +10143,5 @@ exports.listenValidation = listenValidation;
|
|
|
8825
10143
|
exports.listenWalker = listenWalker;
|
|
8826
10144
|
exports.listenWalkerComplete = listenWalkerComplete;
|
|
8827
10145
|
exports.listenWalkerOnce = listenWalkerOnce;
|
|
10146
|
+
exports.setConfig = setConfig;
|
|
8828
10147
|
exports.setLogger = setLogger;
|