backtest-kit 1.5.16 → 1.5.17
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 +767 -94
- package/build/index.mjs +765 -95
- package/package.json +1 -1
- package/types.d.ts +523 -3
package/build/index.cjs
CHANGED
|
@@ -202,6 +202,7 @@ const markdownServices$1 = {
|
|
|
202
202
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
203
203
|
partialMarkdownService: Symbol('partialMarkdownService'),
|
|
204
204
|
outlineMarkdownService: Symbol('outlineMarkdownService'),
|
|
205
|
+
riskMarkdownService: Symbol('riskMarkdownService'),
|
|
205
206
|
};
|
|
206
207
|
const validationServices$1 = {
|
|
207
208
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -442,11 +443,12 @@ const GET_CANDLES_FN = async (dto, since, self) => {
|
|
|
442
443
|
}
|
|
443
444
|
catch (err) {
|
|
444
445
|
const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
|
|
445
|
-
|
|
446
|
+
const payload = {
|
|
446
447
|
error: functoolsKit.errorData(err),
|
|
447
448
|
message: functoolsKit.getErrorMessage(err),
|
|
448
|
-
}
|
|
449
|
-
|
|
449
|
+
};
|
|
450
|
+
self.params.logger.warn(message, payload);
|
|
451
|
+
console.warn(message, payload);
|
|
450
452
|
lastError = err;
|
|
451
453
|
await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
|
|
452
454
|
}
|
|
@@ -1725,6 +1727,12 @@ const partialProfitSubject = new functoolsKit.Subject();
|
|
|
1725
1727
|
* Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
|
|
1726
1728
|
*/
|
|
1727
1729
|
const partialLossSubject = new functoolsKit.Subject();
|
|
1730
|
+
/**
|
|
1731
|
+
* Risk rejection emitter for risk management violations.
|
|
1732
|
+
* Emits ONLY when a signal is rejected due to risk validation failure.
|
|
1733
|
+
* Does not emit for allowed signals (prevents spam).
|
|
1734
|
+
*/
|
|
1735
|
+
const riskSubject = new functoolsKit.Subject();
|
|
1728
1736
|
|
|
1729
1737
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
1730
1738
|
__proto__: null,
|
|
@@ -1739,6 +1747,7 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
1739
1747
|
progressBacktestEmitter: progressBacktestEmitter,
|
|
1740
1748
|
progressOptimizerEmitter: progressOptimizerEmitter,
|
|
1741
1749
|
progressWalkerEmitter: progressWalkerEmitter,
|
|
1750
|
+
riskSubject: riskSubject,
|
|
1742
1751
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
1743
1752
|
signalEmitter: signalEmitter,
|
|
1744
1753
|
signalLiveEmitter: signalLiveEmitter,
|
|
@@ -1825,6 +1834,9 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1825
1834
|
if (signal.position === undefined || signal.position === null) {
|
|
1826
1835
|
errors.push('position is required and must be "long" or "short"');
|
|
1827
1836
|
}
|
|
1837
|
+
if (signal.position !== "long" && signal.position !== "short") {
|
|
1838
|
+
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
1839
|
+
}
|
|
1828
1840
|
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
1829
1841
|
if (!isFinite(currentPrice)) {
|
|
1830
1842
|
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
@@ -2111,10 +2123,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
2111
2123
|
}, {
|
|
2112
2124
|
defaultValue: null,
|
|
2113
2125
|
fallback: (error) => {
|
|
2114
|
-
|
|
2126
|
+
const message = "ClientStrategy exception thrown";
|
|
2127
|
+
const payload = {
|
|
2115
2128
|
error: functoolsKit.errorData(error),
|
|
2116
2129
|
message: functoolsKit.getErrorMessage(error),
|
|
2117
|
-
}
|
|
2130
|
+
};
|
|
2131
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2132
|
+
console.warn(message, payload);
|
|
2118
2133
|
errorEmitter.next(error);
|
|
2119
2134
|
},
|
|
2120
2135
|
});
|
|
@@ -2805,7 +2820,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2805
2820
|
// Moving towards TP
|
|
2806
2821
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2807
2822
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2808
|
-
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2823
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2809
2824
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2810
2825
|
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2811
2826
|
}
|
|
@@ -2814,7 +2829,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2814
2829
|
// Moving towards SL
|
|
2815
2830
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2816
2831
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2817
|
-
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2832
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2818
2833
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2819
2834
|
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2820
2835
|
}
|
|
@@ -2827,7 +2842,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2827
2842
|
// Moving towards TP
|
|
2828
2843
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2829
2844
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2830
|
-
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2845
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2831
2846
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2832
2847
|
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2833
2848
|
}
|
|
@@ -2836,7 +2851,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2836
2851
|
// Moving towards SL
|
|
2837
2852
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2838
2853
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2839
|
-
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2854
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2840
2855
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2841
2856
|
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2842
2857
|
}
|
|
@@ -3791,10 +3806,13 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
|
|
|
3791
3806
|
}, {
|
|
3792
3807
|
defaultValue: false,
|
|
3793
3808
|
fallback: (error) => {
|
|
3794
|
-
|
|
3809
|
+
const message = "ClientRisk exception thrown";
|
|
3810
|
+
const payload = {
|
|
3795
3811
|
error: functoolsKit.errorData(error),
|
|
3796
3812
|
message: functoolsKit.getErrorMessage(error),
|
|
3797
|
-
}
|
|
3813
|
+
};
|
|
3814
|
+
backtest$1.loggerService.warn(message, payload);
|
|
3815
|
+
console.warn(message, payload);
|
|
3798
3816
|
validationSubject.next(error);
|
|
3799
3817
|
},
|
|
3800
3818
|
});
|
|
@@ -3864,17 +3882,25 @@ class ClientRisk {
|
|
|
3864
3882
|
};
|
|
3865
3883
|
// Execute custom validations
|
|
3866
3884
|
let isValid = true;
|
|
3885
|
+
let rejectionNote = "N/A";
|
|
3867
3886
|
if (this.params.validations) {
|
|
3868
3887
|
for (const validation of this.params.validations) {
|
|
3869
3888
|
if (functoolsKit.not(await DO_VALIDATION_FN(typeof validation === "function"
|
|
3870
3889
|
? validation
|
|
3871
3890
|
: validation.validate, payload))) {
|
|
3872
3891
|
isValid = false;
|
|
3892
|
+
// Capture note from validation if available
|
|
3893
|
+
if (typeof validation !== "function" && validation.note) {
|
|
3894
|
+
rejectionNote = validation.note;
|
|
3895
|
+
}
|
|
3873
3896
|
break;
|
|
3874
3897
|
}
|
|
3875
3898
|
}
|
|
3876
3899
|
}
|
|
3877
3900
|
if (!isValid) {
|
|
3901
|
+
// Call params.onRejected for riskSubject emission
|
|
3902
|
+
await this.params.onRejected(params.symbol, params, riskMap.size, rejectionNote, Date.now());
|
|
3903
|
+
// Call schema callbacks.onRejected if defined
|
|
3878
3904
|
if (this.params.callbacks?.onRejected) {
|
|
3879
3905
|
this.params.callbacks.onRejected(params.symbol, params);
|
|
3880
3906
|
}
|
|
@@ -3937,6 +3963,28 @@ class ClientRisk {
|
|
|
3937
3963
|
}
|
|
3938
3964
|
}
|
|
3939
3965
|
|
|
3966
|
+
/**
|
|
3967
|
+
* Callback function for emitting risk rejection events to riskSubject.
|
|
3968
|
+
*
|
|
3969
|
+
* Called by ClientRisk when a signal is rejected due to risk validation failure.
|
|
3970
|
+
* Emits RiskContract event to all subscribers.
|
|
3971
|
+
*
|
|
3972
|
+
* @param symbol - Trading pair symbol
|
|
3973
|
+
* @param params - Risk check arguments
|
|
3974
|
+
* @param activePositionCount - Number of active positions at rejection time
|
|
3975
|
+
* @param comment - Rejection reason from validation note or "N/A"
|
|
3976
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
3977
|
+
*/
|
|
3978
|
+
const COMMIT_REJECTION_FN = async (symbol, params, activePositionCount, comment, timestamp) => await riskSubject.next({
|
|
3979
|
+
symbol,
|
|
3980
|
+
pendingSignal: params.pendingSignal,
|
|
3981
|
+
strategyName: params.strategyName,
|
|
3982
|
+
exchangeName: params.exchangeName,
|
|
3983
|
+
currentPrice: params.currentPrice,
|
|
3984
|
+
activePositionCount,
|
|
3985
|
+
comment,
|
|
3986
|
+
timestamp,
|
|
3987
|
+
});
|
|
3940
3988
|
/**
|
|
3941
3989
|
* Connection service routing risk operations to correct ClientRisk instance.
|
|
3942
3990
|
*
|
|
@@ -3987,6 +4035,7 @@ class RiskConnectionService {
|
|
|
3987
4035
|
return new ClientRisk({
|
|
3988
4036
|
...schema,
|
|
3989
4037
|
logger: this.loggerService,
|
|
4038
|
+
onRejected: COMMIT_REJECTION_FN,
|
|
3990
4039
|
});
|
|
3991
4040
|
});
|
|
3992
4041
|
/**
|
|
@@ -3994,6 +4043,7 @@ class RiskConnectionService {
|
|
|
3994
4043
|
*
|
|
3995
4044
|
* Routes to appropriate ClientRisk instance based on provided context.
|
|
3996
4045
|
* Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
|
|
4046
|
+
* ClientRisk will emit riskSubject event via onRejected callback when signal is rejected.
|
|
3997
4047
|
*
|
|
3998
4048
|
* @param params - Risk check arguments (portfolio state, position details)
|
|
3999
4049
|
* @param context - Execution context with risk name
|
|
@@ -5982,7 +6032,7 @@ function isUnsafe$3(value) {
|
|
|
5982
6032
|
}
|
|
5983
6033
|
return false;
|
|
5984
6034
|
}
|
|
5985
|
-
const columns$
|
|
6035
|
+
const columns$5 = [
|
|
5986
6036
|
{
|
|
5987
6037
|
key: "signalId",
|
|
5988
6038
|
label: "Signal ID",
|
|
@@ -6056,11 +6106,13 @@ const columns$4 = [
|
|
|
6056
6106
|
format: (data) => new Date(data.closeTimestamp).toISOString(),
|
|
6057
6107
|
},
|
|
6058
6108
|
];
|
|
6109
|
+
/** Maximum number of signals to store in backtest reports */
|
|
6110
|
+
const MAX_EVENTS$6 = 250;
|
|
6059
6111
|
/**
|
|
6060
6112
|
* Storage class for accumulating closed signals per strategy.
|
|
6061
6113
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
6062
6114
|
*/
|
|
6063
|
-
let ReportStorage$
|
|
6115
|
+
let ReportStorage$5 = class ReportStorage {
|
|
6064
6116
|
constructor() {
|
|
6065
6117
|
/** Internal list of all closed signals for this strategy */
|
|
6066
6118
|
this._signalList = [];
|
|
@@ -6071,7 +6123,11 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
6071
6123
|
* @param data - Closed signal data with PNL and close reason
|
|
6072
6124
|
*/
|
|
6073
6125
|
addSignal(data) {
|
|
6074
|
-
this._signalList.
|
|
6126
|
+
this._signalList.unshift(data);
|
|
6127
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
6128
|
+
if (this._signalList.length > MAX_EVENTS$6) {
|
|
6129
|
+
this._signalList.pop();
|
|
6130
|
+
}
|
|
6075
6131
|
}
|
|
6076
6132
|
/**
|
|
6077
6133
|
* Calculates statistical data from closed signals (Controller).
|
|
@@ -6154,9 +6210,9 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
6154
6210
|
"No signals closed yet."
|
|
6155
6211
|
].join("\n");
|
|
6156
6212
|
}
|
|
6157
|
-
const header = columns$
|
|
6158
|
-
const separator = columns$
|
|
6159
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
6213
|
+
const header = columns$5.map((col) => col.label);
|
|
6214
|
+
const separator = columns$5.map(() => "---");
|
|
6215
|
+
const rows = this._signalList.map((closedSignal) => columns$5.map((col) => col.format(closedSignal)));
|
|
6160
6216
|
const tableData = [header, separator, ...rows];
|
|
6161
6217
|
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6162
6218
|
return [
|
|
@@ -6232,7 +6288,7 @@ class BacktestMarkdownService {
|
|
|
6232
6288
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
6233
6289
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
6234
6290
|
*/
|
|
6235
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
6291
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$5());
|
|
6236
6292
|
/**
|
|
6237
6293
|
* Processes tick events and accumulates closed signals.
|
|
6238
6294
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -6403,7 +6459,7 @@ function isUnsafe$2(value) {
|
|
|
6403
6459
|
}
|
|
6404
6460
|
return false;
|
|
6405
6461
|
}
|
|
6406
|
-
const columns$
|
|
6462
|
+
const columns$4 = [
|
|
6407
6463
|
{
|
|
6408
6464
|
key: "timestamp",
|
|
6409
6465
|
label: "Timestamp",
|
|
@@ -6487,12 +6543,12 @@ const columns$3 = [
|
|
|
6487
6543
|
},
|
|
6488
6544
|
];
|
|
6489
6545
|
/** Maximum number of events to store in live trading reports */
|
|
6490
|
-
const MAX_EVENTS$
|
|
6546
|
+
const MAX_EVENTS$5 = 250;
|
|
6491
6547
|
/**
|
|
6492
6548
|
* Storage class for accumulating all tick events per strategy.
|
|
6493
6549
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
6494
6550
|
*/
|
|
6495
|
-
let ReportStorage$
|
|
6551
|
+
let ReportStorage$4 = class ReportStorage {
|
|
6496
6552
|
constructor() {
|
|
6497
6553
|
/** Internal list of all tick events for this strategy */
|
|
6498
6554
|
this._eventList = [];
|
|
@@ -6519,9 +6575,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6519
6575
|
return;
|
|
6520
6576
|
}
|
|
6521
6577
|
{
|
|
6522
|
-
this._eventList.
|
|
6523
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6524
|
-
this._eventList.
|
|
6578
|
+
this._eventList.unshift(newEvent);
|
|
6579
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6580
|
+
this._eventList.pop();
|
|
6525
6581
|
}
|
|
6526
6582
|
}
|
|
6527
6583
|
}
|
|
@@ -6531,7 +6587,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6531
6587
|
* @param data - Opened tick result
|
|
6532
6588
|
*/
|
|
6533
6589
|
addOpenedEvent(data) {
|
|
6534
|
-
this._eventList.
|
|
6590
|
+
this._eventList.unshift({
|
|
6535
6591
|
timestamp: data.signal.pendingAt,
|
|
6536
6592
|
action: "opened",
|
|
6537
6593
|
symbol: data.signal.symbol,
|
|
@@ -6544,12 +6600,13 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6544
6600
|
stopLoss: data.signal.priceStopLoss,
|
|
6545
6601
|
});
|
|
6546
6602
|
// Trim queue if exceeded MAX_EVENTS
|
|
6547
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6548
|
-
this._eventList.
|
|
6603
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6604
|
+
this._eventList.pop();
|
|
6549
6605
|
}
|
|
6550
6606
|
}
|
|
6551
6607
|
/**
|
|
6552
6608
|
* Adds an active event to the storage.
|
|
6609
|
+
* Replaces the last active event with the same signalId.
|
|
6553
6610
|
*
|
|
6554
6611
|
* @param data - Active tick result
|
|
6555
6612
|
*/
|
|
@@ -6568,10 +6625,18 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6568
6625
|
percentTp: data.percentTp,
|
|
6569
6626
|
percentSl: data.percentSl,
|
|
6570
6627
|
};
|
|
6571
|
-
|
|
6628
|
+
// Find the last active event with the same signalId
|
|
6629
|
+
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
6630
|
+
// Replace the last active event with the same signalId
|
|
6631
|
+
if (lastActiveIndex !== -1) {
|
|
6632
|
+
this._eventList[lastActiveIndex] = newEvent;
|
|
6633
|
+
return;
|
|
6634
|
+
}
|
|
6635
|
+
// If no previous active event found, add new event
|
|
6636
|
+
this._eventList.unshift(newEvent);
|
|
6572
6637
|
// Trim queue if exceeded MAX_EVENTS
|
|
6573
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6574
|
-
this._eventList.
|
|
6638
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6639
|
+
this._eventList.pop();
|
|
6575
6640
|
}
|
|
6576
6641
|
}
|
|
6577
6642
|
/**
|
|
@@ -6597,10 +6662,10 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6597
6662
|
closeReason: data.closeReason,
|
|
6598
6663
|
duration: durationMin,
|
|
6599
6664
|
};
|
|
6600
|
-
this._eventList.
|
|
6665
|
+
this._eventList.unshift(newEvent);
|
|
6601
6666
|
// Trim queue if exceeded MAX_EVENTS
|
|
6602
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6603
|
-
this._eventList.
|
|
6667
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6668
|
+
this._eventList.pop();
|
|
6604
6669
|
}
|
|
6605
6670
|
}
|
|
6606
6671
|
/**
|
|
@@ -6699,9 +6764,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6699
6764
|
"No events recorded yet."
|
|
6700
6765
|
].join("\n");
|
|
6701
6766
|
}
|
|
6702
|
-
const header = columns$
|
|
6703
|
-
const separator = columns$
|
|
6704
|
-
const rows = this._eventList.map((event) => columns$
|
|
6767
|
+
const header = columns$4.map((col) => col.label);
|
|
6768
|
+
const separator = columns$4.map(() => "---");
|
|
6769
|
+
const rows = this._eventList.map((event) => columns$4.map((col) => col.format(event)));
|
|
6705
6770
|
const tableData = [header, separator, ...rows];
|
|
6706
6771
|
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6707
6772
|
return [
|
|
@@ -6780,7 +6845,7 @@ class LiveMarkdownService {
|
|
|
6780
6845
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
6781
6846
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
6782
6847
|
*/
|
|
6783
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
6848
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
|
|
6784
6849
|
/**
|
|
6785
6850
|
* Processes tick events and accumulates all event types.
|
|
6786
6851
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -6943,7 +7008,7 @@ class LiveMarkdownService {
|
|
|
6943
7008
|
}
|
|
6944
7009
|
}
|
|
6945
7010
|
|
|
6946
|
-
const columns$
|
|
7011
|
+
const columns$3 = [
|
|
6947
7012
|
{
|
|
6948
7013
|
key: "timestamp",
|
|
6949
7014
|
label: "Timestamp",
|
|
@@ -7001,12 +7066,12 @@ const columns$2 = [
|
|
|
7001
7066
|
},
|
|
7002
7067
|
];
|
|
7003
7068
|
/** Maximum number of events to store in schedule reports */
|
|
7004
|
-
const MAX_EVENTS$
|
|
7069
|
+
const MAX_EVENTS$4 = 250;
|
|
7005
7070
|
/**
|
|
7006
7071
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
7007
7072
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
7008
7073
|
*/
|
|
7009
|
-
let ReportStorage$
|
|
7074
|
+
let ReportStorage$3 = class ReportStorage {
|
|
7010
7075
|
constructor() {
|
|
7011
7076
|
/** Internal list of all scheduled events for this strategy */
|
|
7012
7077
|
this._eventList = [];
|
|
@@ -7017,7 +7082,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7017
7082
|
* @param data - Scheduled tick result
|
|
7018
7083
|
*/
|
|
7019
7084
|
addScheduledEvent(data) {
|
|
7020
|
-
this._eventList.
|
|
7085
|
+
this._eventList.unshift({
|
|
7021
7086
|
timestamp: data.signal.scheduledAt,
|
|
7022
7087
|
action: "scheduled",
|
|
7023
7088
|
symbol: data.signal.symbol,
|
|
@@ -7030,8 +7095,8 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7030
7095
|
stopLoss: data.signal.priceStopLoss,
|
|
7031
7096
|
});
|
|
7032
7097
|
// Trim queue if exceeded MAX_EVENTS
|
|
7033
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7034
|
-
this._eventList.
|
|
7098
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7099
|
+
this._eventList.pop();
|
|
7035
7100
|
}
|
|
7036
7101
|
}
|
|
7037
7102
|
/**
|
|
@@ -7055,10 +7120,10 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7055
7120
|
stopLoss: data.signal.priceStopLoss,
|
|
7056
7121
|
duration: durationMin,
|
|
7057
7122
|
};
|
|
7058
|
-
this._eventList.
|
|
7123
|
+
this._eventList.unshift(newEvent);
|
|
7059
7124
|
// Trim queue if exceeded MAX_EVENTS
|
|
7060
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7061
|
-
this._eventList.
|
|
7125
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7126
|
+
this._eventList.pop();
|
|
7062
7127
|
}
|
|
7063
7128
|
}
|
|
7064
7129
|
/**
|
|
@@ -7083,10 +7148,10 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7083
7148
|
closeTimestamp: data.closeTimestamp,
|
|
7084
7149
|
duration: durationMin,
|
|
7085
7150
|
};
|
|
7086
|
-
this._eventList.
|
|
7151
|
+
this._eventList.unshift(newEvent);
|
|
7087
7152
|
// Trim queue if exceeded MAX_EVENTS
|
|
7088
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7089
|
-
this._eventList.
|
|
7153
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7154
|
+
this._eventList.pop();
|
|
7090
7155
|
}
|
|
7091
7156
|
}
|
|
7092
7157
|
/**
|
|
@@ -7155,9 +7220,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7155
7220
|
"No scheduled signals recorded yet."
|
|
7156
7221
|
].join("\n");
|
|
7157
7222
|
}
|
|
7158
|
-
const header = columns$
|
|
7159
|
-
const separator = columns$
|
|
7160
|
-
const rows = this._eventList.map((event) => columns$
|
|
7223
|
+
const header = columns$3.map((col) => col.label);
|
|
7224
|
+
const separator = columns$3.map(() => "---");
|
|
7225
|
+
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
7161
7226
|
const tableData = [header, separator, ...rows];
|
|
7162
7227
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7163
7228
|
return [
|
|
@@ -7225,7 +7290,7 @@ class ScheduleMarkdownService {
|
|
|
7225
7290
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
7226
7291
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
7227
7292
|
*/
|
|
7228
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
7293
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
|
|
7229
7294
|
/**
|
|
7230
7295
|
* Processes tick events and accumulates scheduled/opened/cancelled events.
|
|
7231
7296
|
* Should be called from signalEmitter subscription.
|
|
@@ -7392,7 +7457,7 @@ function percentile(sortedArray, p) {
|
|
|
7392
7457
|
return sortedArray[Math.max(0, index)];
|
|
7393
7458
|
}
|
|
7394
7459
|
/** Maximum number of performance events to store per strategy */
|
|
7395
|
-
const MAX_EVENTS$
|
|
7460
|
+
const MAX_EVENTS$3 = 10000;
|
|
7396
7461
|
/**
|
|
7397
7462
|
* Storage class for accumulating performance metrics per strategy.
|
|
7398
7463
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -7408,10 +7473,10 @@ class PerformanceStorage {
|
|
|
7408
7473
|
* @param event - Performance event with timing data
|
|
7409
7474
|
*/
|
|
7410
7475
|
addEvent(event) {
|
|
7411
|
-
this._events.
|
|
7476
|
+
this._events.unshift(event);
|
|
7412
7477
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
7413
|
-
if (this._events.length > MAX_EVENTS$
|
|
7414
|
-
this._events.
|
|
7478
|
+
if (this._events.length > MAX_EVENTS$3) {
|
|
7479
|
+
this._events.pop();
|
|
7415
7480
|
}
|
|
7416
7481
|
}
|
|
7417
7482
|
/**
|
|
@@ -7877,7 +7942,7 @@ const pnlColumns = [
|
|
|
7877
7942
|
* Storage class for accumulating walker results.
|
|
7878
7943
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
7879
7944
|
*/
|
|
7880
|
-
let ReportStorage$
|
|
7945
|
+
let ReportStorage$2 = class ReportStorage {
|
|
7881
7946
|
constructor(walkerName) {
|
|
7882
7947
|
this.walkerName = walkerName;
|
|
7883
7948
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -7895,17 +7960,13 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7895
7960
|
* @param data - Walker contract with strategy result
|
|
7896
7961
|
*/
|
|
7897
7962
|
addResult(data) {
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
this._totalStrategies = data.totalStrategies;
|
|
7902
|
-
}
|
|
7903
|
-
// Update best stats only if this strategy is the current best
|
|
7963
|
+
this._totalStrategies = data.totalStrategies;
|
|
7964
|
+
this._bestMetric = data.bestMetric;
|
|
7965
|
+
this._bestStrategy = data.bestStrategy;
|
|
7904
7966
|
if (data.strategyName === data.bestStrategy) {
|
|
7905
7967
|
this._bestStats = data.stats;
|
|
7906
7968
|
}
|
|
7907
|
-
|
|
7908
|
-
this._strategyResults.push({
|
|
7969
|
+
this._strategyResults.unshift({
|
|
7909
7970
|
strategyName: data.strategyName,
|
|
7910
7971
|
stats: data.stats,
|
|
7911
7972
|
metricValue: data.metricValue,
|
|
@@ -8089,7 +8150,7 @@ class WalkerMarkdownService {
|
|
|
8089
8150
|
* Memoized function to get or create ReportStorage for a walker.
|
|
8090
8151
|
* Each walker gets its own isolated storage instance.
|
|
8091
8152
|
*/
|
|
8092
|
-
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$
|
|
8153
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
|
|
8093
8154
|
/**
|
|
8094
8155
|
* Processes walker progress events and accumulates strategy results.
|
|
8095
8156
|
* Should be called from walkerEmitter.
|
|
@@ -8260,7 +8321,7 @@ function isUnsafe(value) {
|
|
|
8260
8321
|
}
|
|
8261
8322
|
return false;
|
|
8262
8323
|
}
|
|
8263
|
-
const columns$
|
|
8324
|
+
const columns$2 = [
|
|
8264
8325
|
{
|
|
8265
8326
|
key: "symbol",
|
|
8266
8327
|
label: "Symbol",
|
|
@@ -8323,7 +8384,7 @@ const columns$1 = [
|
|
|
8323
8384
|
},
|
|
8324
8385
|
];
|
|
8325
8386
|
/** Maximum number of signals to store per symbol in heatmap reports */
|
|
8326
|
-
const MAX_EVENTS$
|
|
8387
|
+
const MAX_EVENTS$2 = 250;
|
|
8327
8388
|
/**
|
|
8328
8389
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
8329
8390
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -8344,10 +8405,10 @@ class HeatmapStorage {
|
|
|
8344
8405
|
this.symbolData.set(symbol, []);
|
|
8345
8406
|
}
|
|
8346
8407
|
const signals = this.symbolData.get(symbol);
|
|
8347
|
-
signals.
|
|
8408
|
+
signals.unshift(data);
|
|
8348
8409
|
// Trim queue if exceeded MAX_EVENTS per symbol
|
|
8349
|
-
if (signals.length > MAX_EVENTS$
|
|
8350
|
-
signals.
|
|
8410
|
+
if (signals.length > MAX_EVENTS$2) {
|
|
8411
|
+
signals.pop();
|
|
8351
8412
|
}
|
|
8352
8413
|
}
|
|
8353
8414
|
/**
|
|
@@ -8567,9 +8628,9 @@ class HeatmapStorage {
|
|
|
8567
8628
|
"*No data available*"
|
|
8568
8629
|
].join("\n");
|
|
8569
8630
|
}
|
|
8570
|
-
const header = columns$
|
|
8571
|
-
const separator = columns$
|
|
8572
|
-
const rows = data.symbols.map((row) => columns$
|
|
8631
|
+
const header = columns$2.map((col) => col.label);
|
|
8632
|
+
const separator = columns$2.map(() => "---");
|
|
8633
|
+
const rows = data.symbols.map((row) => columns$2.map((col) => col.format(row)));
|
|
8573
8634
|
const tableData = [header, separator, ...rows];
|
|
8574
8635
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8575
8636
|
return [
|
|
@@ -10607,7 +10668,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
|
|
|
10607
10668
|
revenuePercent,
|
|
10608
10669
|
backtest,
|
|
10609
10670
|
});
|
|
10610
|
-
await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
10671
|
+
await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
|
|
10611
10672
|
}
|
|
10612
10673
|
}
|
|
10613
10674
|
if (shouldPersist) {
|
|
@@ -10654,7 +10715,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
|
|
|
10654
10715
|
lossPercent,
|
|
10655
10716
|
backtest,
|
|
10656
10717
|
});
|
|
10657
|
-
await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
10718
|
+
await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
|
|
10658
10719
|
}
|
|
10659
10720
|
}
|
|
10660
10721
|
if (shouldPersist) {
|
|
@@ -10925,6 +10986,7 @@ class ClientPartial {
|
|
|
10925
10986
|
symbol,
|
|
10926
10987
|
data,
|
|
10927
10988
|
priceClose,
|
|
10989
|
+
backtest,
|
|
10928
10990
|
});
|
|
10929
10991
|
if (this._states === NEED_FETCH) {
|
|
10930
10992
|
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
@@ -10941,14 +11003,18 @@ class ClientPartial {
|
|
|
10941
11003
|
* Emits PartialProfitContract event to all subscribers.
|
|
10942
11004
|
*
|
|
10943
11005
|
* @param symbol - Trading pair symbol
|
|
11006
|
+
* @param strategyName - Strategy name that generated this signal
|
|
11007
|
+
* @param exchangeName - Exchange name where this signal is being executed
|
|
10944
11008
|
* @param data - Signal row data
|
|
10945
11009
|
* @param currentPrice - Current market price
|
|
10946
11010
|
* @param level - Profit level reached
|
|
10947
11011
|
* @param backtest - True if backtest mode
|
|
10948
11012
|
* @param timestamp - Event timestamp in milliseconds
|
|
10949
11013
|
*/
|
|
10950
|
-
const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
11014
|
+
const COMMIT_PROFIT_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
10951
11015
|
symbol,
|
|
11016
|
+
strategyName,
|
|
11017
|
+
exchangeName,
|
|
10952
11018
|
data,
|
|
10953
11019
|
currentPrice,
|
|
10954
11020
|
level,
|
|
@@ -10962,14 +11028,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
|
|
|
10962
11028
|
* Emits PartialLossContract event to all subscribers.
|
|
10963
11029
|
*
|
|
10964
11030
|
* @param symbol - Trading pair symbol
|
|
11031
|
+
* @param strategyName - Strategy name that generated this signal
|
|
11032
|
+
* @param exchangeName - Exchange name where this signal is being executed
|
|
10965
11033
|
* @param data - Signal row data
|
|
10966
11034
|
* @param currentPrice - Current market price
|
|
10967
11035
|
* @param level - Loss level reached
|
|
10968
11036
|
* @param backtest - True if backtest mode
|
|
10969
11037
|
* @param timestamp - Event timestamp in milliseconds
|
|
10970
11038
|
*/
|
|
10971
|
-
const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
11039
|
+
const COMMIT_LOSS_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
10972
11040
|
symbol,
|
|
11041
|
+
strategyName,
|
|
11042
|
+
exchangeName,
|
|
10973
11043
|
data,
|
|
10974
11044
|
currentPrice,
|
|
10975
11045
|
level,
|
|
@@ -11115,7 +11185,7 @@ class PartialConnectionService {
|
|
|
11115
11185
|
}
|
|
11116
11186
|
}
|
|
11117
11187
|
|
|
11118
|
-
const columns = [
|
|
11188
|
+
const columns$1 = [
|
|
11119
11189
|
{
|
|
11120
11190
|
key: "action",
|
|
11121
11191
|
label: "Action",
|
|
@@ -11163,12 +11233,12 @@ const columns = [
|
|
|
11163
11233
|
},
|
|
11164
11234
|
];
|
|
11165
11235
|
/** Maximum number of events to store in partial reports */
|
|
11166
|
-
const MAX_EVENTS = 250;
|
|
11236
|
+
const MAX_EVENTS$1 = 250;
|
|
11167
11237
|
/**
|
|
11168
11238
|
* Storage class for accumulating partial profit/loss events per symbol-strategy pair.
|
|
11169
11239
|
* Maintains a chronological list of profit and loss level events.
|
|
11170
11240
|
*/
|
|
11171
|
-
class ReportStorage {
|
|
11241
|
+
let ReportStorage$1 = class ReportStorage {
|
|
11172
11242
|
constructor() {
|
|
11173
11243
|
/** Internal list of all partial events for this symbol */
|
|
11174
11244
|
this._eventList = [];
|
|
@@ -11182,7 +11252,7 @@ class ReportStorage {
|
|
|
11182
11252
|
* @param backtest - True if backtest mode
|
|
11183
11253
|
*/
|
|
11184
11254
|
addProfitEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11185
|
-
this._eventList.
|
|
11255
|
+
this._eventList.unshift({
|
|
11186
11256
|
timestamp,
|
|
11187
11257
|
action: "profit",
|
|
11188
11258
|
symbol: data.symbol,
|
|
@@ -11194,8 +11264,8 @@ class ReportStorage {
|
|
|
11194
11264
|
backtest,
|
|
11195
11265
|
});
|
|
11196
11266
|
// Trim queue if exceeded MAX_EVENTS
|
|
11197
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11198
|
-
this._eventList.
|
|
11267
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11268
|
+
this._eventList.pop();
|
|
11199
11269
|
}
|
|
11200
11270
|
}
|
|
11201
11271
|
/**
|
|
@@ -11207,7 +11277,7 @@ class ReportStorage {
|
|
|
11207
11277
|
* @param backtest - True if backtest mode
|
|
11208
11278
|
*/
|
|
11209
11279
|
addLossEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11210
|
-
this._eventList.
|
|
11280
|
+
this._eventList.unshift({
|
|
11211
11281
|
timestamp,
|
|
11212
11282
|
action: "loss",
|
|
11213
11283
|
symbol: data.symbol,
|
|
@@ -11219,8 +11289,8 @@ class ReportStorage {
|
|
|
11219
11289
|
backtest,
|
|
11220
11290
|
});
|
|
11221
11291
|
// Trim queue if exceeded MAX_EVENTS
|
|
11222
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11223
|
-
this._eventList.
|
|
11292
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11293
|
+
this._eventList.pop();
|
|
11224
11294
|
}
|
|
11225
11295
|
}
|
|
11226
11296
|
/**
|
|
@@ -11262,9 +11332,9 @@ class ReportStorage {
|
|
|
11262
11332
|
"No partial profit/loss events recorded yet."
|
|
11263
11333
|
].join("\n");
|
|
11264
11334
|
}
|
|
11265
|
-
const header = columns.map((col) => col.label);
|
|
11266
|
-
const separator = columns.map(() => "---");
|
|
11267
|
-
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
11335
|
+
const header = columns$1.map((col) => col.label);
|
|
11336
|
+
const separator = columns$1.map(() => "---");
|
|
11337
|
+
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
11268
11338
|
const tableData = [header, separator, ...rows];
|
|
11269
11339
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
11270
11340
|
return [
|
|
@@ -11298,7 +11368,7 @@ class ReportStorage {
|
|
|
11298
11368
|
console.error(`Failed to save markdown report:`, error);
|
|
11299
11369
|
}
|
|
11300
11370
|
}
|
|
11301
|
-
}
|
|
11371
|
+
};
|
|
11302
11372
|
/**
|
|
11303
11373
|
* Service for generating and saving partial profit/loss markdown reports.
|
|
11304
11374
|
*
|
|
@@ -11328,7 +11398,7 @@ class PartialMarkdownService {
|
|
|
11328
11398
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
11329
11399
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
11330
11400
|
*/
|
|
11331
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
|
|
11401
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
|
|
11332
11402
|
/**
|
|
11333
11403
|
* Processes profit events and accumulates them.
|
|
11334
11404
|
* Should be called from partialProfitSubject subscription.
|
|
@@ -11638,7 +11708,7 @@ class PartialGlobalService {
|
|
|
11638
11708
|
* Warning threshold for message size in kilobytes.
|
|
11639
11709
|
* Messages exceeding this size trigger console warnings.
|
|
11640
11710
|
*/
|
|
11641
|
-
const WARN_KB =
|
|
11711
|
+
const WARN_KB = 20;
|
|
11642
11712
|
/**
|
|
11643
11713
|
* Internal function for dumping signal data to markdown files.
|
|
11644
11714
|
* Creates a directory structure with system prompts, user messages, and LLM output.
|
|
@@ -11900,6 +11970,353 @@ class ConfigValidationService {
|
|
|
11900
11970
|
}
|
|
11901
11971
|
}
|
|
11902
11972
|
|
|
11973
|
+
const columns = [
|
|
11974
|
+
{
|
|
11975
|
+
key: "symbol",
|
|
11976
|
+
label: "Symbol",
|
|
11977
|
+
format: (data) => data.symbol,
|
|
11978
|
+
},
|
|
11979
|
+
{
|
|
11980
|
+
key: "strategyName",
|
|
11981
|
+
label: "Strategy",
|
|
11982
|
+
format: (data) => data.strategyName,
|
|
11983
|
+
},
|
|
11984
|
+
{
|
|
11985
|
+
key: "signalId",
|
|
11986
|
+
label: "Signal ID",
|
|
11987
|
+
format: (data) => data.pendingSignal.id || "N/A",
|
|
11988
|
+
},
|
|
11989
|
+
{
|
|
11990
|
+
key: "position",
|
|
11991
|
+
label: "Position",
|
|
11992
|
+
format: (data) => data.pendingSignal.position.toUpperCase(),
|
|
11993
|
+
},
|
|
11994
|
+
{
|
|
11995
|
+
key: "exchangeName",
|
|
11996
|
+
label: "Exchange",
|
|
11997
|
+
format: (data) => data.exchangeName,
|
|
11998
|
+
},
|
|
11999
|
+
{
|
|
12000
|
+
key: "openPrice",
|
|
12001
|
+
label: "Open Price",
|
|
12002
|
+
format: (data) => data.pendingSignal.priceOpen !== undefined
|
|
12003
|
+
? `${data.pendingSignal.priceOpen.toFixed(8)} USD`
|
|
12004
|
+
: "N/A",
|
|
12005
|
+
},
|
|
12006
|
+
{
|
|
12007
|
+
key: "takeProfit",
|
|
12008
|
+
label: "Take Profit",
|
|
12009
|
+
format: (data) => data.pendingSignal.priceTakeProfit !== undefined
|
|
12010
|
+
? `${data.pendingSignal.priceTakeProfit.toFixed(8)} USD`
|
|
12011
|
+
: "N/A",
|
|
12012
|
+
},
|
|
12013
|
+
{
|
|
12014
|
+
key: "stopLoss",
|
|
12015
|
+
label: "Stop Loss",
|
|
12016
|
+
format: (data) => data.pendingSignal.priceStopLoss !== undefined
|
|
12017
|
+
? `${data.pendingSignal.priceStopLoss.toFixed(8)} USD`
|
|
12018
|
+
: "N/A",
|
|
12019
|
+
},
|
|
12020
|
+
{
|
|
12021
|
+
key: "currentPrice",
|
|
12022
|
+
label: "Current Price",
|
|
12023
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
12024
|
+
},
|
|
12025
|
+
{
|
|
12026
|
+
key: "activePositionCount",
|
|
12027
|
+
label: "Active Positions",
|
|
12028
|
+
format: (data) => data.activePositionCount.toString(),
|
|
12029
|
+
},
|
|
12030
|
+
{
|
|
12031
|
+
key: "comment",
|
|
12032
|
+
label: "Reason",
|
|
12033
|
+
format: (data) => data.comment,
|
|
12034
|
+
},
|
|
12035
|
+
{
|
|
12036
|
+
key: "timestamp",
|
|
12037
|
+
label: "Timestamp",
|
|
12038
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
12039
|
+
},
|
|
12040
|
+
];
|
|
12041
|
+
/** Maximum number of events to store in risk reports */
|
|
12042
|
+
const MAX_EVENTS = 250;
|
|
12043
|
+
/**
|
|
12044
|
+
* Storage class for accumulating risk rejection events per symbol-strategy pair.
|
|
12045
|
+
* Maintains a chronological list of rejected signals due to risk limits.
|
|
12046
|
+
*/
|
|
12047
|
+
class ReportStorage {
|
|
12048
|
+
constructor() {
|
|
12049
|
+
/** Internal list of all risk rejection events for this symbol */
|
|
12050
|
+
this._eventList = [];
|
|
12051
|
+
}
|
|
12052
|
+
/**
|
|
12053
|
+
* Adds a risk rejection event to the storage.
|
|
12054
|
+
*
|
|
12055
|
+
* @param event - Risk rejection event data
|
|
12056
|
+
*/
|
|
12057
|
+
addRejectionEvent(event) {
|
|
12058
|
+
this._eventList.unshift(event);
|
|
12059
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
12060
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
12061
|
+
this._eventList.pop();
|
|
12062
|
+
}
|
|
12063
|
+
}
|
|
12064
|
+
/**
|
|
12065
|
+
* Calculates statistical data from risk rejection events (Controller).
|
|
12066
|
+
*
|
|
12067
|
+
* @returns Statistical data (empty object if no events)
|
|
12068
|
+
*/
|
|
12069
|
+
async getData() {
|
|
12070
|
+
if (this._eventList.length === 0) {
|
|
12071
|
+
return {
|
|
12072
|
+
eventList: [],
|
|
12073
|
+
totalRejections: 0,
|
|
12074
|
+
bySymbol: {},
|
|
12075
|
+
byStrategy: {},
|
|
12076
|
+
};
|
|
12077
|
+
}
|
|
12078
|
+
const bySymbol = {};
|
|
12079
|
+
const byStrategy = {};
|
|
12080
|
+
for (const event of this._eventList) {
|
|
12081
|
+
bySymbol[event.symbol] = (bySymbol[event.symbol] || 0) + 1;
|
|
12082
|
+
byStrategy[event.strategyName] = (byStrategy[event.strategyName] || 0) + 1;
|
|
12083
|
+
}
|
|
12084
|
+
return {
|
|
12085
|
+
eventList: this._eventList,
|
|
12086
|
+
totalRejections: this._eventList.length,
|
|
12087
|
+
bySymbol,
|
|
12088
|
+
byStrategy,
|
|
12089
|
+
};
|
|
12090
|
+
}
|
|
12091
|
+
/**
|
|
12092
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair (View).
|
|
12093
|
+
*
|
|
12094
|
+
* @param symbol - Trading pair symbol
|
|
12095
|
+
* @param strategyName - Strategy name
|
|
12096
|
+
* @returns Markdown formatted report with all events
|
|
12097
|
+
*/
|
|
12098
|
+
async getReport(symbol, strategyName) {
|
|
12099
|
+
const stats = await this.getData();
|
|
12100
|
+
if (stats.totalRejections === 0) {
|
|
12101
|
+
return [
|
|
12102
|
+
`# Risk Rejection Report: ${symbol}:${strategyName}`,
|
|
12103
|
+
"",
|
|
12104
|
+
"No risk rejections recorded yet.",
|
|
12105
|
+
].join("\n");
|
|
12106
|
+
}
|
|
12107
|
+
const header = columns.map((col) => col.label);
|
|
12108
|
+
const separator = columns.map(() => "---");
|
|
12109
|
+
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
12110
|
+
const tableData = [header, separator, ...rows];
|
|
12111
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
12112
|
+
return [
|
|
12113
|
+
`# Risk Rejection Report: ${symbol}:${strategyName}`,
|
|
12114
|
+
"",
|
|
12115
|
+
table,
|
|
12116
|
+
"",
|
|
12117
|
+
`**Total rejections:** ${stats.totalRejections}`,
|
|
12118
|
+
"",
|
|
12119
|
+
"## Rejections by Symbol",
|
|
12120
|
+
...Object.entries(stats.bySymbol).map(([sym, count]) => `- ${sym}: ${count}`),
|
|
12121
|
+
"",
|
|
12122
|
+
"## Rejections by Strategy",
|
|
12123
|
+
...Object.entries(stats.byStrategy).map(([strat, count]) => `- ${strat}: ${count}`),
|
|
12124
|
+
].join("\n");
|
|
12125
|
+
}
|
|
12126
|
+
/**
|
|
12127
|
+
* Saves symbol-strategy report to disk.
|
|
12128
|
+
*
|
|
12129
|
+
* @param symbol - Trading pair symbol
|
|
12130
|
+
* @param strategyName - Strategy name
|
|
12131
|
+
* @param path - Directory path to save report (default: "./dump/risk")
|
|
12132
|
+
*/
|
|
12133
|
+
async dump(symbol, strategyName, path$1 = "./dump/risk") {
|
|
12134
|
+
const markdown = await this.getReport(symbol, strategyName);
|
|
12135
|
+
try {
|
|
12136
|
+
const dir = path.join(process.cwd(), path$1);
|
|
12137
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12138
|
+
const filename = `${symbol}_${strategyName}.md`;
|
|
12139
|
+
const filepath = path.join(dir, filename);
|
|
12140
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
12141
|
+
console.log(`Risk rejection report saved: ${filepath}`);
|
|
12142
|
+
}
|
|
12143
|
+
catch (error) {
|
|
12144
|
+
console.error(`Failed to save markdown report:`, error);
|
|
12145
|
+
}
|
|
12146
|
+
}
|
|
12147
|
+
}
|
|
12148
|
+
/**
|
|
12149
|
+
* Service for generating and saving risk rejection markdown reports.
|
|
12150
|
+
*
|
|
12151
|
+
* Features:
|
|
12152
|
+
* - Listens to risk rejection events via riskSubject
|
|
12153
|
+
* - Accumulates all rejection events per symbol-strategy pair
|
|
12154
|
+
* - Generates markdown tables with detailed rejection information
|
|
12155
|
+
* - Provides statistics (total rejections, by symbol, by strategy)
|
|
12156
|
+
* - Saves reports to disk in dump/risk/{symbol}_{strategyName}.md
|
|
12157
|
+
*
|
|
12158
|
+
* @example
|
|
12159
|
+
* ```typescript
|
|
12160
|
+
* const service = new RiskMarkdownService();
|
|
12161
|
+
*
|
|
12162
|
+
* // Service automatically subscribes to subjects on init
|
|
12163
|
+
* // No manual callback setup needed
|
|
12164
|
+
*
|
|
12165
|
+
* // Later: generate and save report
|
|
12166
|
+
* await service.dump("BTCUSDT", "my-strategy");
|
|
12167
|
+
* ```
|
|
12168
|
+
*/
|
|
12169
|
+
class RiskMarkdownService {
|
|
12170
|
+
constructor() {
|
|
12171
|
+
/** Logger service for debug output */
|
|
12172
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
12173
|
+
/**
|
|
12174
|
+
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
12175
|
+
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
12176
|
+
*/
|
|
12177
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
|
|
12178
|
+
/**
|
|
12179
|
+
* Processes risk rejection events and accumulates them.
|
|
12180
|
+
* Should be called from riskSubject subscription.
|
|
12181
|
+
*
|
|
12182
|
+
* @param data - Risk rejection event data
|
|
12183
|
+
*
|
|
12184
|
+
* @example
|
|
12185
|
+
* ```typescript
|
|
12186
|
+
* const service = new RiskMarkdownService();
|
|
12187
|
+
* // Service automatically subscribes in init()
|
|
12188
|
+
* ```
|
|
12189
|
+
*/
|
|
12190
|
+
this.tickRejection = async (data) => {
|
|
12191
|
+
this.loggerService.log("riskMarkdownService tickRejection", {
|
|
12192
|
+
data,
|
|
12193
|
+
});
|
|
12194
|
+
const storage = this.getStorage(data.symbol, data.strategyName);
|
|
12195
|
+
storage.addRejectionEvent(data);
|
|
12196
|
+
};
|
|
12197
|
+
/**
|
|
12198
|
+
* Gets statistical data from all risk rejection events for a symbol-strategy pair.
|
|
12199
|
+
* Delegates to ReportStorage.getData().
|
|
12200
|
+
*
|
|
12201
|
+
* @param symbol - Trading pair symbol to get data for
|
|
12202
|
+
* @param strategyName - Strategy name to get data for
|
|
12203
|
+
* @returns Statistical data object with all metrics
|
|
12204
|
+
*
|
|
12205
|
+
* @example
|
|
12206
|
+
* ```typescript
|
|
12207
|
+
* const service = new RiskMarkdownService();
|
|
12208
|
+
* const stats = await service.getData("BTCUSDT", "my-strategy");
|
|
12209
|
+
* console.log(stats.totalRejections, stats.bySymbol);
|
|
12210
|
+
* ```
|
|
12211
|
+
*/
|
|
12212
|
+
this.getData = async (symbol, strategyName) => {
|
|
12213
|
+
this.loggerService.log("riskMarkdownService getData", {
|
|
12214
|
+
symbol,
|
|
12215
|
+
strategyName,
|
|
12216
|
+
});
|
|
12217
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12218
|
+
return storage.getData();
|
|
12219
|
+
};
|
|
12220
|
+
/**
|
|
12221
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair.
|
|
12222
|
+
* Delegates to ReportStorage.getReport().
|
|
12223
|
+
*
|
|
12224
|
+
* @param symbol - Trading pair symbol to generate report for
|
|
12225
|
+
* @param strategyName - Strategy name to generate report for
|
|
12226
|
+
* @returns Markdown formatted report string with table of all events
|
|
12227
|
+
*
|
|
12228
|
+
* @example
|
|
12229
|
+
* ```typescript
|
|
12230
|
+
* const service = new RiskMarkdownService();
|
|
12231
|
+
* const markdown = await service.getReport("BTCUSDT", "my-strategy");
|
|
12232
|
+
* console.log(markdown);
|
|
12233
|
+
* ```
|
|
12234
|
+
*/
|
|
12235
|
+
this.getReport = async (symbol, strategyName) => {
|
|
12236
|
+
this.loggerService.log("riskMarkdownService getReport", {
|
|
12237
|
+
symbol,
|
|
12238
|
+
strategyName,
|
|
12239
|
+
});
|
|
12240
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12241
|
+
return storage.getReport(symbol, strategyName);
|
|
12242
|
+
};
|
|
12243
|
+
/**
|
|
12244
|
+
* Saves symbol-strategy report to disk.
|
|
12245
|
+
* Creates directory if it doesn't exist.
|
|
12246
|
+
* Delegates to ReportStorage.dump().
|
|
12247
|
+
*
|
|
12248
|
+
* @param symbol - Trading pair symbol to save report for
|
|
12249
|
+
* @param strategyName - Strategy name to save report for
|
|
12250
|
+
* @param path - Directory path to save report (default: "./dump/risk")
|
|
12251
|
+
*
|
|
12252
|
+
* @example
|
|
12253
|
+
* ```typescript
|
|
12254
|
+
* const service = new RiskMarkdownService();
|
|
12255
|
+
*
|
|
12256
|
+
* // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
|
|
12257
|
+
* await service.dump("BTCUSDT", "my-strategy");
|
|
12258
|
+
*
|
|
12259
|
+
* // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
|
|
12260
|
+
* await service.dump("BTCUSDT", "my-strategy", "./custom/path");
|
|
12261
|
+
* ```
|
|
12262
|
+
*/
|
|
12263
|
+
this.dump = async (symbol, strategyName, path = "./dump/risk") => {
|
|
12264
|
+
this.loggerService.log("riskMarkdownService dump", {
|
|
12265
|
+
symbol,
|
|
12266
|
+
strategyName,
|
|
12267
|
+
path,
|
|
12268
|
+
});
|
|
12269
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12270
|
+
await storage.dump(symbol, strategyName, path);
|
|
12271
|
+
};
|
|
12272
|
+
/**
|
|
12273
|
+
* Clears accumulated event data from storage.
|
|
12274
|
+
* If ctx is provided, clears only that specific symbol-strategy pair's data.
|
|
12275
|
+
* If nothing is provided, clears all data.
|
|
12276
|
+
*
|
|
12277
|
+
* @param ctx - Optional context with symbol and strategyName
|
|
12278
|
+
*
|
|
12279
|
+
* @example
|
|
12280
|
+
* ```typescript
|
|
12281
|
+
* const service = new RiskMarkdownService();
|
|
12282
|
+
*
|
|
12283
|
+
* // Clear specific symbol-strategy pair
|
|
12284
|
+
* await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
|
|
12285
|
+
*
|
|
12286
|
+
* // Clear all data
|
|
12287
|
+
* await service.clear();
|
|
12288
|
+
* ```
|
|
12289
|
+
*/
|
|
12290
|
+
this.clear = async (ctx) => {
|
|
12291
|
+
this.loggerService.log("riskMarkdownService clear", {
|
|
12292
|
+
ctx,
|
|
12293
|
+
});
|
|
12294
|
+
if (ctx) {
|
|
12295
|
+
const key = `${ctx.symbol}:${ctx.strategyName}`;
|
|
12296
|
+
this.getStorage.clear(key);
|
|
12297
|
+
}
|
|
12298
|
+
else {
|
|
12299
|
+
this.getStorage.clear();
|
|
12300
|
+
}
|
|
12301
|
+
};
|
|
12302
|
+
/**
|
|
12303
|
+
* Initializes the service by subscribing to risk rejection events.
|
|
12304
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
12305
|
+
* Automatically called on first use.
|
|
12306
|
+
*
|
|
12307
|
+
* @example
|
|
12308
|
+
* ```typescript
|
|
12309
|
+
* const service = new RiskMarkdownService();
|
|
12310
|
+
* await service.init(); // Subscribe to rejection events
|
|
12311
|
+
* ```
|
|
12312
|
+
*/
|
|
12313
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
12314
|
+
this.loggerService.log("riskMarkdownService init");
|
|
12315
|
+
riskSubject.subscribe(this.tickRejection);
|
|
12316
|
+
});
|
|
12317
|
+
}
|
|
12318
|
+
}
|
|
12319
|
+
|
|
11903
12320
|
{
|
|
11904
12321
|
provide(TYPES.loggerService, () => new LoggerService());
|
|
11905
12322
|
}
|
|
@@ -11960,6 +12377,7 @@ class ConfigValidationService {
|
|
|
11960
12377
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
11961
12378
|
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
11962
12379
|
provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
|
|
12380
|
+
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
11963
12381
|
}
|
|
11964
12382
|
{
|
|
11965
12383
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -12035,6 +12453,7 @@ const markdownServices = {
|
|
|
12035
12453
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
12036
12454
|
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
12037
12455
|
outlineMarkdownService: inject(TYPES.outlineMarkdownService),
|
|
12456
|
+
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
12038
12457
|
};
|
|
12039
12458
|
const validationServices = {
|
|
12040
12459
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -12809,6 +13228,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
|
|
|
12809
13228
|
const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
|
|
12810
13229
|
const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
|
|
12811
13230
|
const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
|
|
13231
|
+
const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
|
|
13232
|
+
const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
|
|
12812
13233
|
/**
|
|
12813
13234
|
* Subscribes to all signal events with queued async processing.
|
|
12814
13235
|
*
|
|
@@ -13607,6 +14028,75 @@ function listenPartialLossOnce(filterFn, fn) {
|
|
|
13607
14028
|
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
|
|
13608
14029
|
return partialLossSubject.filter(filterFn).once(fn);
|
|
13609
14030
|
}
|
|
14031
|
+
/**
|
|
14032
|
+
* Subscribes to risk rejection events with queued async processing.
|
|
14033
|
+
*
|
|
14034
|
+
* Emits ONLY when a signal is rejected due to risk validation failure.
|
|
14035
|
+
* Does not emit for allowed signals (prevents spam).
|
|
14036
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
14037
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
14038
|
+
*
|
|
14039
|
+
* @param fn - Callback function to handle risk rejection events
|
|
14040
|
+
* @returns Unsubscribe function to stop listening to events
|
|
14041
|
+
*
|
|
14042
|
+
* @example
|
|
14043
|
+
* ```typescript
|
|
14044
|
+
* import { listenRisk } from "./function/event";
|
|
14045
|
+
*
|
|
14046
|
+
* const unsubscribe = listenRisk((event) => {
|
|
14047
|
+
* console.log(`[RISK REJECTED] Signal for ${event.symbol}`);
|
|
14048
|
+
* console.log(`Strategy: ${event.strategyName}`);
|
|
14049
|
+
* console.log(`Position: ${event.pendingSignal.position}`);
|
|
14050
|
+
* console.log(`Active positions: ${event.activePositionCount}`);
|
|
14051
|
+
* console.log(`Reason: ${event.comment}`);
|
|
14052
|
+
* console.log(`Price: ${event.currentPrice}`);
|
|
14053
|
+
* });
|
|
14054
|
+
*
|
|
14055
|
+
* // Later: stop listening
|
|
14056
|
+
* unsubscribe();
|
|
14057
|
+
* ```
|
|
14058
|
+
*/
|
|
14059
|
+
function listenRisk(fn) {
|
|
14060
|
+
backtest$1.loggerService.log(LISTEN_RISK_METHOD_NAME);
|
|
14061
|
+
return riskSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
14062
|
+
}
|
|
14063
|
+
/**
|
|
14064
|
+
* Subscribes to filtered risk rejection events with one-time execution.
|
|
14065
|
+
*
|
|
14066
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
14067
|
+
* and automatically unsubscribes. Useful for waiting for specific risk rejection conditions.
|
|
14068
|
+
*
|
|
14069
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
14070
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
14071
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
14072
|
+
*
|
|
14073
|
+
* @example
|
|
14074
|
+
* ```typescript
|
|
14075
|
+
* import { listenRiskOnce } from "./function/event";
|
|
14076
|
+
*
|
|
14077
|
+
* // Wait for first risk rejection on BTCUSDT
|
|
14078
|
+
* listenRiskOnce(
|
|
14079
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
14080
|
+
* (event) => {
|
|
14081
|
+
* console.log("BTCUSDT signal rejected!");
|
|
14082
|
+
* console.log("Reason:", event.comment);
|
|
14083
|
+
* }
|
|
14084
|
+
* );
|
|
14085
|
+
*
|
|
14086
|
+
* // Wait for rejection due to position limit
|
|
14087
|
+
* const cancel = listenRiskOnce(
|
|
14088
|
+
* (event) => event.comment.includes("Max") && event.activePositionCount >= 3,
|
|
14089
|
+
* (event) => console.log("Position limit reached:", event.activePositionCount)
|
|
14090
|
+
* );
|
|
14091
|
+
*
|
|
14092
|
+
* // Cancel if needed before event fires
|
|
14093
|
+
* cancel();
|
|
14094
|
+
* ```
|
|
14095
|
+
*/
|
|
14096
|
+
function listenRiskOnce(filterFn, fn) {
|
|
14097
|
+
backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
|
|
14098
|
+
return riskSubject.filter(filterFn).once(fn);
|
|
14099
|
+
}
|
|
13610
14100
|
|
|
13611
14101
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
13612
14102
|
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
@@ -16231,6 +16721,186 @@ class ConstantUtils {
|
|
|
16231
16721
|
*/
|
|
16232
16722
|
const Constant = new ConstantUtils();
|
|
16233
16723
|
|
|
16724
|
+
const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
|
|
16725
|
+
const RISK_METHOD_NAME_GET_REPORT = "RiskUtils.getReport";
|
|
16726
|
+
const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
|
|
16727
|
+
/**
|
|
16728
|
+
* Utility class for accessing risk rejection reports and statistics.
|
|
16729
|
+
*
|
|
16730
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
16731
|
+
* accumulated by RiskMarkdownService from risk rejection events.
|
|
16732
|
+
*
|
|
16733
|
+
* Features:
|
|
16734
|
+
* - Statistical data extraction (total rejections count, by symbol, by strategy)
|
|
16735
|
+
* - Markdown report generation with event tables
|
|
16736
|
+
* - File export to disk
|
|
16737
|
+
*
|
|
16738
|
+
* Data source:
|
|
16739
|
+
* - RiskMarkdownService listens to riskSubject
|
|
16740
|
+
* - Accumulates rejection events in ReportStorage (max 250 events per symbol-strategy pair)
|
|
16741
|
+
* - Events include: timestamp, symbol, strategyName, position, exchangeName, price, activePositionCount, comment
|
|
16742
|
+
*
|
|
16743
|
+
* @example
|
|
16744
|
+
* ```typescript
|
|
16745
|
+
* import { Risk } from "./classes/Risk";
|
|
16746
|
+
*
|
|
16747
|
+
* // Get statistical data for BTCUSDT:my-strategy
|
|
16748
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16749
|
+
* console.log(`Total rejections: ${stats.totalRejections}`);
|
|
16750
|
+
* console.log(`By symbol:`, stats.bySymbol);
|
|
16751
|
+
* console.log(`By strategy:`, stats.byStrategy);
|
|
16752
|
+
*
|
|
16753
|
+
* // Generate markdown report
|
|
16754
|
+
* const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16755
|
+
* console.log(markdown); // Formatted table with all rejection events
|
|
16756
|
+
*
|
|
16757
|
+
* // Export report to file
|
|
16758
|
+
* await Risk.dump("BTCUSDT", "my-strategy"); // Saves to ./dump/risk/BTCUSDT_my-strategy.md
|
|
16759
|
+
* await Risk.dump("BTCUSDT", "my-strategy", "./custom/path"); // Custom directory
|
|
16760
|
+
* ```
|
|
16761
|
+
*/
|
|
16762
|
+
class RiskUtils {
|
|
16763
|
+
constructor() {
|
|
16764
|
+
/**
|
|
16765
|
+
* Retrieves statistical data from accumulated risk rejection events.
|
|
16766
|
+
*
|
|
16767
|
+
* Delegates to RiskMarkdownService.getData() which reads from ReportStorage.
|
|
16768
|
+
* Returns aggregated metrics calculated from all rejection events.
|
|
16769
|
+
*
|
|
16770
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16771
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16772
|
+
* @returns Promise resolving to RiskStatistics object with counts and event list
|
|
16773
|
+
*
|
|
16774
|
+
* @example
|
|
16775
|
+
* ```typescript
|
|
16776
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16777
|
+
*
|
|
16778
|
+
* console.log(`Total rejections: ${stats.totalRejections}`);
|
|
16779
|
+
* console.log(`Rejections by symbol:`, stats.bySymbol);
|
|
16780
|
+
* console.log(`Rejections by strategy:`, stats.byStrategy);
|
|
16781
|
+
*
|
|
16782
|
+
* // Iterate through all rejection events
|
|
16783
|
+
* for (const event of stats.eventList) {
|
|
16784
|
+
* console.log(`REJECTED: ${event.symbol} - ${event.comment} (${event.activePositionCount} active)`);
|
|
16785
|
+
* }
|
|
16786
|
+
* ```
|
|
16787
|
+
*/
|
|
16788
|
+
this.getData = async (symbol, strategyName) => {
|
|
16789
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_GET_DATA, { symbol, strategyName });
|
|
16790
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_DATA);
|
|
16791
|
+
{
|
|
16792
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16793
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_DATA);
|
|
16794
|
+
}
|
|
16795
|
+
return await backtest$1.riskMarkdownService.getData(symbol, strategyName);
|
|
16796
|
+
};
|
|
16797
|
+
/**
|
|
16798
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair.
|
|
16799
|
+
*
|
|
16800
|
+
* Creates formatted table containing:
|
|
16801
|
+
* - Symbol
|
|
16802
|
+
* - Strategy
|
|
16803
|
+
* - Position (LONG/SHORT)
|
|
16804
|
+
* - Exchange
|
|
16805
|
+
* - Price
|
|
16806
|
+
* - Active Positions (at rejection time)
|
|
16807
|
+
* - Reason (from validation note)
|
|
16808
|
+
* - Timestamp (ISO 8601)
|
|
16809
|
+
*
|
|
16810
|
+
* Also includes summary statistics at the end (total rejections, by symbol, by strategy).
|
|
16811
|
+
*
|
|
16812
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16813
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16814
|
+
* @returns Promise resolving to markdown formatted report string
|
|
16815
|
+
*
|
|
16816
|
+
* @example
|
|
16817
|
+
* ```typescript
|
|
16818
|
+
* const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16819
|
+
* console.log(markdown);
|
|
16820
|
+
*
|
|
16821
|
+
* // Output:
|
|
16822
|
+
* // # Risk Rejection Report: BTCUSDT:my-strategy
|
|
16823
|
+
* //
|
|
16824
|
+
* // | Symbol | Strategy | Position | Exchange | Price | Active Positions | Reason | Timestamp |
|
|
16825
|
+
* // | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
16826
|
+
* // | BTCUSDT | my-strategy | LONG | binance | 50000.00000000 USD | 3 | Max 3 positions allowed | 2024-01-15T10:30:00.000Z |
|
|
16827
|
+
* //
|
|
16828
|
+
* // **Total rejections:** 1
|
|
16829
|
+
* //
|
|
16830
|
+
* // ## Rejections by Symbol
|
|
16831
|
+
* // - BTCUSDT: 1
|
|
16832
|
+
* //
|
|
16833
|
+
* // ## Rejections by Strategy
|
|
16834
|
+
* // - my-strategy: 1
|
|
16835
|
+
* ```
|
|
16836
|
+
*/
|
|
16837
|
+
this.getReport = async (symbol, strategyName) => {
|
|
16838
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_GET_REPORT, { symbol, strategyName });
|
|
16839
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_REPORT);
|
|
16840
|
+
{
|
|
16841
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16842
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_REPORT);
|
|
16843
|
+
}
|
|
16844
|
+
return await backtest$1.riskMarkdownService.getReport(symbol, strategyName);
|
|
16845
|
+
};
|
|
16846
|
+
/**
|
|
16847
|
+
* Generates and saves markdown report to file.
|
|
16848
|
+
*
|
|
16849
|
+
* Creates directory if it doesn't exist.
|
|
16850
|
+
* Filename format: {symbol}_{strategyName}.md (e.g., "BTCUSDT_my-strategy.md")
|
|
16851
|
+
*
|
|
16852
|
+
* Delegates to RiskMarkdownService.dump() which:
|
|
16853
|
+
* 1. Generates markdown report via getReport()
|
|
16854
|
+
* 2. Creates output directory (recursive mkdir)
|
|
16855
|
+
* 3. Writes file with UTF-8 encoding
|
|
16856
|
+
* 4. Logs success/failure to console
|
|
16857
|
+
*
|
|
16858
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16859
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16860
|
+
* @param path - Output directory path (default: "./dump/risk")
|
|
16861
|
+
* @returns Promise that resolves when file is written
|
|
16862
|
+
*
|
|
16863
|
+
* @example
|
|
16864
|
+
* ```typescript
|
|
16865
|
+
* // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
|
|
16866
|
+
* await Risk.dump("BTCUSDT", "my-strategy");
|
|
16867
|
+
*
|
|
16868
|
+
* // Save to custom path: ./reports/risk/BTCUSDT_my-strategy.md
|
|
16869
|
+
* await Risk.dump("BTCUSDT", "my-strategy", "./reports/risk");
|
|
16870
|
+
*
|
|
16871
|
+
* // After multiple symbols backtested, export all risk reports
|
|
16872
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
|
|
16873
|
+
* await Risk.dump(symbol, "my-strategy", "./backtest-results");
|
|
16874
|
+
* }
|
|
16875
|
+
* ```
|
|
16876
|
+
*/
|
|
16877
|
+
this.dump = async (symbol, strategyName, path) => {
|
|
16878
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_DUMP, { symbol, strategyName, path });
|
|
16879
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_DUMP);
|
|
16880
|
+
{
|
|
16881
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16882
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_DUMP);
|
|
16883
|
+
}
|
|
16884
|
+
await backtest$1.riskMarkdownService.dump(symbol, strategyName, path);
|
|
16885
|
+
};
|
|
16886
|
+
}
|
|
16887
|
+
}
|
|
16888
|
+
/**
|
|
16889
|
+
* Global singleton instance of RiskUtils.
|
|
16890
|
+
* Provides static-like access to risk rejection reporting methods.
|
|
16891
|
+
*
|
|
16892
|
+
* @example
|
|
16893
|
+
* ```typescript
|
|
16894
|
+
* import { Risk } from "backtest-kit";
|
|
16895
|
+
*
|
|
16896
|
+
* // Usage same as RiskUtils methods
|
|
16897
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16898
|
+
* const report = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16899
|
+
* await Risk.dump("BTCUSDT", "my-strategy");
|
|
16900
|
+
* ```
|
|
16901
|
+
*/
|
|
16902
|
+
const Risk = new RiskUtils();
|
|
16903
|
+
|
|
16234
16904
|
exports.Backtest = Backtest;
|
|
16235
16905
|
exports.Constant = Constant;
|
|
16236
16906
|
exports.ExecutionContextService = ExecutionContextService;
|
|
@@ -16246,6 +16916,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
|
16246
16916
|
exports.PersistScheduleAdapter = PersistScheduleAdapter;
|
|
16247
16917
|
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
16248
16918
|
exports.PositionSize = PositionSize;
|
|
16919
|
+
exports.Risk = Risk;
|
|
16249
16920
|
exports.Schedule = Schedule;
|
|
16250
16921
|
exports.Walker = Walker;
|
|
16251
16922
|
exports.addExchange = addExchange;
|
|
@@ -16288,6 +16959,8 @@ exports.listenPartialLossOnce = listenPartialLossOnce;
|
|
|
16288
16959
|
exports.listenPartialProfit = listenPartialProfit;
|
|
16289
16960
|
exports.listenPartialProfitOnce = listenPartialProfitOnce;
|
|
16290
16961
|
exports.listenPerformance = listenPerformance;
|
|
16962
|
+
exports.listenRisk = listenRisk;
|
|
16963
|
+
exports.listenRiskOnce = listenRiskOnce;
|
|
16291
16964
|
exports.listenSignal = listenSignal;
|
|
16292
16965
|
exports.listenSignalBacktest = listenSignalBacktest;
|
|
16293
16966
|
exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|