backtest-kit 1.5.0 → 1.5.2
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/build/index.cjs +304 -69
- package/build/index.mjs +304 -69
- package/package.json +1 -1
- package/types.d.ts +23 -0
package/build/index.cjs
CHANGED
|
@@ -25,7 +25,13 @@ const GLOBAL_CONFIG = {
|
|
|
25
25
|
* Must be greater than trading fees to ensure profitable trades
|
|
26
26
|
* Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
|
|
27
27
|
*/
|
|
28
|
-
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.
|
|
28
|
+
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
|
|
29
|
+
/**
|
|
30
|
+
* Minimum StopLoss distance from priceOpen (percentage)
|
|
31
|
+
* Prevents signals from being immediately stopped out due to price volatility
|
|
32
|
+
* Default: 0.5% (buffer to avoid instant stop loss on normal market fluctuations)
|
|
33
|
+
*/
|
|
34
|
+
CC_MIN_STOPLOSS_DISTANCE_PERCENT: 0.5,
|
|
29
35
|
/**
|
|
30
36
|
* Maximum StopLoss distance from priceOpen (percentage)
|
|
31
37
|
* Prevents catastrophic losses from extreme StopLoss values
|
|
@@ -38,6 +44,15 @@ const GLOBAL_CONFIG = {
|
|
|
38
44
|
* Default: 1440 minutes (1 day)
|
|
39
45
|
*/
|
|
40
46
|
CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
|
|
47
|
+
/**
|
|
48
|
+
* Maximum time allowed for signal generation (in seconds).
|
|
49
|
+
* Prevents long-running or stuck signal generation routines from blocking
|
|
50
|
+
* execution or consuming resources indefinitely. If generation exceeds this
|
|
51
|
+
* threshold the attempt should be aborted, logged and optionally retried.
|
|
52
|
+
*
|
|
53
|
+
* Default: 180 seconds (3 minutes)
|
|
54
|
+
*/
|
|
55
|
+
CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
|
|
41
56
|
/**
|
|
42
57
|
* Number of retries for getCandles function
|
|
43
58
|
* Default: 3 retries
|
|
@@ -1755,6 +1770,7 @@ const INTERVAL_MINUTES$1 = {
|
|
|
1755
1770
|
"30m": 30,
|
|
1756
1771
|
"1h": 60,
|
|
1757
1772
|
};
|
|
1773
|
+
const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
1758
1774
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
1759
1775
|
const errors = [];
|
|
1760
1776
|
// ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
|
|
@@ -1811,18 +1827,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1811
1827
|
if (signal.priceStopLoss >= signal.priceOpen) {
|
|
1812
1828
|
errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
|
|
1813
1829
|
}
|
|
1814
|
-
// ЗАЩИТА ОТ
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
//
|
|
1818
|
-
if (
|
|
1819
|
-
errors.push(`Long: currentPrice (${currentPrice})
|
|
1820
|
-
`Signal would be immediately
|
|
1830
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1831
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1832
|
+
// LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
|
|
1833
|
+
// SL < currentPrice < TP
|
|
1834
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
1835
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1836
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1821
1837
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1838
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
1839
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1840
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1844
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1845
|
+
// LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
|
|
1846
|
+
// SL < priceOpen < TP
|
|
1847
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
1848
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1849
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1850
|
+
}
|
|
1851
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
1852
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1853
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
1826
1854
|
}
|
|
1827
1855
|
}
|
|
1828
1856
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1834,8 +1862,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1834
1862
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1835
1863
|
}
|
|
1836
1864
|
}
|
|
1865
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1866
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1867
|
+
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1868
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1869
|
+
errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1870
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1871
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1837
1874
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1838
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1875
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1839
1876
|
const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
|
|
1840
1877
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1841
1878
|
errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1852,18 +1889,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1852
1889
|
if (signal.priceStopLoss <= signal.priceOpen) {
|
|
1853
1890
|
errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
|
|
1854
1891
|
}
|
|
1855
|
-
// ЗАЩИТА ОТ
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
//
|
|
1859
|
-
if (
|
|
1860
|
-
errors.push(`Short: currentPrice (${currentPrice})
|
|
1861
|
-
`Signal would be immediately
|
|
1892
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
|
|
1893
|
+
if (!isScheduled && isFinite(currentPrice)) {
|
|
1894
|
+
// SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
|
|
1895
|
+
// TP < currentPrice < SL
|
|
1896
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
1897
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1898
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
1899
|
+
}
|
|
1900
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
1901
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1902
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
// ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
|
|
1906
|
+
if (isScheduled && isFinite(signal.priceOpen)) {
|
|
1907
|
+
// SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
|
|
1908
|
+
// TP < priceOpen < SL
|
|
1909
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
1910
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
1911
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
1862
1912
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
`Signal is invalid - the profit opportunity has already passed.`);
|
|
1913
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
1914
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
1915
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
1867
1916
|
}
|
|
1868
1917
|
}
|
|
1869
1918
|
// ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
|
|
@@ -1875,8 +1924,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1875
1924
|
`Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
|
|
1876
1925
|
}
|
|
1877
1926
|
}
|
|
1927
|
+
// ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
|
|
1928
|
+
if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1929
|
+
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1930
|
+
if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
|
|
1931
|
+
errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
1932
|
+
`Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
|
|
1933
|
+
`Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1878
1936
|
// ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
|
|
1879
|
-
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT
|
|
1937
|
+
if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1880
1938
|
const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
|
|
1881
1939
|
if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
|
|
1882
1940
|
errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
|
|
@@ -1938,7 +1996,14 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
1938
1996
|
}))) {
|
|
1939
1997
|
return null;
|
|
1940
1998
|
}
|
|
1941
|
-
const
|
|
1999
|
+
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
2000
|
+
const signal = await Promise.race([
|
|
2001
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
|
|
2002
|
+
functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
|
|
2003
|
+
]);
|
|
2004
|
+
if (typeof signal === "symbol") {
|
|
2005
|
+
throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
|
|
2006
|
+
}
|
|
1942
2007
|
if (!signal) {
|
|
1943
2008
|
return null;
|
|
1944
2009
|
}
|
|
@@ -2224,6 +2289,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
|
|
|
2224
2289
|
strategyName: self.params.method.context.strategyName,
|
|
2225
2290
|
exchangeName: self.params.method.context.exchangeName,
|
|
2226
2291
|
symbol: self.params.execution.context.symbol,
|
|
2292
|
+
percentTp: 0,
|
|
2293
|
+
percentSl: 0,
|
|
2227
2294
|
};
|
|
2228
2295
|
if (self.params.callbacks?.onTick) {
|
|
2229
2296
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -2350,6 +2417,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
2350
2417
|
return result;
|
|
2351
2418
|
};
|
|
2352
2419
|
const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
2420
|
+
let percentTp = 0;
|
|
2421
|
+
let percentSl = 0;
|
|
2353
2422
|
// Calculate percentage of path to TP/SL for partial fill/loss callbacks
|
|
2354
2423
|
{
|
|
2355
2424
|
if (signal.position === "long") {
|
|
@@ -2359,18 +2428,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2359
2428
|
// Moving towards TP
|
|
2360
2429
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2361
2430
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2362
|
-
|
|
2431
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2432
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2363
2433
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2364
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2434
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2365
2435
|
}
|
|
2366
2436
|
}
|
|
2367
2437
|
else if (currentDistance < 0) {
|
|
2368
2438
|
// Moving towards SL
|
|
2369
2439
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2370
2440
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2371
|
-
|
|
2441
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2442
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2372
2443
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2373
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2444
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2374
2445
|
}
|
|
2375
2446
|
}
|
|
2376
2447
|
}
|
|
@@ -2381,18 +2452,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2381
2452
|
// Moving towards TP
|
|
2382
2453
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2383
2454
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2384
|
-
|
|
2455
|
+
percentTp = Math.min(progressPercent, 100);
|
|
2456
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2385
2457
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2386
|
-
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice,
|
|
2458
|
+
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
|
|
2387
2459
|
}
|
|
2388
2460
|
}
|
|
2389
2461
|
if (currentDistance < 0) {
|
|
2390
2462
|
// Moving towards SL
|
|
2391
2463
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2392
2464
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2393
|
-
|
|
2465
|
+
percentSl = Math.min(progressPercent, 100);
|
|
2466
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
|
|
2394
2467
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2395
|
-
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice,
|
|
2468
|
+
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
|
|
2396
2469
|
}
|
|
2397
2470
|
}
|
|
2398
2471
|
}
|
|
@@ -2404,6 +2477,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
2404
2477
|
strategyName: self.params.method.context.strategyName,
|
|
2405
2478
|
exchangeName: self.params.method.context.exchangeName,
|
|
2406
2479
|
symbol: self.params.execution.context.symbol,
|
|
2480
|
+
percentTp,
|
|
2481
|
+
percentSl,
|
|
2407
2482
|
};
|
|
2408
2483
|
if (self.params.callbacks?.onTick) {
|
|
2409
2484
|
self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
|
|
@@ -2550,8 +2625,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
2550
2625
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
|
|
2551
2626
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2552
2627
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
2628
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2553
2629
|
for (let i = 0; i < candles.length; i++) {
|
|
2554
2630
|
const candle = candles[i];
|
|
2631
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2632
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2633
|
+
if (i < bufferCandlesCount) {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2555
2636
|
const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
|
|
2556
2637
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2557
2638
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
@@ -2617,11 +2698,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
2617
2698
|
};
|
|
2618
2699
|
const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
2619
2700
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
2620
|
-
|
|
2621
|
-
|
|
2701
|
+
const bufferCandlesCount = candlesCount - 1;
|
|
2702
|
+
// КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
|
|
2703
|
+
// Первые bufferCandlesCount свечей - это буфер для VWAP
|
|
2704
|
+
for (let i = 0; i < candles.length; i++) {
|
|
2705
|
+
const currentCandle = candles[i];
|
|
2706
|
+
const currentCandleTimestamp = currentCandle.timestamp;
|
|
2707
|
+
// КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
|
|
2708
|
+
// BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
|
|
2709
|
+
if (i < bufferCandlesCount) {
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
// Берем последние candlesCount свечей для VWAP (включая буфер)
|
|
2713
|
+
const startIndex = Math.max(0, i - (candlesCount - 1));
|
|
2714
|
+
const recentCandles = candles.slice(startIndex, i + 1);
|
|
2622
2715
|
const averagePrice = GET_AVG_PRICE_FN(recentCandles);
|
|
2623
|
-
const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
|
|
2624
|
-
const currentCandle = recentCandles[recentCandles.length - 1];
|
|
2625
2716
|
let shouldClose = false;
|
|
2626
2717
|
let closeReason;
|
|
2627
2718
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -2921,9 +3012,10 @@ class ClientStrategy {
|
|
|
2921
3012
|
* 4. If cancelled: returns closed result with closeReason "cancelled"
|
|
2922
3013
|
*
|
|
2923
3014
|
* For pending signals:
|
|
2924
|
-
* 1. Iterates through candles
|
|
2925
|
-
* 2.
|
|
2926
|
-
* 3.
|
|
3015
|
+
* 1. Iterates through ALL candles starting from the first one
|
|
3016
|
+
* 2. Checks TP/SL using candle.high/low (immediate detection)
|
|
3017
|
+
* 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
|
|
3018
|
+
* 4. Returns closed result (either TP/SL or time_expired)
|
|
2927
3019
|
*
|
|
2928
3020
|
* @param candles - Array of candles to process
|
|
2929
3021
|
* @returns Promise resolving to closed signal result with PNL
|
|
@@ -3000,6 +3092,8 @@ class ClientStrategy {
|
|
|
3000
3092
|
action: "active",
|
|
3001
3093
|
signal: scheduled,
|
|
3002
3094
|
currentPrice: lastPrice,
|
|
3095
|
+
percentSl: 0,
|
|
3096
|
+
percentTp: 0,
|
|
3003
3097
|
strategyName: this.params.method.context.strategyName,
|
|
3004
3098
|
exchangeName: this.params.method.context.exchangeName,
|
|
3005
3099
|
symbol: this.params.execution.context.symbol,
|
|
@@ -4877,13 +4971,17 @@ class BacktestLogicPrivateService {
|
|
|
4877
4971
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4878
4972
|
});
|
|
4879
4973
|
// Запрашиваем минутные свечи для мониторинга активации/отмены
|
|
4880
|
-
// КРИТИЧНО:
|
|
4881
|
-
//
|
|
4882
|
-
//
|
|
4883
|
-
|
|
4974
|
+
// КРИТИЧНО: запрашиваем:
|
|
4975
|
+
// - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
|
|
4976
|
+
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
4977
|
+
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
4978
|
+
// - +1 потому что when включается как первая свеча
|
|
4979
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
4980
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
4981
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
|
|
4884
4982
|
let candles;
|
|
4885
4983
|
try {
|
|
4886
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded,
|
|
4984
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
4887
4985
|
}
|
|
4888
4986
|
catch (error) {
|
|
4889
4987
|
console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
@@ -4891,6 +4989,7 @@ class BacktestLogicPrivateService {
|
|
|
4891
4989
|
symbol,
|
|
4892
4990
|
signalId: signal.id,
|
|
4893
4991
|
candlesNeeded,
|
|
4992
|
+
bufferMinutes,
|
|
4894
4993
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
4895
4994
|
});
|
|
4896
4995
|
await errorEmitter.next(error);
|
|
@@ -4963,16 +5062,23 @@ class BacktestLogicPrivateService {
|
|
|
4963
5062
|
signalId: signal.id,
|
|
4964
5063
|
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
4965
5064
|
});
|
|
4966
|
-
// Получаем свечи для
|
|
5065
|
+
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
5066
|
+
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
5067
|
+
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
5068
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
5069
|
+
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
5070
|
+
const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
|
|
4967
5071
|
let candles;
|
|
4968
5072
|
try {
|
|
4969
|
-
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m",
|
|
5073
|
+
candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
4970
5074
|
}
|
|
4971
5075
|
catch (error) {
|
|
4972
5076
|
console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
|
|
4973
5077
|
this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
|
|
4974
5078
|
symbol,
|
|
4975
5079
|
signalId: signal.id,
|
|
5080
|
+
totalCandles,
|
|
5081
|
+
bufferMinutes,
|
|
4976
5082
|
error: functoolsKit.errorData(error), message: functoolsKit.getErrorMessage(error),
|
|
4977
5083
|
});
|
|
4978
5084
|
await errorEmitter.next(error);
|
|
@@ -5848,14 +5954,33 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
5848
5954
|
async getReport(strategyName) {
|
|
5849
5955
|
const stats = await this.getData();
|
|
5850
5956
|
if (stats.totalSignals === 0) {
|
|
5851
|
-
return
|
|
5957
|
+
return [
|
|
5958
|
+
`# Backtest Report: ${strategyName}`,
|
|
5959
|
+
"",
|
|
5960
|
+
"No signals closed yet."
|
|
5961
|
+
].join("\n");
|
|
5852
5962
|
}
|
|
5853
5963
|
const header = columns$4.map((col) => col.label);
|
|
5854
5964
|
const separator = columns$4.map(() => "---");
|
|
5855
5965
|
const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
|
|
5856
5966
|
const tableData = [header, separator, ...rows];
|
|
5857
|
-
const table =
|
|
5858
|
-
return
|
|
5967
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
5968
|
+
return [
|
|
5969
|
+
`# Backtest Report: ${strategyName}`,
|
|
5970
|
+
"",
|
|
5971
|
+
table,
|
|
5972
|
+
"",
|
|
5973
|
+
`**Total signals:** ${stats.totalSignals}`,
|
|
5974
|
+
`**Closed signals:** ${stats.totalSignals}`,
|
|
5975
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
5976
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
5977
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
5978
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
5979
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5980
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
5981
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
5982
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
5983
|
+
].join("\n");
|
|
5859
5984
|
}
|
|
5860
5985
|
/**
|
|
5861
5986
|
* Saves strategy report to disk.
|
|
@@ -6137,6 +6262,16 @@ const columns$3 = [
|
|
|
6137
6262
|
label: "Stop Loss",
|
|
6138
6263
|
format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
|
|
6139
6264
|
},
|
|
6265
|
+
{
|
|
6266
|
+
key: "percentTp",
|
|
6267
|
+
label: "% to TP",
|
|
6268
|
+
format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
|
|
6269
|
+
},
|
|
6270
|
+
{
|
|
6271
|
+
key: "percentSl",
|
|
6272
|
+
label: "% to SL",
|
|
6273
|
+
format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
|
|
6274
|
+
},
|
|
6140
6275
|
{
|
|
6141
6276
|
key: "pnl",
|
|
6142
6277
|
label: "PNL (net)",
|
|
@@ -6239,6 +6374,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6239
6374
|
openPrice: data.signal.priceOpen,
|
|
6240
6375
|
takeProfit: data.signal.priceTakeProfit,
|
|
6241
6376
|
stopLoss: data.signal.priceStopLoss,
|
|
6377
|
+
percentTp: data.percentTp,
|
|
6378
|
+
percentSl: data.percentSl,
|
|
6242
6379
|
};
|
|
6243
6380
|
// Replace existing event or add new one
|
|
6244
6381
|
if (existingIndex !== -1) {
|
|
@@ -6380,14 +6517,33 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6380
6517
|
async getReport(strategyName) {
|
|
6381
6518
|
const stats = await this.getData();
|
|
6382
6519
|
if (stats.totalEvents === 0) {
|
|
6383
|
-
return
|
|
6520
|
+
return [
|
|
6521
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6522
|
+
"",
|
|
6523
|
+
"No events recorded yet."
|
|
6524
|
+
].join("\n");
|
|
6384
6525
|
}
|
|
6385
6526
|
const header = columns$3.map((col) => col.label);
|
|
6386
6527
|
const separator = columns$3.map(() => "---");
|
|
6387
6528
|
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
6388
6529
|
const tableData = [header, separator, ...rows];
|
|
6389
|
-
const table =
|
|
6390
|
-
return
|
|
6530
|
+
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6531
|
+
return [
|
|
6532
|
+
`# Live Trading Report: ${strategyName}`,
|
|
6533
|
+
"",
|
|
6534
|
+
table,
|
|
6535
|
+
"",
|
|
6536
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6537
|
+
`**Closed signals:** ${stats.totalClosed}`,
|
|
6538
|
+
`**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
|
|
6539
|
+
`**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
|
|
6540
|
+
`**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
|
|
6541
|
+
`**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
|
|
6542
|
+
`**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6543
|
+
`**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
|
|
6544
|
+
`**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
|
|
6545
|
+
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
6546
|
+
].join("\n");
|
|
6391
6547
|
}
|
|
6392
6548
|
/**
|
|
6393
6549
|
* Saves strategy report to disk.
|
|
@@ -6784,14 +6940,28 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
6784
6940
|
async getReport(strategyName) {
|
|
6785
6941
|
const stats = await this.getData();
|
|
6786
6942
|
if (stats.totalEvents === 0) {
|
|
6787
|
-
return
|
|
6943
|
+
return [
|
|
6944
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6945
|
+
"",
|
|
6946
|
+
"No scheduled signals recorded yet."
|
|
6947
|
+
].join("\n");
|
|
6788
6948
|
}
|
|
6789
6949
|
const header = columns$2.map((col) => col.label);
|
|
6790
6950
|
const separator = columns$2.map(() => "---");
|
|
6791
6951
|
const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
|
|
6792
6952
|
const tableData = [header, separator, ...rows];
|
|
6793
|
-
const table =
|
|
6794
|
-
return
|
|
6953
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
6954
|
+
return [
|
|
6955
|
+
`# Scheduled Signals Report: ${strategyName}`,
|
|
6956
|
+
"",
|
|
6957
|
+
table,
|
|
6958
|
+
"",
|
|
6959
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
6960
|
+
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
6961
|
+
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
6962
|
+
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
6963
|
+
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
|
|
6964
|
+
].join("\n");
|
|
6795
6965
|
}
|
|
6796
6966
|
/**
|
|
6797
6967
|
* Saves strategy report to disk.
|
|
@@ -7109,7 +7279,11 @@ class PerformanceStorage {
|
|
|
7109
7279
|
async getReport(strategyName) {
|
|
7110
7280
|
const stats = await this.getData(strategyName);
|
|
7111
7281
|
if (stats.totalEvents === 0) {
|
|
7112
|
-
return
|
|
7282
|
+
return [
|
|
7283
|
+
`# Performance Report: ${strategyName}`,
|
|
7284
|
+
"",
|
|
7285
|
+
"No performance metrics recorded yet."
|
|
7286
|
+
].join("\n");
|
|
7113
7287
|
}
|
|
7114
7288
|
// Sort metrics by total duration (descending) to show bottlenecks first
|
|
7115
7289
|
const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
|
|
@@ -7146,13 +7320,29 @@ class PerformanceStorage {
|
|
|
7146
7320
|
metric.maxWaitTime.toFixed(2),
|
|
7147
7321
|
]);
|
|
7148
7322
|
const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
|
|
7149
|
-
const summaryTable =
|
|
7323
|
+
const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7150
7324
|
// Calculate percentage of total time for each metric
|
|
7151
7325
|
const percentages = sortedMetrics.map((metric) => {
|
|
7152
7326
|
const pct = (metric.totalDuration / stats.totalDuration) * 100;
|
|
7153
7327
|
return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
|
|
7154
7328
|
});
|
|
7155
|
-
return
|
|
7329
|
+
return [
|
|
7330
|
+
`# Performance Report: ${strategyName}`,
|
|
7331
|
+
"",
|
|
7332
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
7333
|
+
`**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
|
|
7334
|
+
`**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
|
|
7335
|
+
"",
|
|
7336
|
+
"## Time Distribution",
|
|
7337
|
+
"",
|
|
7338
|
+
percentages.join("\n"),
|
|
7339
|
+
"",
|
|
7340
|
+
"## Detailed Metrics",
|
|
7341
|
+
"",
|
|
7342
|
+
summaryTable,
|
|
7343
|
+
"",
|
|
7344
|
+
"**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type."
|
|
7345
|
+
].join("\n");
|
|
7156
7346
|
}
|
|
7157
7347
|
/**
|
|
7158
7348
|
* Saves performance report to disk.
|
|
@@ -7556,7 +7746,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7556
7746
|
// Build table rows
|
|
7557
7747
|
const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
|
|
7558
7748
|
const tableData = [header, separator, ...rows];
|
|
7559
|
-
return
|
|
7749
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7560
7750
|
}
|
|
7561
7751
|
/**
|
|
7562
7752
|
* Generates PNL table showing all closed signals across all strategies (View).
|
|
@@ -7593,7 +7783,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7593
7783
|
// Build table rows
|
|
7594
7784
|
const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
|
|
7595
7785
|
const tableData = [header, separator, ...rows];
|
|
7596
|
-
return
|
|
7786
|
+
return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7597
7787
|
}
|
|
7598
7788
|
/**
|
|
7599
7789
|
* Generates markdown report with all strategy results (View).
|
|
@@ -7608,7 +7798,30 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7608
7798
|
const results = await this.getData(symbol, metric, context);
|
|
7609
7799
|
// Get total signals for best strategy
|
|
7610
7800
|
const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
|
|
7611
|
-
return
|
|
7801
|
+
return [
|
|
7802
|
+
`# Walker Comparison Report: ${results.walkerName}`,
|
|
7803
|
+
"",
|
|
7804
|
+
`**Symbol:** ${results.symbol}`,
|
|
7805
|
+
`**Exchange:** ${results.exchangeName}`,
|
|
7806
|
+
`**Frame:** ${results.frameName}`,
|
|
7807
|
+
`**Optimization Metric:** ${results.metric}`,
|
|
7808
|
+
`**Strategies Tested:** ${results.totalStrategies}`,
|
|
7809
|
+
"",
|
|
7810
|
+
`## Best Strategy: ${results.bestStrategy}`,
|
|
7811
|
+
"",
|
|
7812
|
+
`**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
|
|
7813
|
+
`**Total Signals:** ${bestStrategySignals}`,
|
|
7814
|
+
"",
|
|
7815
|
+
"## Top Strategies Comparison",
|
|
7816
|
+
"",
|
|
7817
|
+
this.getComparisonTable(metric, 10),
|
|
7818
|
+
"",
|
|
7819
|
+
"## All Signals (PNL Table)",
|
|
7820
|
+
"",
|
|
7821
|
+
this.getPnlTable(),
|
|
7822
|
+
"",
|
|
7823
|
+
"**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
|
|
7824
|
+
].join("\n");
|
|
7612
7825
|
}
|
|
7613
7826
|
/**
|
|
7614
7827
|
* Saves walker report to disk.
|
|
@@ -8122,14 +8335,24 @@ class HeatmapStorage {
|
|
|
8122
8335
|
async getReport(strategyName) {
|
|
8123
8336
|
const data = await this.getData();
|
|
8124
8337
|
if (data.symbols.length === 0) {
|
|
8125
|
-
return
|
|
8338
|
+
return [
|
|
8339
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8340
|
+
"",
|
|
8341
|
+
"*No data available*"
|
|
8342
|
+
].join("\n");
|
|
8126
8343
|
}
|
|
8127
8344
|
const header = columns$1.map((col) => col.label);
|
|
8128
8345
|
const separator = columns$1.map(() => "---");
|
|
8129
8346
|
const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
|
|
8130
8347
|
const tableData = [header, separator, ...rows];
|
|
8131
|
-
const table =
|
|
8132
|
-
return
|
|
8348
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8349
|
+
return [
|
|
8350
|
+
`# Portfolio Heatmap: ${strategyName}`,
|
|
8351
|
+
"",
|
|
8352
|
+
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
|
|
8353
|
+
"",
|
|
8354
|
+
table
|
|
8355
|
+
].join("\n");
|
|
8133
8356
|
}
|
|
8134
8357
|
/**
|
|
8135
8358
|
* Saves heatmap report to disk.
|
|
@@ -10687,14 +10910,26 @@ class ReportStorage {
|
|
|
10687
10910
|
async getReport(symbol, strategyName) {
|
|
10688
10911
|
const stats = await this.getData();
|
|
10689
10912
|
if (stats.totalEvents === 0) {
|
|
10690
|
-
return
|
|
10913
|
+
return [
|
|
10914
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10915
|
+
"",
|
|
10916
|
+
"No partial profit/loss events recorded yet."
|
|
10917
|
+
].join("\n");
|
|
10691
10918
|
}
|
|
10692
10919
|
const header = columns.map((col) => col.label);
|
|
10693
10920
|
const separator = columns.map(() => "---");
|
|
10694
10921
|
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
10695
10922
|
const tableData = [header, separator, ...rows];
|
|
10696
|
-
const table =
|
|
10697
|
-
return
|
|
10923
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
10924
|
+
return [
|
|
10925
|
+
`# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
|
|
10926
|
+
"",
|
|
10927
|
+
table,
|
|
10928
|
+
"",
|
|
10929
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
10930
|
+
`**Profit events:** ${stats.totalProfit}`,
|
|
10931
|
+
`**Loss events:** ${stats.totalLoss}`
|
|
10932
|
+
].join("\n");
|
|
10698
10933
|
}
|
|
10699
10934
|
/**
|
|
10700
10935
|
* Saves symbol-strategy report to disk.
|