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