backtest-kit 7.8.0 → 8.1.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 +43 -0
- package/build/index.cjs +189 -7
- package/build/index.mjs +189 -7
- package/package.json +1 -1
- package/types.d.ts +39 -3
package/README.md
CHANGED
|
@@ -1742,6 +1742,49 @@ Install the skill once and get AI-assisted backtest-kit development inside Claud
|
|
|
1742
1742
|
npx skills add https://github.com/backtest-kit/backtest-kit-skills
|
|
1743
1743
|
```
|
|
1744
1744
|
|
|
1745
|
+
## 🧩 Strategy Examples
|
|
1746
|
+
|
|
1747
|
+
#### 🧠 Neural Network Strategy (Oct 2021)
|
|
1748
|
+
|
|
1749
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/oct_2021.strategy)
|
|
1750
|
+
|
|
1751
|
+
Trains a feed-forward `TensorFlow` neural network (8→6→4→1 architecture) every 8 hours to predict where the next candle will close within its high-low range. When current price is below predicted price, opens a LONG with 1% trailing take-profit.
|
|
1752
|
+
|
|
1753
|
+
#### 🌲 Pine Script Range Breakout (Dec 2025)
|
|
1754
|
+
|
|
1755
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/dec_2025.strategy)
|
|
1756
|
+
|
|
1757
|
+
Runs `btc_dec2025_range.pine` on 1h candles via `@backtest-kit/pinets`, extracting Bollinger Bands, range boundaries, and volume spikes. Signals fire only on confirmed breakouts when price hasn't already moved past the signal close.
|
|
1758
|
+
|
|
1759
|
+
#### 🔪 Signal Inversion Strategy (Jan 2026)
|
|
1760
|
+
|
|
1761
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/jan_2026.strategy)
|
|
1762
|
+
|
|
1763
|
+
The strategy takes published signals from a real Telegram crypto channel (Crypto Yoda), enters at the same price zone and timestamp, but **inverts the direction** and uses the liquidity of the crowd that blindly follows the recommendation regardless of the contents of the order book.
|
|
1764
|
+
|
|
1765
|
+
#### 📰 AI News Sentiment (Feb 2026)
|
|
1766
|
+
|
|
1767
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2026.strategy)
|
|
1768
|
+
|
|
1769
|
+
Every 4-8 hours, fetches live crypto/macro news via Tavily, passes headlines to Ollama (local LLM), and opens positions based on `bullish`/`bearish`/`wait` forecasts. Conflicting signals flip positions mid-trade. Achieved +16.99% during a -16.4% month.
|
|
1770
|
+
|
|
1771
|
+
#### 🪂 SHORT DCA Ladder (Mar 2026)
|
|
1772
|
+
|
|
1773
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/mar_2026.strategy)
|
|
1774
|
+
|
|
1775
|
+
Opens a SHORT on every pending signal, then adds rungs (up to 10) whenever price spikes upward outside a ±1-5% band around last entry. Closes at 0.5% blended profit.
|
|
1776
|
+
|
|
1777
|
+
#### 🧗 LONG DCA Ladder (Apr 2026)
|
|
1778
|
+
|
|
1779
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/apr_2026.strategy)
|
|
1780
|
+
|
|
1781
|
+
Same mechanics as SHORT version but LONG-biased with 3% profit target. Deployed 2.4 entries per trade on average, achieved +67.85% PNL on deployed capital with improved percentage drawdown (-2.59% vs -3.99% without DCA).
|
|
1782
|
+
|
|
1783
|
+
#### 🐍 Python EMA Crossover (Feb 2021)
|
|
1784
|
+
|
|
1785
|
+
> Link to [the source code](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2021.strategy)
|
|
1786
|
+
|
|
1787
|
+
Python-based (WASI) strategy that uses EMA(9) and EMA(21) crossover signals executed via WebAssembly. Trades trigger when fast EMA crosses slow EMA, confirmed by 4h range midpoint.
|
|
1745
1788
|
|
|
1746
1789
|
## 🤖 Are you a robot?
|
|
1747
1790
|
|
package/build/index.cjs
CHANGED
|
@@ -590,6 +590,20 @@ const GLOBAL_CONFIG = {
|
|
|
590
590
|
* Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
|
|
591
591
|
*/
|
|
592
592
|
CC_ENABLE_PPPL_EVERYWHERE: false,
|
|
593
|
+
/**
|
|
594
|
+
* Enables long signals in strategies that are primarily designed for short signals.
|
|
595
|
+
* This allows the strategy to generate and manage long signals in addition to short signals, even if the original design was focused on short trading.
|
|
596
|
+
* This can help expand the strategy's applicability and take advantage of bullish market conditions, but may require additional logic to manage long signal behavior effectively.
|
|
597
|
+
*
|
|
598
|
+
* Default: false (long signals are only enabled in strategies that are designed for them, ensuring strategy logic is aligned with signal types)
|
|
599
|
+
*/
|
|
600
|
+
CC_ENABLE_LONG_SIGNAL: true,
|
|
601
|
+
/**
|
|
602
|
+
* Enables short signals in strategies that are primarily designed for long signals.
|
|
603
|
+
* This allows the strategy to generate and manage short signals in addition to long signals, even if the original design was focused on long trading.
|
|
604
|
+
* This can help expand the strategy's applicability and take advantage of bearish market conditions, but may require additional logic to manage short signal behavior effectively.
|
|
605
|
+
*/
|
|
606
|
+
CC_ENABLE_SHORT_SIGNAL: true,
|
|
593
607
|
/**
|
|
594
608
|
* Enables trailing logic (Trailing Take / Trailing Stop) without requiring absorption conditions.
|
|
595
609
|
* Allows trailing mechanisms to be activated regardless of whether absorption has been detected.
|
|
@@ -4710,6 +4724,12 @@ const validateCommonSignal = (signal) => {
|
|
|
4710
4724
|
if (signal.position !== "long" && signal.position !== "short") {
|
|
4711
4725
|
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
4712
4726
|
}
|
|
4727
|
+
if (signal.position === "long" && !GLOBAL_CONFIG.CC_ENABLE_LONG_SIGNAL) {
|
|
4728
|
+
errors.push(`Long signals are disabled (CC_ENABLE_LONG_SIGNAL=false)`);
|
|
4729
|
+
}
|
|
4730
|
+
if (signal.position === "short" && !GLOBAL_CONFIG.CC_ENABLE_SHORT_SIGNAL) {
|
|
4731
|
+
errors.push(`Short signals are disabled (CC_ENABLE_SHORT_SIGNAL=false)`);
|
|
4732
|
+
}
|
|
4713
4733
|
}
|
|
4714
4734
|
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
4715
4735
|
{
|
|
@@ -5545,6 +5565,10 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
5545
5565
|
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5546
5566
|
_fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5547
5567
|
};
|
|
5568
|
+
{
|
|
5569
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(signalRow, signal.priceOpen);
|
|
5570
|
+
signalRow._fall = { price: signal.priceOpen, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
5571
|
+
}
|
|
5548
5572
|
// Валидируем сигнал перед возвратом
|
|
5549
5573
|
validatePendingSignal(signalRow, currentPrice);
|
|
5550
5574
|
return signalRow;
|
|
@@ -5594,6 +5618,10 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
5594
5618
|
_peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5595
5619
|
_fall: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5596
5620
|
};
|
|
5621
|
+
{
|
|
5622
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(signalRow, currentPrice);
|
|
5623
|
+
signalRow._fall = { price: currentPrice, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
5624
|
+
}
|
|
5597
5625
|
// Валидируем сигнал перед возвратом
|
|
5598
5626
|
validatePendingSignal(signalRow, currentPrice);
|
|
5599
5627
|
return signalRow;
|
|
@@ -6306,6 +6334,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
6306
6334
|
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
6307
6335
|
_fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
6308
6336
|
};
|
|
6337
|
+
{
|
|
6338
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(activatedSignal, activatedSignal.priceOpen);
|
|
6339
|
+
activatedSignal._fall = { price: activatedSignal.priceOpen, timestamp: activationTime, pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen };
|
|
6340
|
+
}
|
|
6309
6341
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
6310
6342
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
6311
6343
|
if (!syncOpenAllowed) {
|
|
@@ -7189,6 +7221,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
7189
7221
|
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
7190
7222
|
_fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
7191
7223
|
};
|
|
7224
|
+
{
|
|
7225
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(activatedSignal, activatedSignal.priceOpen);
|
|
7226
|
+
activatedSignal._fall = { price: activatedSignal.priceOpen, timestamp: activationTime, pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen };
|
|
7227
|
+
}
|
|
7192
7228
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
7193
7229
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
7194
7230
|
if (!syncOpenAllowed) {
|
|
@@ -7386,6 +7422,10 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
|
|
|
7386
7422
|
_peak: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
7387
7423
|
_fall: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
7388
7424
|
};
|
|
7425
|
+
{
|
|
7426
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(pendingSignal, pendingSignal.priceOpen);
|
|
7427
|
+
pendingSignal._fall = { price: pendingSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
7428
|
+
}
|
|
7389
7429
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
7390
7430
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(candle.timestamp, pendingSignal.priceOpen, pendingSignal, self);
|
|
7391
7431
|
if (!syncOpenAllowed) {
|
|
@@ -8830,6 +8870,10 @@ class ClientStrategy {
|
|
|
8830
8870
|
_peak: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, pnlEntries: 0, priceOpen: 0 },
|
|
8831
8871
|
_fall: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, pnlEntries: 0, priceOpen: 0 },
|
|
8832
8872
|
};
|
|
8873
|
+
{
|
|
8874
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(pendingSignal, pendingSignal.priceOpen);
|
|
8875
|
+
pendingSignal._fall = { price: pendingSignal.priceOpen, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, pnlEntries, priceOpen };
|
|
8876
|
+
}
|
|
8833
8877
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(currentTime, currentPrice, pendingSignal, this);
|
|
8834
8878
|
if (!syncOpenAllowed) {
|
|
8835
8879
|
this.params.logger.info("ClientStrategy tick: user-activated signal rejected by sync", {
|
|
@@ -19393,6 +19437,24 @@ const heat_columns = [
|
|
|
19393
19437
|
format: (data) => data.avgFallPnl !== null ? functoolsKit.str(data.avgFallPnl, "%") : "N/A",
|
|
19394
19438
|
isVisible: () => true,
|
|
19395
19439
|
},
|
|
19440
|
+
{
|
|
19441
|
+
key: "sortinoRatio",
|
|
19442
|
+
label: "Sortino",
|
|
19443
|
+
format: (data) => data.sortinoRatio !== null ? functoolsKit.str(data.sortinoRatio) : "N/A",
|
|
19444
|
+
isVisible: () => true,
|
|
19445
|
+
},
|
|
19446
|
+
{
|
|
19447
|
+
key: "calmarRatio",
|
|
19448
|
+
label: "Calmar",
|
|
19449
|
+
format: (data) => data.calmarRatio !== null ? functoolsKit.str(data.calmarRatio) : "N/A",
|
|
19450
|
+
isVisible: () => true,
|
|
19451
|
+
},
|
|
19452
|
+
{
|
|
19453
|
+
key: "recoveryFactor",
|
|
19454
|
+
label: "Recovery",
|
|
19455
|
+
format: (data) => data.recoveryFactor !== null ? functoolsKit.str(data.recoveryFactor) : "N/A",
|
|
19456
|
+
isVisible: () => true,
|
|
19457
|
+
},
|
|
19396
19458
|
];
|
|
19397
19459
|
|
|
19398
19460
|
/**
|
|
@@ -20955,6 +21017,30 @@ const walker_strategy_columns = [
|
|
|
20955
21017
|
: "N/A",
|
|
20956
21018
|
isVisible: () => true,
|
|
20957
21019
|
},
|
|
21020
|
+
{
|
|
21021
|
+
key: "sortinoRatio",
|
|
21022
|
+
label: "Sortino",
|
|
21023
|
+
format: (data) => data.stats.sortinoRatio !== null
|
|
21024
|
+
? `${data.stats.sortinoRatio.toFixed(3)}`
|
|
21025
|
+
: "N/A",
|
|
21026
|
+
isVisible: () => true,
|
|
21027
|
+
},
|
|
21028
|
+
{
|
|
21029
|
+
key: "calmarRatio",
|
|
21030
|
+
label: "Calmar",
|
|
21031
|
+
format: (data) => data.stats.calmarRatio !== null
|
|
21032
|
+
? `${data.stats.calmarRatio.toFixed(3)}`
|
|
21033
|
+
: "N/A",
|
|
21034
|
+
isVisible: () => true,
|
|
21035
|
+
},
|
|
21036
|
+
{
|
|
21037
|
+
key: "recoveryFactor",
|
|
21038
|
+
label: "Recovery",
|
|
21039
|
+
format: (data) => data.stats.recoveryFactor !== null
|
|
21040
|
+
? `${data.stats.recoveryFactor.toFixed(3)}`
|
|
21041
|
+
: "N/A",
|
|
21042
|
+
isVisible: () => true,
|
|
21043
|
+
},
|
|
20958
21044
|
{
|
|
20959
21045
|
key: "firstEventTime",
|
|
20960
21046
|
label: "First Event",
|
|
@@ -21504,13 +21590,13 @@ class ReportBase {
|
|
|
21504
21590
|
* Waits for drain event if write buffer is full.
|
|
21505
21591
|
* Times out after 15 seconds and returns TIMEOUT_SYMBOL.
|
|
21506
21592
|
*/
|
|
21507
|
-
this[_d] = functoolsKit.timeout(async (line) => {
|
|
21593
|
+
this[_d] = functoolsKit.queued(functoolsKit.timeout(async (line) => {
|
|
21508
21594
|
if (!this._stream.write(line)) {
|
|
21509
21595
|
await new Promise((resolve) => {
|
|
21510
21596
|
this._stream.once("drain", resolve);
|
|
21511
21597
|
});
|
|
21512
21598
|
}
|
|
21513
|
-
}, 15000);
|
|
21599
|
+
}, 15000));
|
|
21514
21600
|
LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_CTOR, {
|
|
21515
21601
|
reportName: this.reportName,
|
|
21516
21602
|
baseDir,
|
|
@@ -21798,6 +21884,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21798
21884
|
expectedYearlyReturns: null,
|
|
21799
21885
|
avgPeakPnl: null,
|
|
21800
21886
|
avgFallPnl: null,
|
|
21887
|
+
sortinoRatio: null,
|
|
21888
|
+
calmarRatio: null,
|
|
21889
|
+
recoveryFactor: null,
|
|
21801
21890
|
};
|
|
21802
21891
|
}
|
|
21803
21892
|
const totalSignals = this._signalList.length;
|
|
@@ -21831,6 +21920,16 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21831
21920
|
// Calculate average peak and fall PNL across all signals
|
|
21832
21921
|
const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / totalSignals;
|
|
21833
21922
|
const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / totalSignals;
|
|
21923
|
+
// Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
|
|
21924
|
+
const fallReturns = this._signalList.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
|
|
21925
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
21926
|
+
const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalSignals;
|
|
21927
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
21928
|
+
const sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
|
|
21929
|
+
// Avg absolute peak drawdown per signal — used as denominator for Calmar and Recovery
|
|
21930
|
+
const avgAbsFall = fallReturns.reduce((sum, r) => sum + Math.abs(r), 0) / totalSignals;
|
|
21931
|
+
const calmarRatio = avgAbsFall > 0 ? expectedYearlyReturns / avgAbsFall : 0;
|
|
21932
|
+
const recoveryFactor = avgAbsFall > 0 ? totalPnl / avgAbsFall : 0;
|
|
21834
21933
|
return {
|
|
21835
21934
|
signalList: this._signalList,
|
|
21836
21935
|
totalSignals,
|
|
@@ -21846,6 +21945,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21846
21945
|
expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
21847
21946
|
avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
|
|
21848
21947
|
avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
|
|
21948
|
+
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
21949
|
+
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
21950
|
+
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
21849
21951
|
};
|
|
21850
21952
|
}
|
|
21851
21953
|
/**
|
|
@@ -21892,6 +21994,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21892
21994
|
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
21893
21995
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
21894
21996
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
21997
|
+
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
21998
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
21999
|
+
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
21895
22000
|
].join("\n");
|
|
21896
22001
|
}
|
|
21897
22002
|
/**
|
|
@@ -22492,6 +22597,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22492
22597
|
expectedYearlyReturns: null,
|
|
22493
22598
|
avgPeakPnl: null,
|
|
22494
22599
|
avgFallPnl: null,
|
|
22600
|
+
sortinoRatio: null,
|
|
22601
|
+
calmarRatio: null,
|
|
22602
|
+
recoveryFactor: null,
|
|
22495
22603
|
};
|
|
22496
22604
|
}
|
|
22497
22605
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
@@ -22541,6 +22649,21 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22541
22649
|
const avgFallPnl = totalClosed > 0
|
|
22542
22650
|
? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
|
|
22543
22651
|
: 0;
|
|
22652
|
+
// Downside per signal: fallPnl captures the worst intra-trade dip (maxDrawdown.pnlPercentage)
|
|
22653
|
+
const fallReturns = closedEvents.map((e) => e.fallPnl || 0);
|
|
22654
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
22655
|
+
let sortinoRatio = 0;
|
|
22656
|
+
if (totalClosed > 0) {
|
|
22657
|
+
const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalClosed;
|
|
22658
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
22659
|
+
sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
|
|
22660
|
+
}
|
|
22661
|
+
// Avg absolute peak drawdown per signal — denominator for Calmar and Recovery
|
|
22662
|
+
const avgAbsFall = totalClosed > 0
|
|
22663
|
+
? fallReturns.reduce((sum, r) => sum + Math.abs(r), 0) / totalClosed
|
|
22664
|
+
: 0;
|
|
22665
|
+
const calmarRatio = avgAbsFall > 0 ? expectedYearlyReturns / avgAbsFall : 0;
|
|
22666
|
+
const recoveryFactor = avgAbsFall > 0 ? totalPnl / avgAbsFall : 0;
|
|
22544
22667
|
return {
|
|
22545
22668
|
eventList: this._eventList,
|
|
22546
22669
|
totalEvents: this._eventList.length,
|
|
@@ -22557,6 +22680,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22557
22680
|
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
22558
22681
|
avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
|
|
22559
22682
|
avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
|
|
22683
|
+
sortinoRatio: isUnsafe$2(sortinoRatio) ? null : sortinoRatio,
|
|
22684
|
+
calmarRatio: isUnsafe$2(calmarRatio) ? null : calmarRatio,
|
|
22685
|
+
recoveryFactor: isUnsafe$2(recoveryFactor) ? null : recoveryFactor,
|
|
22560
22686
|
};
|
|
22561
22687
|
}
|
|
22562
22688
|
/**
|
|
@@ -22603,6 +22729,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22603
22729
|
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
22604
22730
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
22605
22731
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
22732
|
+
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
22733
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
22734
|
+
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
22606
22735
|
].join("\n");
|
|
22607
22736
|
}
|
|
22608
22737
|
/**
|
|
@@ -24048,6 +24177,9 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
24048
24177
|
"",
|
|
24049
24178
|
`**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
|
|
24050
24179
|
`**Total Signals:** ${bestStrategySignals}`,
|
|
24180
|
+
`**Sortino Ratio:** ${results.bestStats?.sortinoRatio != null ? `${results.bestStats.sortinoRatio.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24181
|
+
`**Calmar Ratio:** ${results.bestStats?.calmarRatio != null ? `${results.bestStats.calmarRatio.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24182
|
+
`**Recovery Factor:** ${results.bestStats?.recoveryFactor != null ? `${results.bestStats.recoveryFactor.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24051
24183
|
"",
|
|
24052
24184
|
"## Top Strategies Comparison",
|
|
24053
24185
|
"",
|
|
@@ -24514,6 +24646,27 @@ class HeatmapStorage {
|
|
|
24514
24646
|
avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / signals.length;
|
|
24515
24647
|
avgFallPnl = signals.reduce((acc, s) => acc + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / signals.length;
|
|
24516
24648
|
}
|
|
24649
|
+
// Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
|
|
24650
|
+
const fallReturns = signals.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
|
|
24651
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
24652
|
+
let sortinoRatio = null;
|
|
24653
|
+
if (signals.length > 0 && avgPnl !== null) {
|
|
24654
|
+
const fallVariance = fallReturns.reduce((acc, r) => acc + Math.pow(r, 2), 0) / signals.length;
|
|
24655
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
24656
|
+
if (fallDeviation > 0) {
|
|
24657
|
+
sortinoRatio = avgPnl / fallDeviation;
|
|
24658
|
+
}
|
|
24659
|
+
}
|
|
24660
|
+
// Avg absolute peak drawdown per signal — denominator for Calmar and Recovery
|
|
24661
|
+
const avgAbsFall = signals.length > 0
|
|
24662
|
+
? fallReturns.reduce((acc, r) => acc + Math.abs(r), 0) / signals.length
|
|
24663
|
+
: 0;
|
|
24664
|
+
let calmarRatio = null;
|
|
24665
|
+
let recoveryFactor = null;
|
|
24666
|
+
if (avgAbsFall > 0 && totalPnl !== null) {
|
|
24667
|
+
calmarRatio = totalPnl / avgAbsFall;
|
|
24668
|
+
recoveryFactor = totalPnl / avgAbsFall;
|
|
24669
|
+
}
|
|
24517
24670
|
// Apply safe math checks
|
|
24518
24671
|
if (isUnsafe(winRate))
|
|
24519
24672
|
winRate = null;
|
|
@@ -24539,6 +24692,12 @@ class HeatmapStorage {
|
|
|
24539
24692
|
avgPeakPnl = null;
|
|
24540
24693
|
if (isUnsafe(avgFallPnl))
|
|
24541
24694
|
avgFallPnl = null;
|
|
24695
|
+
if (isUnsafe(sortinoRatio))
|
|
24696
|
+
sortinoRatio = null;
|
|
24697
|
+
if (isUnsafe(calmarRatio))
|
|
24698
|
+
calmarRatio = null;
|
|
24699
|
+
if (isUnsafe(recoveryFactor))
|
|
24700
|
+
recoveryFactor = null;
|
|
24542
24701
|
return {
|
|
24543
24702
|
symbol,
|
|
24544
24703
|
totalPnl,
|
|
@@ -24558,6 +24717,9 @@ class HeatmapStorage {
|
|
|
24558
24717
|
expectancy,
|
|
24559
24718
|
avgPeakPnl,
|
|
24560
24719
|
avgFallPnl,
|
|
24720
|
+
sortinoRatio,
|
|
24721
|
+
calmarRatio,
|
|
24722
|
+
recoveryFactor,
|
|
24561
24723
|
};
|
|
24562
24724
|
}
|
|
24563
24725
|
/**
|
|
@@ -29424,6 +29586,9 @@ class WalkerReportService {
|
|
|
29424
29586
|
annualizedSharpeRatio: data.stats.annualizedSharpeRatio,
|
|
29425
29587
|
certaintyRatio: data.stats.certaintyRatio,
|
|
29426
29588
|
expectedYearlyReturns: data.stats.expectedYearlyReturns,
|
|
29589
|
+
sortinoRatio: data.stats.sortinoRatio,
|
|
29590
|
+
calmarRatio: data.stats.calmarRatio,
|
|
29591
|
+
recoveryFactor: data.stats.recoveryFactor,
|
|
29427
29592
|
firstEventTime,
|
|
29428
29593
|
lastEventTime,
|
|
29429
29594
|
}, {
|
|
@@ -50738,7 +50903,8 @@ class DumpAdapter {
|
|
|
50738
50903
|
const unClose = signalEmitter
|
|
50739
50904
|
.filter(({ action }) => action === "closed")
|
|
50740
50905
|
.connect(({ signal }) => handleDispose(signal.id));
|
|
50741
|
-
|
|
50906
|
+
const unEnable = () => this.enable.clear();
|
|
50907
|
+
return functoolsKit.compose(() => unCancel(), () => unClose(), () => unEnable());
|
|
50742
50908
|
});
|
|
50743
50909
|
/**
|
|
50744
50910
|
* Deactivates the adapter by unsubscribing from signal lifecycle events.
|
|
@@ -51425,7 +51591,7 @@ class ReportUtils {
|
|
|
51425
51591
|
*
|
|
51426
51592
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
51427
51593
|
*/
|
|
51428
|
-
this.enable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
51594
|
+
this.enable = functoolsKit.singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
51429
51595
|
LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
51430
51596
|
backtest: bt,
|
|
51431
51597
|
breakeven,
|
|
@@ -51479,8 +51645,11 @@ class ReportUtils {
|
|
|
51479
51645
|
if (max_drawdown) {
|
|
51480
51646
|
unList.push(backtest.maxDrawdownReportService.subscribe());
|
|
51481
51647
|
}
|
|
51648
|
+
{
|
|
51649
|
+
unList.push(() => this.enable.clear());
|
|
51650
|
+
}
|
|
51482
51651
|
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
51483
|
-
};
|
|
51652
|
+
});
|
|
51484
51653
|
/**
|
|
51485
51654
|
* Disables report services selectively.
|
|
51486
51655
|
*
|
|
@@ -51531,6 +51700,11 @@ class ReportUtils {
|
|
|
51531
51700
|
strategy,
|
|
51532
51701
|
sync,
|
|
51533
51702
|
});
|
|
51703
|
+
if (this.enable.hasValue()) {
|
|
51704
|
+
const lastSubscription = this.enable();
|
|
51705
|
+
lastSubscription();
|
|
51706
|
+
return;
|
|
51707
|
+
}
|
|
51534
51708
|
if (bt) {
|
|
51535
51709
|
backtest.backtestReportService.unsubscribe();
|
|
51536
51710
|
}
|
|
@@ -51692,7 +51866,7 @@ class MarkdownUtils {
|
|
|
51692
51866
|
*
|
|
51693
51867
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
51694
51868
|
*/
|
|
51695
|
-
this.enable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
51869
|
+
this.enable = functoolsKit.singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
51696
51870
|
LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
51697
51871
|
backtest: bt,
|
|
51698
51872
|
breakeven,
|
|
@@ -51747,8 +51921,11 @@ class MarkdownUtils {
|
|
|
51747
51921
|
if (max_drawdown) {
|
|
51748
51922
|
unList.push(backtest.maxDrawdownMarkdownService.subscribe());
|
|
51749
51923
|
}
|
|
51924
|
+
{
|
|
51925
|
+
unList.push(() => this.enable.clear());
|
|
51926
|
+
}
|
|
51750
51927
|
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
51751
|
-
};
|
|
51928
|
+
});
|
|
51752
51929
|
/**
|
|
51753
51930
|
* Disables markdown report services selectively.
|
|
51754
51931
|
*
|
|
@@ -51801,6 +51978,11 @@ class MarkdownUtils {
|
|
|
51801
51978
|
sync,
|
|
51802
51979
|
highest_profit,
|
|
51803
51980
|
});
|
|
51981
|
+
if (this.enable.hasValue()) {
|
|
51982
|
+
const lastSubscription = this.enable();
|
|
51983
|
+
lastSubscription();
|
|
51984
|
+
return;
|
|
51985
|
+
}
|
|
51804
51986
|
if (bt) {
|
|
51805
51987
|
backtest.backtestMarkdownService.unsubscribe();
|
|
51806
51988
|
}
|
package/build/index.mjs
CHANGED
|
@@ -570,6 +570,20 @@ const GLOBAL_CONFIG = {
|
|
|
570
570
|
* Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
|
|
571
571
|
*/
|
|
572
572
|
CC_ENABLE_PPPL_EVERYWHERE: false,
|
|
573
|
+
/**
|
|
574
|
+
* Enables long signals in strategies that are primarily designed for short signals.
|
|
575
|
+
* This allows the strategy to generate and manage long signals in addition to short signals, even if the original design was focused on short trading.
|
|
576
|
+
* This can help expand the strategy's applicability and take advantage of bullish market conditions, but may require additional logic to manage long signal behavior effectively.
|
|
577
|
+
*
|
|
578
|
+
* Default: false (long signals are only enabled in strategies that are designed for them, ensuring strategy logic is aligned with signal types)
|
|
579
|
+
*/
|
|
580
|
+
CC_ENABLE_LONG_SIGNAL: true,
|
|
581
|
+
/**
|
|
582
|
+
* Enables short signals in strategies that are primarily designed for long signals.
|
|
583
|
+
* This allows the strategy to generate and manage short signals in addition to long signals, even if the original design was focused on long trading.
|
|
584
|
+
* This can help expand the strategy's applicability and take advantage of bearish market conditions, but may require additional logic to manage short signal behavior effectively.
|
|
585
|
+
*/
|
|
586
|
+
CC_ENABLE_SHORT_SIGNAL: true,
|
|
573
587
|
/**
|
|
574
588
|
* Enables trailing logic (Trailing Take / Trailing Stop) without requiring absorption conditions.
|
|
575
589
|
* Allows trailing mechanisms to be activated regardless of whether absorption has been detected.
|
|
@@ -4690,6 +4704,12 @@ const validateCommonSignal = (signal) => {
|
|
|
4690
4704
|
if (signal.position !== "long" && signal.position !== "short") {
|
|
4691
4705
|
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
4692
4706
|
}
|
|
4707
|
+
if (signal.position === "long" && !GLOBAL_CONFIG.CC_ENABLE_LONG_SIGNAL) {
|
|
4708
|
+
errors.push(`Long signals are disabled (CC_ENABLE_LONG_SIGNAL=false)`);
|
|
4709
|
+
}
|
|
4710
|
+
if (signal.position === "short" && !GLOBAL_CONFIG.CC_ENABLE_SHORT_SIGNAL) {
|
|
4711
|
+
errors.push(`Short signals are disabled (CC_ENABLE_SHORT_SIGNAL=false)`);
|
|
4712
|
+
}
|
|
4693
4713
|
}
|
|
4694
4714
|
// ЗАЩИТА ОТ NaN/Infinity: все цены должны быть конечными числами
|
|
4695
4715
|
{
|
|
@@ -5525,6 +5545,10 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5525
5545
|
_peak: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5526
5546
|
_fall: { price: signal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5527
5547
|
};
|
|
5548
|
+
{
|
|
5549
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(signalRow, signal.priceOpen);
|
|
5550
|
+
signalRow._fall = { price: signal.priceOpen, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
5551
|
+
}
|
|
5528
5552
|
// Валидируем сигнал перед возвратом
|
|
5529
5553
|
validatePendingSignal(signalRow, currentPrice);
|
|
5530
5554
|
return signalRow;
|
|
@@ -5574,6 +5598,10 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
5574
5598
|
_peak: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5575
5599
|
_fall: { price: currentPrice, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
5576
5600
|
};
|
|
5601
|
+
{
|
|
5602
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(signalRow, currentPrice);
|
|
5603
|
+
signalRow._fall = { price: currentPrice, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
5604
|
+
}
|
|
5577
5605
|
// Валидируем сигнал перед возвратом
|
|
5578
5606
|
validatePendingSignal(signalRow, currentPrice);
|
|
5579
5607
|
return signalRow;
|
|
@@ -6286,6 +6314,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
6286
6314
|
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
6287
6315
|
_fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
6288
6316
|
};
|
|
6317
|
+
{
|
|
6318
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(activatedSignal, activatedSignal.priceOpen);
|
|
6319
|
+
activatedSignal._fall = { price: activatedSignal.priceOpen, timestamp: activationTime, pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen };
|
|
6320
|
+
}
|
|
6289
6321
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
6290
6322
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
6291
6323
|
if (!syncOpenAllowed) {
|
|
@@ -7169,6 +7201,10 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
7169
7201
|
_peak: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
7170
7202
|
_fall: { price: scheduled.priceOpen, timestamp: activationTime, pnlPercentage: 0, pnlCost: 0, pnlEntries: 0, priceClose: 0, priceOpen: 0 },
|
|
7171
7203
|
};
|
|
7204
|
+
{
|
|
7205
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(activatedSignal, activatedSignal.priceOpen);
|
|
7206
|
+
activatedSignal._fall = { price: activatedSignal.priceOpen, timestamp: activationTime, pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen };
|
|
7207
|
+
}
|
|
7172
7208
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
7173
7209
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(activationTime, activatedSignal.priceOpen, activatedSignal, self);
|
|
7174
7210
|
if (!syncOpenAllowed) {
|
|
@@ -7366,6 +7402,10 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
|
|
|
7366
7402
|
_peak: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
7367
7403
|
_fall: { price: activatedSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage: 0, pnlCost: 0, priceClose: 0, priceOpen: 0, pnlEntries: 0 },
|
|
7368
7404
|
};
|
|
7405
|
+
{
|
|
7406
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(pendingSignal, pendingSignal.priceOpen);
|
|
7407
|
+
pendingSignal._fall = { price: pendingSignal.priceOpen, timestamp: candle.timestamp, pnlPercentage, pnlCost, priceClose, priceOpen, pnlEntries };
|
|
7408
|
+
}
|
|
7369
7409
|
// Sync open: if external system rejects — cancel scheduled signal instead of opening
|
|
7370
7410
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(candle.timestamp, pendingSignal.priceOpen, pendingSignal, self);
|
|
7371
7411
|
if (!syncOpenAllowed) {
|
|
@@ -8810,6 +8850,10 @@ class ClientStrategy {
|
|
|
8810
8850
|
_peak: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, pnlEntries: 0, priceOpen: 0 },
|
|
8811
8851
|
_fall: { price: activatedSignal.priceOpen, timestamp: currentTime, pnlPercentage: 0, pnlCost: 0, priceClose: 0, pnlEntries: 0, priceOpen: 0 },
|
|
8812
8852
|
};
|
|
8853
|
+
{
|
|
8854
|
+
const { pnlPercentage, pnlCost, pnlEntries, priceClose, priceOpen } = toProfitLossDto(pendingSignal, pendingSignal.priceOpen);
|
|
8855
|
+
pendingSignal._fall = { price: pendingSignal.priceOpen, timestamp: currentTime, pnlPercentage, pnlCost, priceClose, pnlEntries, priceOpen };
|
|
8856
|
+
}
|
|
8813
8857
|
const syncOpenAllowed = await CALL_SIGNAL_SYNC_OPEN_FN(currentTime, currentPrice, pendingSignal, this);
|
|
8814
8858
|
if (!syncOpenAllowed) {
|
|
8815
8859
|
this.params.logger.info("ClientStrategy tick: user-activated signal rejected by sync", {
|
|
@@ -19373,6 +19417,24 @@ const heat_columns = [
|
|
|
19373
19417
|
format: (data) => data.avgFallPnl !== null ? str(data.avgFallPnl, "%") : "N/A",
|
|
19374
19418
|
isVisible: () => true,
|
|
19375
19419
|
},
|
|
19420
|
+
{
|
|
19421
|
+
key: "sortinoRatio",
|
|
19422
|
+
label: "Sortino",
|
|
19423
|
+
format: (data) => data.sortinoRatio !== null ? str(data.sortinoRatio) : "N/A",
|
|
19424
|
+
isVisible: () => true,
|
|
19425
|
+
},
|
|
19426
|
+
{
|
|
19427
|
+
key: "calmarRatio",
|
|
19428
|
+
label: "Calmar",
|
|
19429
|
+
format: (data) => data.calmarRatio !== null ? str(data.calmarRatio) : "N/A",
|
|
19430
|
+
isVisible: () => true,
|
|
19431
|
+
},
|
|
19432
|
+
{
|
|
19433
|
+
key: "recoveryFactor",
|
|
19434
|
+
label: "Recovery",
|
|
19435
|
+
format: (data) => data.recoveryFactor !== null ? str(data.recoveryFactor) : "N/A",
|
|
19436
|
+
isVisible: () => true,
|
|
19437
|
+
},
|
|
19376
19438
|
];
|
|
19377
19439
|
|
|
19378
19440
|
/**
|
|
@@ -20935,6 +20997,30 @@ const walker_strategy_columns = [
|
|
|
20935
20997
|
: "N/A",
|
|
20936
20998
|
isVisible: () => true,
|
|
20937
20999
|
},
|
|
21000
|
+
{
|
|
21001
|
+
key: "sortinoRatio",
|
|
21002
|
+
label: "Sortino",
|
|
21003
|
+
format: (data) => data.stats.sortinoRatio !== null
|
|
21004
|
+
? `${data.stats.sortinoRatio.toFixed(3)}`
|
|
21005
|
+
: "N/A",
|
|
21006
|
+
isVisible: () => true,
|
|
21007
|
+
},
|
|
21008
|
+
{
|
|
21009
|
+
key: "calmarRatio",
|
|
21010
|
+
label: "Calmar",
|
|
21011
|
+
format: (data) => data.stats.calmarRatio !== null
|
|
21012
|
+
? `${data.stats.calmarRatio.toFixed(3)}`
|
|
21013
|
+
: "N/A",
|
|
21014
|
+
isVisible: () => true,
|
|
21015
|
+
},
|
|
21016
|
+
{
|
|
21017
|
+
key: "recoveryFactor",
|
|
21018
|
+
label: "Recovery",
|
|
21019
|
+
format: (data) => data.stats.recoveryFactor !== null
|
|
21020
|
+
? `${data.stats.recoveryFactor.toFixed(3)}`
|
|
21021
|
+
: "N/A",
|
|
21022
|
+
isVisible: () => true,
|
|
21023
|
+
},
|
|
20938
21024
|
{
|
|
20939
21025
|
key: "firstEventTime",
|
|
20940
21026
|
label: "First Event",
|
|
@@ -21484,13 +21570,13 @@ class ReportBase {
|
|
|
21484
21570
|
* Waits for drain event if write buffer is full.
|
|
21485
21571
|
* Times out after 15 seconds and returns TIMEOUT_SYMBOL.
|
|
21486
21572
|
*/
|
|
21487
|
-
this[_d] = timeout(async (line) => {
|
|
21573
|
+
this[_d] = queued(timeout(async (line) => {
|
|
21488
21574
|
if (!this._stream.write(line)) {
|
|
21489
21575
|
await new Promise((resolve) => {
|
|
21490
21576
|
this._stream.once("drain", resolve);
|
|
21491
21577
|
});
|
|
21492
21578
|
}
|
|
21493
|
-
}, 15000);
|
|
21579
|
+
}, 15000));
|
|
21494
21580
|
LOGGER_SERVICE$3.debug(REPORT_BASE_METHOD_NAME_CTOR, {
|
|
21495
21581
|
reportName: this.reportName,
|
|
21496
21582
|
baseDir,
|
|
@@ -21778,6 +21864,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21778
21864
|
expectedYearlyReturns: null,
|
|
21779
21865
|
avgPeakPnl: null,
|
|
21780
21866
|
avgFallPnl: null,
|
|
21867
|
+
sortinoRatio: null,
|
|
21868
|
+
calmarRatio: null,
|
|
21869
|
+
recoveryFactor: null,
|
|
21781
21870
|
};
|
|
21782
21871
|
}
|
|
21783
21872
|
const totalSignals = this._signalList.length;
|
|
@@ -21811,6 +21900,16 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21811
21900
|
// Calculate average peak and fall PNL across all signals
|
|
21812
21901
|
const avgPeakPnl = this._signalList.reduce((sum, s) => sum + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / totalSignals;
|
|
21813
21902
|
const avgFallPnl = this._signalList.reduce((sum, s) => sum + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / totalSignals;
|
|
21903
|
+
// Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
|
|
21904
|
+
const fallReturns = this._signalList.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
|
|
21905
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
21906
|
+
const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalSignals;
|
|
21907
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
21908
|
+
const sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
|
|
21909
|
+
// Avg absolute peak drawdown per signal — used as denominator for Calmar and Recovery
|
|
21910
|
+
const avgAbsFall = fallReturns.reduce((sum, r) => sum + Math.abs(r), 0) / totalSignals;
|
|
21911
|
+
const calmarRatio = avgAbsFall > 0 ? expectedYearlyReturns / avgAbsFall : 0;
|
|
21912
|
+
const recoveryFactor = avgAbsFall > 0 ? totalPnl / avgAbsFall : 0;
|
|
21814
21913
|
return {
|
|
21815
21914
|
signalList: this._signalList,
|
|
21816
21915
|
totalSignals,
|
|
@@ -21826,6 +21925,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21826
21925
|
expectedYearlyReturns: isUnsafe$3(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
21827
21926
|
avgPeakPnl: isUnsafe$3(avgPeakPnl) ? null : avgPeakPnl,
|
|
21828
21927
|
avgFallPnl: isUnsafe$3(avgFallPnl) ? null : avgFallPnl,
|
|
21928
|
+
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
21929
|
+
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
21930
|
+
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
21829
21931
|
};
|
|
21830
21932
|
}
|
|
21831
21933
|
/**
|
|
@@ -21872,6 +21974,9 @@ let ReportStorage$a = class ReportStorage {
|
|
|
21872
21974
|
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
21873
21975
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
21874
21976
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
21977
|
+
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
21978
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
21979
|
+
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
21875
21980
|
].join("\n");
|
|
21876
21981
|
}
|
|
21877
21982
|
/**
|
|
@@ -22472,6 +22577,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22472
22577
|
expectedYearlyReturns: null,
|
|
22473
22578
|
avgPeakPnl: null,
|
|
22474
22579
|
avgFallPnl: null,
|
|
22580
|
+
sortinoRatio: null,
|
|
22581
|
+
calmarRatio: null,
|
|
22582
|
+
recoveryFactor: null,
|
|
22475
22583
|
};
|
|
22476
22584
|
}
|
|
22477
22585
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
@@ -22521,6 +22629,21 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22521
22629
|
const avgFallPnl = totalClosed > 0
|
|
22522
22630
|
? closedEvents.reduce((sum, e) => sum + (e.fallPnl || 0), 0) / totalClosed
|
|
22523
22631
|
: 0;
|
|
22632
|
+
// Downside per signal: fallPnl captures the worst intra-trade dip (maxDrawdown.pnlPercentage)
|
|
22633
|
+
const fallReturns = closedEvents.map((e) => e.fallPnl || 0);
|
|
22634
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
22635
|
+
let sortinoRatio = 0;
|
|
22636
|
+
if (totalClosed > 0) {
|
|
22637
|
+
const fallVariance = fallReturns.reduce((sum, r) => sum + Math.pow(r, 2), 0) / totalClosed;
|
|
22638
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
22639
|
+
sortinoRatio = fallDeviation > 0 ? avgPnl / fallDeviation : 0;
|
|
22640
|
+
}
|
|
22641
|
+
// Avg absolute peak drawdown per signal — denominator for Calmar and Recovery
|
|
22642
|
+
const avgAbsFall = totalClosed > 0
|
|
22643
|
+
? fallReturns.reduce((sum, r) => sum + Math.abs(r), 0) / totalClosed
|
|
22644
|
+
: 0;
|
|
22645
|
+
const calmarRatio = avgAbsFall > 0 ? expectedYearlyReturns / avgAbsFall : 0;
|
|
22646
|
+
const recoveryFactor = avgAbsFall > 0 ? totalPnl / avgAbsFall : 0;
|
|
22524
22647
|
return {
|
|
22525
22648
|
eventList: this._eventList,
|
|
22526
22649
|
totalEvents: this._eventList.length,
|
|
@@ -22537,6 +22660,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22537
22660
|
expectedYearlyReturns: isUnsafe$2(expectedYearlyReturns) ? null : expectedYearlyReturns,
|
|
22538
22661
|
avgPeakPnl: isUnsafe$2(avgPeakPnl) ? null : avgPeakPnl,
|
|
22539
22662
|
avgFallPnl: isUnsafe$2(avgFallPnl) ? null : avgFallPnl,
|
|
22663
|
+
sortinoRatio: isUnsafe$2(sortinoRatio) ? null : sortinoRatio,
|
|
22664
|
+
calmarRatio: isUnsafe$2(calmarRatio) ? null : calmarRatio,
|
|
22665
|
+
recoveryFactor: isUnsafe$2(recoveryFactor) ? null : recoveryFactor,
|
|
22540
22666
|
};
|
|
22541
22667
|
}
|
|
22542
22668
|
/**
|
|
@@ -22583,6 +22709,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
22583
22709
|
`**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
|
|
22584
22710
|
`**Avg Peak PNL:** ${stats.avgPeakPnl === null ? "N/A" : `${stats.avgPeakPnl > 0 ? "+" : ""}${stats.avgPeakPnl.toFixed(2)}% (higher is better)`}`,
|
|
22585
22711
|
`**Avg Max Drawdown PNL:** ${stats.avgFallPnl === null ? "N/A" : `${stats.avgFallPnl.toFixed(2)}% (closer to 0 is better)`}`,
|
|
22712
|
+
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
22713
|
+
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
22714
|
+
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
22586
22715
|
].join("\n");
|
|
22587
22716
|
}
|
|
22588
22717
|
/**
|
|
@@ -24028,6 +24157,9 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
24028
24157
|
"",
|
|
24029
24158
|
`**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
|
|
24030
24159
|
`**Total Signals:** ${bestStrategySignals}`,
|
|
24160
|
+
`**Sortino Ratio:** ${results.bestStats?.sortinoRatio != null ? `${results.bestStats.sortinoRatio.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24161
|
+
`**Calmar Ratio:** ${results.bestStats?.calmarRatio != null ? `${results.bestStats.calmarRatio.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24162
|
+
`**Recovery Factor:** ${results.bestStats?.recoveryFactor != null ? `${results.bestStats.recoveryFactor.toFixed(3)} (higher is better)` : "N/A"}`,
|
|
24031
24163
|
"",
|
|
24032
24164
|
"## Top Strategies Comparison",
|
|
24033
24165
|
"",
|
|
@@ -24494,6 +24626,27 @@ class HeatmapStorage {
|
|
|
24494
24626
|
avgPeakPnl = signals.reduce((acc, s) => acc + (s.signal.peakProfit?.pnlPercentage ?? 0), 0) / signals.length;
|
|
24495
24627
|
avgFallPnl = signals.reduce((acc, s) => acc + (s.signal.maxDrawdown?.pnlPercentage ?? 0), 0) / signals.length;
|
|
24496
24628
|
}
|
|
24629
|
+
// Downside per signal: maxDrawdown.pnlPercentage captures the worst intra-trade dip
|
|
24630
|
+
const fallReturns = signals.map((s) => s.signal.maxDrawdown?.pnlPercentage ?? 0);
|
|
24631
|
+
// Calculate Sortino Ratio: avgPnl / stdDev(maxDrawdown per signal)
|
|
24632
|
+
let sortinoRatio = null;
|
|
24633
|
+
if (signals.length > 0 && avgPnl !== null) {
|
|
24634
|
+
const fallVariance = fallReturns.reduce((acc, r) => acc + Math.pow(r, 2), 0) / signals.length;
|
|
24635
|
+
const fallDeviation = Math.sqrt(fallVariance);
|
|
24636
|
+
if (fallDeviation > 0) {
|
|
24637
|
+
sortinoRatio = avgPnl / fallDeviation;
|
|
24638
|
+
}
|
|
24639
|
+
}
|
|
24640
|
+
// Avg absolute peak drawdown per signal — denominator for Calmar and Recovery
|
|
24641
|
+
const avgAbsFall = signals.length > 0
|
|
24642
|
+
? fallReturns.reduce((acc, r) => acc + Math.abs(r), 0) / signals.length
|
|
24643
|
+
: 0;
|
|
24644
|
+
let calmarRatio = null;
|
|
24645
|
+
let recoveryFactor = null;
|
|
24646
|
+
if (avgAbsFall > 0 && totalPnl !== null) {
|
|
24647
|
+
calmarRatio = totalPnl / avgAbsFall;
|
|
24648
|
+
recoveryFactor = totalPnl / avgAbsFall;
|
|
24649
|
+
}
|
|
24497
24650
|
// Apply safe math checks
|
|
24498
24651
|
if (isUnsafe(winRate))
|
|
24499
24652
|
winRate = null;
|
|
@@ -24519,6 +24672,12 @@ class HeatmapStorage {
|
|
|
24519
24672
|
avgPeakPnl = null;
|
|
24520
24673
|
if (isUnsafe(avgFallPnl))
|
|
24521
24674
|
avgFallPnl = null;
|
|
24675
|
+
if (isUnsafe(sortinoRatio))
|
|
24676
|
+
sortinoRatio = null;
|
|
24677
|
+
if (isUnsafe(calmarRatio))
|
|
24678
|
+
calmarRatio = null;
|
|
24679
|
+
if (isUnsafe(recoveryFactor))
|
|
24680
|
+
recoveryFactor = null;
|
|
24522
24681
|
return {
|
|
24523
24682
|
symbol,
|
|
24524
24683
|
totalPnl,
|
|
@@ -24538,6 +24697,9 @@ class HeatmapStorage {
|
|
|
24538
24697
|
expectancy,
|
|
24539
24698
|
avgPeakPnl,
|
|
24540
24699
|
avgFallPnl,
|
|
24700
|
+
sortinoRatio,
|
|
24701
|
+
calmarRatio,
|
|
24702
|
+
recoveryFactor,
|
|
24541
24703
|
};
|
|
24542
24704
|
}
|
|
24543
24705
|
/**
|
|
@@ -29404,6 +29566,9 @@ class WalkerReportService {
|
|
|
29404
29566
|
annualizedSharpeRatio: data.stats.annualizedSharpeRatio,
|
|
29405
29567
|
certaintyRatio: data.stats.certaintyRatio,
|
|
29406
29568
|
expectedYearlyReturns: data.stats.expectedYearlyReturns,
|
|
29569
|
+
sortinoRatio: data.stats.sortinoRatio,
|
|
29570
|
+
calmarRatio: data.stats.calmarRatio,
|
|
29571
|
+
recoveryFactor: data.stats.recoveryFactor,
|
|
29407
29572
|
firstEventTime,
|
|
29408
29573
|
lastEventTime,
|
|
29409
29574
|
}, {
|
|
@@ -50718,7 +50883,8 @@ class DumpAdapter {
|
|
|
50718
50883
|
const unClose = signalEmitter
|
|
50719
50884
|
.filter(({ action }) => action === "closed")
|
|
50720
50885
|
.connect(({ signal }) => handleDispose(signal.id));
|
|
50721
|
-
|
|
50886
|
+
const unEnable = () => this.enable.clear();
|
|
50887
|
+
return compose(() => unCancel(), () => unClose(), () => unEnable());
|
|
50722
50888
|
});
|
|
50723
50889
|
/**
|
|
50724
50890
|
* Deactivates the adapter by unsubscribing from signal lifecycle events.
|
|
@@ -51405,7 +51571,7 @@ class ReportUtils {
|
|
|
51405
51571
|
*
|
|
51406
51572
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
51407
51573
|
*/
|
|
51408
|
-
this.enable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
51574
|
+
this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$2) => {
|
|
51409
51575
|
LOGGER_SERVICE$2.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
51410
51576
|
backtest: bt,
|
|
51411
51577
|
breakeven,
|
|
@@ -51459,8 +51625,11 @@ class ReportUtils {
|
|
|
51459
51625
|
if (max_drawdown) {
|
|
51460
51626
|
unList.push(backtest.maxDrawdownReportService.subscribe());
|
|
51461
51627
|
}
|
|
51628
|
+
{
|
|
51629
|
+
unList.push(() => this.enable.clear());
|
|
51630
|
+
}
|
|
51462
51631
|
return compose(...unList.map((un) => () => void un()));
|
|
51463
|
-
};
|
|
51632
|
+
});
|
|
51464
51633
|
/**
|
|
51465
51634
|
* Disables report services selectively.
|
|
51466
51635
|
*
|
|
@@ -51511,6 +51680,11 @@ class ReportUtils {
|
|
|
51511
51680
|
strategy,
|
|
51512
51681
|
sync,
|
|
51513
51682
|
});
|
|
51683
|
+
if (this.enable.hasValue()) {
|
|
51684
|
+
const lastSubscription = this.enable();
|
|
51685
|
+
lastSubscription();
|
|
51686
|
+
return;
|
|
51687
|
+
}
|
|
51514
51688
|
if (bt) {
|
|
51515
51689
|
backtest.backtestReportService.unsubscribe();
|
|
51516
51690
|
}
|
|
@@ -51672,7 +51846,7 @@ class MarkdownUtils {
|
|
|
51672
51846
|
*
|
|
51673
51847
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
51674
51848
|
*/
|
|
51675
|
-
this.enable = ({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
51849
|
+
this.enable = singleshot(({ backtest: bt = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, sync = false, highest_profit = false, max_drawdown = false, } = WILDCARD_TARGET$1) => {
|
|
51676
51850
|
LOGGER_SERVICE$1.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
51677
51851
|
backtest: bt,
|
|
51678
51852
|
breakeven,
|
|
@@ -51727,8 +51901,11 @@ class MarkdownUtils {
|
|
|
51727
51901
|
if (max_drawdown) {
|
|
51728
51902
|
unList.push(backtest.maxDrawdownMarkdownService.subscribe());
|
|
51729
51903
|
}
|
|
51904
|
+
{
|
|
51905
|
+
unList.push(() => this.enable.clear());
|
|
51906
|
+
}
|
|
51730
51907
|
return compose(...unList.map((un) => () => void un()));
|
|
51731
|
-
};
|
|
51908
|
+
});
|
|
51732
51909
|
/**
|
|
51733
51910
|
* Disables markdown report services selectively.
|
|
51734
51911
|
*
|
|
@@ -51781,6 +51958,11 @@ class MarkdownUtils {
|
|
|
51781
51958
|
sync,
|
|
51782
51959
|
highest_profit,
|
|
51783
51960
|
});
|
|
51961
|
+
if (this.enable.hasValue()) {
|
|
51962
|
+
const lastSubscription = this.enable();
|
|
51963
|
+
lastSubscription();
|
|
51964
|
+
return;
|
|
51965
|
+
}
|
|
51784
51966
|
if (bt) {
|
|
51785
51967
|
backtest.backtestMarkdownService.unsubscribe();
|
|
51786
51968
|
}
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -4466,6 +4466,12 @@ interface BacktestStatisticsModel {
|
|
|
4466
4466
|
avgPeakPnl: number | null;
|
|
4467
4467
|
/** Average fall PNL percentage across all signals (_fall.pnlPercentage), null if unsafe. Lower (more negative) means deeper drawdowns. */
|
|
4468
4468
|
avgFallPnl: number | null;
|
|
4469
|
+
/** Sortino Ratio (avgPnl / downside deviation — stdDev of losses only), null if unsafe. Higher is better. */
|
|
4470
|
+
sortinoRatio: number | null;
|
|
4471
|
+
/** Calmar Ratio (annualized expected return / max drawdown), null if unsafe. Higher is better. */
|
|
4472
|
+
calmarRatio: number | null;
|
|
4473
|
+
/** Recovery Factor (totalPnl / max drawdown), null if unsafe. Higher is better. */
|
|
4474
|
+
recoveryFactor: number | null;
|
|
4469
4475
|
}
|
|
4470
4476
|
|
|
4471
4477
|
/**
|
|
@@ -6478,6 +6484,20 @@ declare const GLOBAL_CONFIG: {
|
|
|
6478
6484
|
* Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
|
|
6479
6485
|
*/
|
|
6480
6486
|
CC_ENABLE_PPPL_EVERYWHERE: boolean;
|
|
6487
|
+
/**
|
|
6488
|
+
* Enables long signals in strategies that are primarily designed for short signals.
|
|
6489
|
+
* This allows the strategy to generate and manage long signals in addition to short signals, even if the original design was focused on short trading.
|
|
6490
|
+
* This can help expand the strategy's applicability and take advantage of bullish market conditions, but may require additional logic to manage long signal behavior effectively.
|
|
6491
|
+
*
|
|
6492
|
+
* Default: false (long signals are only enabled in strategies that are designed for them, ensuring strategy logic is aligned with signal types)
|
|
6493
|
+
*/
|
|
6494
|
+
CC_ENABLE_LONG_SIGNAL: boolean;
|
|
6495
|
+
/**
|
|
6496
|
+
* Enables short signals in strategies that are primarily designed for long signals.
|
|
6497
|
+
* This allows the strategy to generate and manage short signals in addition to long signals, even if the original design was focused on long trading.
|
|
6498
|
+
* This can help expand the strategy's applicability and take advantage of bearish market conditions, but may require additional logic to manage short signal behavior effectively.
|
|
6499
|
+
*/
|
|
6500
|
+
CC_ENABLE_SHORT_SIGNAL: boolean;
|
|
6481
6501
|
/**
|
|
6482
6502
|
* Enables trailing logic (Trailing Take / Trailing Stop) without requiring absorption conditions.
|
|
6483
6503
|
* Allows trailing mechanisms to be activated regardless of whether absorption has been detected.
|
|
@@ -6626,6 +6646,8 @@ declare function getConfig(): {
|
|
|
6626
6646
|
CC_ENABLE_CANDLE_FETCH_MUTEX: boolean;
|
|
6627
6647
|
CC_ENABLE_DCA_EVERYWHERE: boolean;
|
|
6628
6648
|
CC_ENABLE_PPPL_EVERYWHERE: boolean;
|
|
6649
|
+
CC_ENABLE_LONG_SIGNAL: boolean;
|
|
6650
|
+
CC_ENABLE_SHORT_SIGNAL: boolean;
|
|
6629
6651
|
CC_ENABLE_TRAILING_EVERYWHERE: boolean;
|
|
6630
6652
|
CC_POSITION_ENTRY_COST: number;
|
|
6631
6653
|
};
|
|
@@ -6682,6 +6704,8 @@ declare function getDefaultConfig(): Readonly<{
|
|
|
6682
6704
|
CC_ENABLE_CANDLE_FETCH_MUTEX: boolean;
|
|
6683
6705
|
CC_ENABLE_DCA_EVERYWHERE: boolean;
|
|
6684
6706
|
CC_ENABLE_PPPL_EVERYWHERE: boolean;
|
|
6707
|
+
CC_ENABLE_LONG_SIGNAL: boolean;
|
|
6708
|
+
CC_ENABLE_SHORT_SIGNAL: boolean;
|
|
6685
6709
|
CC_ENABLE_TRAILING_EVERYWHERE: boolean;
|
|
6686
6710
|
CC_POSITION_ENTRY_COST: number;
|
|
6687
6711
|
}>;
|
|
@@ -10259,6 +10283,12 @@ interface IHeatmapRow {
|
|
|
10259
10283
|
avgPeakPnl: number | null;
|
|
10260
10284
|
/** Average fall PNL percentage across all trades (_fall.pnlPercentage). Closer to 0 is better. */
|
|
10261
10285
|
avgFallPnl: number | null;
|
|
10286
|
+
/** Sortino Ratio (avgPnl / downside deviation — stdDev of losses only). Higher is better. */
|
|
10287
|
+
sortinoRatio: number | null;
|
|
10288
|
+
/** Calmar Ratio (totalPnl / maxDrawdown). Higher is better. */
|
|
10289
|
+
calmarRatio: number | null;
|
|
10290
|
+
/** Recovery Factor (totalPnl / maxDrawdown). Higher is better. */
|
|
10291
|
+
recoveryFactor: number | null;
|
|
10262
10292
|
}
|
|
10263
10293
|
|
|
10264
10294
|
/**
|
|
@@ -12131,6 +12161,12 @@ interface LiveStatisticsModel {
|
|
|
12131
12161
|
avgPeakPnl: number | null;
|
|
12132
12162
|
/** Average fall PNL percentage across all closed signals (_fall.pnlPercentage), null if unsafe. Closer to 0 is better. */
|
|
12133
12163
|
avgFallPnl: number | null;
|
|
12164
|
+
/** Sortino Ratio (avgPnl / downside deviation — stdDev of losses only), null if unsafe. Higher is better. */
|
|
12165
|
+
sortinoRatio: number | null;
|
|
12166
|
+
/** Calmar Ratio (annualized expected return / max drawdown), null if unsafe. Higher is better. */
|
|
12167
|
+
calmarRatio: number | null;
|
|
12168
|
+
/** Recovery Factor (totalPnl / max drawdown), null if unsafe. Higher is better. */
|
|
12169
|
+
recoveryFactor: number | null;
|
|
12134
12170
|
}
|
|
12135
12171
|
|
|
12136
12172
|
/**
|
|
@@ -14593,7 +14629,7 @@ declare class ReportBase implements TReportBase {
|
|
|
14593
14629
|
* Waits for drain event if write buffer is full.
|
|
14594
14630
|
* Times out after 15 seconds and returns TIMEOUT_SYMBOL.
|
|
14595
14631
|
*/
|
|
14596
|
-
[WRITE_SAFE_SYMBOL]:
|
|
14632
|
+
[WRITE_SAFE_SYMBOL]: functools_kit.IWrappedQueuedFn<symbol | void, [line: string]>;
|
|
14597
14633
|
/**
|
|
14598
14634
|
* Initializes the JSONL file and write stream.
|
|
14599
14635
|
* Safe to call multiple times - singleshot ensures one-time execution.
|
|
@@ -14717,7 +14753,7 @@ declare class ReportUtils {
|
|
|
14717
14753
|
*
|
|
14718
14754
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
14719
14755
|
*/
|
|
14720
|
-
enable: ({ backtest: bt, breakeven, heat, live, partial, performance, risk, schedule, walker, strategy, sync, highest_profit, max_drawdown, }?: Partial<IReportTarget>) => (...args: any[]) => any
|
|
14756
|
+
enable: (({ backtest: bt, breakeven, heat, live, partial, performance, risk, schedule, walker, strategy, sync, highest_profit, max_drawdown, }?: Partial<IReportTarget>) => (...args: any[]) => any) & functools_kit.ISingleshotClearable<({ backtest: bt, breakeven, heat, live, partial, performance, risk, schedule, walker, strategy, sync, highest_profit, max_drawdown, }?: Partial<IReportTarget>) => (...args: any[]) => any>;
|
|
14721
14757
|
/**
|
|
14722
14758
|
* Disables report services selectively.
|
|
14723
14759
|
*
|
|
@@ -14834,7 +14870,7 @@ declare class MarkdownUtils {
|
|
|
14834
14870
|
*
|
|
14835
14871
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
14836
14872
|
*/
|
|
14837
|
-
enable: ({ backtest: bt, breakeven, heat, live, partial, performance, strategy, risk, schedule, walker, sync, highest_profit, max_drawdown, }?: Partial<IMarkdownTarget>) => (...args: any[]) => any
|
|
14873
|
+
enable: (({ backtest: bt, breakeven, heat, live, partial, performance, strategy, risk, schedule, walker, sync, highest_profit, max_drawdown, }?: Partial<IMarkdownTarget>) => (...args: any[]) => any) & functools_kit.ISingleshotClearable<({ backtest: bt, breakeven, heat, live, partial, performance, strategy, risk, schedule, walker, sync, highest_profit, max_drawdown, }?: Partial<IMarkdownTarget>) => (...args: any[]) => any>;
|
|
14838
14874
|
/**
|
|
14839
14875
|
* Disables markdown report services selectively.
|
|
14840
14876
|
*
|