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