backtest-kit 1.5.15 → 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 +851 -119
- package/build/index.mjs +849 -120
- package/package.json +1 -1
- package/types.d.ts +540 -13
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,35 @@ 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();
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
/**
|
|
7101
|
+
* Adds an opened event to the storage.
|
|
7102
|
+
*
|
|
7103
|
+
* @param data - Opened tick result
|
|
7104
|
+
*/
|
|
7105
|
+
addOpenedEvent(data) {
|
|
7106
|
+
const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
|
|
7107
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
7108
|
+
const newEvent = {
|
|
7109
|
+
timestamp: data.signal.pendingAt,
|
|
7110
|
+
action: "opened",
|
|
7111
|
+
symbol: data.signal.symbol,
|
|
7112
|
+
signalId: data.signal.id,
|
|
7113
|
+
position: data.signal.position,
|
|
7114
|
+
note: data.signal.note,
|
|
7115
|
+
currentPrice: data.currentPrice,
|
|
7116
|
+
priceOpen: data.signal.priceOpen,
|
|
7117
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
7118
|
+
stopLoss: data.signal.priceStopLoss,
|
|
7119
|
+
duration: durationMin,
|
|
7120
|
+
};
|
|
7121
|
+
this._eventList.unshift(newEvent);
|
|
7122
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
7123
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7124
|
+
this._eventList.pop();
|
|
7033
7125
|
}
|
|
7034
7126
|
}
|
|
7035
7127
|
/**
|
|
@@ -7054,10 +7146,10 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7054
7146
|
closeTimestamp: data.closeTimestamp,
|
|
7055
7147
|
duration: durationMin,
|
|
7056
7148
|
};
|
|
7057
|
-
this._eventList.
|
|
7149
|
+
this._eventList.unshift(newEvent);
|
|
7058
7150
|
// Trim queue if exceeded MAX_EVENTS
|
|
7059
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7060
|
-
this._eventList.
|
|
7151
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7152
|
+
this._eventList.pop();
|
|
7061
7153
|
}
|
|
7062
7154
|
}
|
|
7063
7155
|
/**
|
|
@@ -7071,29 +7163,44 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7071
7163
|
eventList: [],
|
|
7072
7164
|
totalEvents: 0,
|
|
7073
7165
|
totalScheduled: 0,
|
|
7166
|
+
totalOpened: 0,
|
|
7074
7167
|
totalCancelled: 0,
|
|
7075
7168
|
cancellationRate: null,
|
|
7169
|
+
activationRate: null,
|
|
7076
7170
|
avgWaitTime: null,
|
|
7171
|
+
avgActivationTime: null,
|
|
7077
7172
|
};
|
|
7078
7173
|
}
|
|
7079
7174
|
const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
|
|
7175
|
+
const openedEvents = this._eventList.filter((e) => e.action === "opened");
|
|
7080
7176
|
const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
|
|
7081
7177
|
const totalScheduled = scheduledEvents.length;
|
|
7178
|
+
const totalOpened = openedEvents.length;
|
|
7082
7179
|
const totalCancelled = cancelledEvents.length;
|
|
7083
7180
|
// Calculate cancellation rate
|
|
7084
7181
|
const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
|
|
7182
|
+
// Calculate activation rate
|
|
7183
|
+
const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
|
|
7085
7184
|
// Calculate average wait time for cancelled signals
|
|
7086
7185
|
const avgWaitTime = totalCancelled > 0
|
|
7087
7186
|
? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
7088
7187
|
totalCancelled
|
|
7089
7188
|
: null;
|
|
7189
|
+
// Calculate average activation time for opened signals
|
|
7190
|
+
const avgActivationTime = totalOpened > 0
|
|
7191
|
+
? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
7192
|
+
totalOpened
|
|
7193
|
+
: null;
|
|
7090
7194
|
return {
|
|
7091
7195
|
eventList: this._eventList,
|
|
7092
7196
|
totalEvents: this._eventList.length,
|
|
7093
7197
|
totalScheduled,
|
|
7198
|
+
totalOpened,
|
|
7094
7199
|
totalCancelled,
|
|
7095
7200
|
cancellationRate,
|
|
7201
|
+
activationRate,
|
|
7096
7202
|
avgWaitTime,
|
|
7203
|
+
avgActivationTime,
|
|
7097
7204
|
};
|
|
7098
7205
|
}
|
|
7099
7206
|
/**
|
|
@@ -7111,9 +7218,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7111
7218
|
"No scheduled signals recorded yet."
|
|
7112
7219
|
].join("\n");
|
|
7113
7220
|
}
|
|
7114
|
-
const header = columns$
|
|
7115
|
-
const separator = columns$
|
|
7116
|
-
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)));
|
|
7117
7224
|
const tableData = [header, separator, ...rows];
|
|
7118
7225
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7119
7226
|
return [
|
|
@@ -7123,8 +7230,11 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7123
7230
|
"",
|
|
7124
7231
|
`**Total events:** ${stats.totalEvents}`,
|
|
7125
7232
|
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
7233
|
+
`**Opened signals:** ${stats.totalOpened}`,
|
|
7126
7234
|
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
7235
|
+
`**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
|
|
7127
7236
|
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
7237
|
+
`**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
|
|
7128
7238
|
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
|
|
7129
7239
|
].join("\n");
|
|
7130
7240
|
}
|
|
@@ -7178,12 +7288,12 @@ class ScheduleMarkdownService {
|
|
|
7178
7288
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
7179
7289
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
7180
7290
|
*/
|
|
7181
|
-
this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
7291
|
+
this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
|
|
7182
7292
|
/**
|
|
7183
|
-
* Processes tick events and accumulates scheduled/cancelled events.
|
|
7184
|
-
* Should be called from
|
|
7293
|
+
* Processes tick events and accumulates scheduled/opened/cancelled events.
|
|
7294
|
+
* Should be called from signalEmitter subscription.
|
|
7185
7295
|
*
|
|
7186
|
-
* Processes only scheduled and cancelled event types.
|
|
7296
|
+
* Processes only scheduled, opened and cancelled event types.
|
|
7187
7297
|
*
|
|
7188
7298
|
* @param data - Tick result from strategy execution
|
|
7189
7299
|
*
|
|
@@ -7201,6 +7311,13 @@ class ScheduleMarkdownService {
|
|
|
7201
7311
|
if (data.action === "scheduled") {
|
|
7202
7312
|
storage.addScheduledEvent(data);
|
|
7203
7313
|
}
|
|
7314
|
+
else if (data.action === "opened") {
|
|
7315
|
+
// Check if this opened signal was previously scheduled
|
|
7316
|
+
// by checking if signal has scheduledAt != pendingAt
|
|
7317
|
+
if (data.signal.scheduledAt !== data.signal.pendingAt) {
|
|
7318
|
+
storage.addOpenedEvent(data);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7204
7321
|
else if (data.action === "cancelled") {
|
|
7205
7322
|
storage.addCancelledEvent(data);
|
|
7206
7323
|
}
|
|
@@ -7338,7 +7455,7 @@ function percentile(sortedArray, p) {
|
|
|
7338
7455
|
return sortedArray[Math.max(0, index)];
|
|
7339
7456
|
}
|
|
7340
7457
|
/** Maximum number of performance events to store per strategy */
|
|
7341
|
-
const MAX_EVENTS$
|
|
7458
|
+
const MAX_EVENTS$3 = 10000;
|
|
7342
7459
|
/**
|
|
7343
7460
|
* Storage class for accumulating performance metrics per strategy.
|
|
7344
7461
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -7354,10 +7471,10 @@ class PerformanceStorage {
|
|
|
7354
7471
|
* @param event - Performance event with timing data
|
|
7355
7472
|
*/
|
|
7356
7473
|
addEvent(event) {
|
|
7357
|
-
this._events.
|
|
7474
|
+
this._events.unshift(event);
|
|
7358
7475
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
7359
|
-
if (this._events.length > MAX_EVENTS$
|
|
7360
|
-
this._events.
|
|
7476
|
+
if (this._events.length > MAX_EVENTS$3) {
|
|
7477
|
+
this._events.pop();
|
|
7361
7478
|
}
|
|
7362
7479
|
}
|
|
7363
7480
|
/**
|
|
@@ -7823,7 +7940,7 @@ const pnlColumns = [
|
|
|
7823
7940
|
* Storage class for accumulating walker results.
|
|
7824
7941
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
7825
7942
|
*/
|
|
7826
|
-
let ReportStorage$
|
|
7943
|
+
let ReportStorage$2 = class ReportStorage {
|
|
7827
7944
|
constructor(walkerName) {
|
|
7828
7945
|
this.walkerName = walkerName;
|
|
7829
7946
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -7841,17 +7958,13 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7841
7958
|
* @param data - Walker contract with strategy result
|
|
7842
7959
|
*/
|
|
7843
7960
|
addResult(data) {
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
this._totalStrategies = data.totalStrategies;
|
|
7848
|
-
}
|
|
7849
|
-
// 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;
|
|
7850
7964
|
if (data.strategyName === data.bestStrategy) {
|
|
7851
7965
|
this._bestStats = data.stats;
|
|
7852
7966
|
}
|
|
7853
|
-
|
|
7854
|
-
this._strategyResults.push({
|
|
7967
|
+
this._strategyResults.unshift({
|
|
7855
7968
|
strategyName: data.strategyName,
|
|
7856
7969
|
stats: data.stats,
|
|
7857
7970
|
metricValue: data.metricValue,
|
|
@@ -8035,7 +8148,7 @@ class WalkerMarkdownService {
|
|
|
8035
8148
|
* Memoized function to get or create ReportStorage for a walker.
|
|
8036
8149
|
* Each walker gets its own isolated storage instance.
|
|
8037
8150
|
*/
|
|
8038
|
-
this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$
|
|
8151
|
+
this.getStorage = memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
|
|
8039
8152
|
/**
|
|
8040
8153
|
* Processes walker progress events and accumulates strategy results.
|
|
8041
8154
|
* Should be called from walkerEmitter.
|
|
@@ -8206,7 +8319,7 @@ function isUnsafe(value) {
|
|
|
8206
8319
|
}
|
|
8207
8320
|
return false;
|
|
8208
8321
|
}
|
|
8209
|
-
const columns$
|
|
8322
|
+
const columns$2 = [
|
|
8210
8323
|
{
|
|
8211
8324
|
key: "symbol",
|
|
8212
8325
|
label: "Symbol",
|
|
@@ -8268,6 +8381,8 @@ const columns$1 = [
|
|
|
8268
8381
|
format: (data) => data.totalTrades.toString(),
|
|
8269
8382
|
},
|
|
8270
8383
|
];
|
|
8384
|
+
/** Maximum number of signals to store per symbol in heatmap reports */
|
|
8385
|
+
const MAX_EVENTS$2 = 250;
|
|
8271
8386
|
/**
|
|
8272
8387
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
8273
8388
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -8287,7 +8402,12 @@ class HeatmapStorage {
|
|
|
8287
8402
|
if (!this.symbolData.has(symbol)) {
|
|
8288
8403
|
this.symbolData.set(symbol, []);
|
|
8289
8404
|
}
|
|
8290
|
-
this.symbolData.get(symbol)
|
|
8405
|
+
const signals = this.symbolData.get(symbol);
|
|
8406
|
+
signals.unshift(data);
|
|
8407
|
+
// Trim queue if exceeded MAX_EVENTS per symbol
|
|
8408
|
+
if (signals.length > MAX_EVENTS$2) {
|
|
8409
|
+
signals.pop();
|
|
8410
|
+
}
|
|
8291
8411
|
}
|
|
8292
8412
|
/**
|
|
8293
8413
|
* Calculates statistics for a single symbol.
|
|
@@ -8506,9 +8626,9 @@ class HeatmapStorage {
|
|
|
8506
8626
|
"*No data available*"
|
|
8507
8627
|
].join("\n");
|
|
8508
8628
|
}
|
|
8509
|
-
const header = columns$
|
|
8510
|
-
const separator = columns$
|
|
8511
|
-
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)));
|
|
8512
8632
|
const tableData = [header, separator, ...rows];
|
|
8513
8633
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8514
8634
|
return [
|
|
@@ -10546,7 +10666,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
|
|
|
10546
10666
|
revenuePercent,
|
|
10547
10667
|
backtest,
|
|
10548
10668
|
});
|
|
10549
|
-
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());
|
|
10550
10670
|
}
|
|
10551
10671
|
}
|
|
10552
10672
|
if (shouldPersist) {
|
|
@@ -10593,7 +10713,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
|
|
|
10593
10713
|
lossPercent,
|
|
10594
10714
|
backtest,
|
|
10595
10715
|
});
|
|
10596
|
-
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());
|
|
10597
10717
|
}
|
|
10598
10718
|
}
|
|
10599
10719
|
if (shouldPersist) {
|
|
@@ -10864,6 +10984,7 @@ class ClientPartial {
|
|
|
10864
10984
|
symbol,
|
|
10865
10985
|
data,
|
|
10866
10986
|
priceClose,
|
|
10987
|
+
backtest,
|
|
10867
10988
|
});
|
|
10868
10989
|
if (this._states === NEED_FETCH) {
|
|
10869
10990
|
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
@@ -10880,14 +11001,18 @@ class ClientPartial {
|
|
|
10880
11001
|
* Emits PartialProfitContract event to all subscribers.
|
|
10881
11002
|
*
|
|
10882
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
|
|
10883
11006
|
* @param data - Signal row data
|
|
10884
11007
|
* @param currentPrice - Current market price
|
|
10885
11008
|
* @param level - Profit level reached
|
|
10886
11009
|
* @param backtest - True if backtest mode
|
|
10887
11010
|
* @param timestamp - Event timestamp in milliseconds
|
|
10888
11011
|
*/
|
|
10889
|
-
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({
|
|
10890
11013
|
symbol,
|
|
11014
|
+
strategyName,
|
|
11015
|
+
exchangeName,
|
|
10891
11016
|
data,
|
|
10892
11017
|
currentPrice,
|
|
10893
11018
|
level,
|
|
@@ -10901,14 +11026,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
|
|
|
10901
11026
|
* Emits PartialLossContract event to all subscribers.
|
|
10902
11027
|
*
|
|
10903
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
|
|
10904
11031
|
* @param data - Signal row data
|
|
10905
11032
|
* @param currentPrice - Current market price
|
|
10906
11033
|
* @param level - Loss level reached
|
|
10907
11034
|
* @param backtest - True if backtest mode
|
|
10908
11035
|
* @param timestamp - Event timestamp in milliseconds
|
|
10909
11036
|
*/
|
|
10910
|
-
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({
|
|
10911
11038
|
symbol,
|
|
11039
|
+
strategyName,
|
|
11040
|
+
exchangeName,
|
|
10912
11041
|
data,
|
|
10913
11042
|
currentPrice,
|
|
10914
11043
|
level,
|
|
@@ -11054,7 +11183,7 @@ class PartialConnectionService {
|
|
|
11054
11183
|
}
|
|
11055
11184
|
}
|
|
11056
11185
|
|
|
11057
|
-
const columns = [
|
|
11186
|
+
const columns$1 = [
|
|
11058
11187
|
{
|
|
11059
11188
|
key: "action",
|
|
11060
11189
|
label: "Action",
|
|
@@ -11102,12 +11231,12 @@ const columns = [
|
|
|
11102
11231
|
},
|
|
11103
11232
|
];
|
|
11104
11233
|
/** Maximum number of events to store in partial reports */
|
|
11105
|
-
const MAX_EVENTS = 250;
|
|
11234
|
+
const MAX_EVENTS$1 = 250;
|
|
11106
11235
|
/**
|
|
11107
11236
|
* Storage class for accumulating partial profit/loss events per symbol-strategy pair.
|
|
11108
11237
|
* Maintains a chronological list of profit and loss level events.
|
|
11109
11238
|
*/
|
|
11110
|
-
class ReportStorage {
|
|
11239
|
+
let ReportStorage$1 = class ReportStorage {
|
|
11111
11240
|
constructor() {
|
|
11112
11241
|
/** Internal list of all partial events for this symbol */
|
|
11113
11242
|
this._eventList = [];
|
|
@@ -11121,7 +11250,7 @@ class ReportStorage {
|
|
|
11121
11250
|
* @param backtest - True if backtest mode
|
|
11122
11251
|
*/
|
|
11123
11252
|
addProfitEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11124
|
-
this._eventList.
|
|
11253
|
+
this._eventList.unshift({
|
|
11125
11254
|
timestamp,
|
|
11126
11255
|
action: "profit",
|
|
11127
11256
|
symbol: data.symbol,
|
|
@@ -11133,8 +11262,8 @@ class ReportStorage {
|
|
|
11133
11262
|
backtest,
|
|
11134
11263
|
});
|
|
11135
11264
|
// Trim queue if exceeded MAX_EVENTS
|
|
11136
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11137
|
-
this._eventList.
|
|
11265
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11266
|
+
this._eventList.pop();
|
|
11138
11267
|
}
|
|
11139
11268
|
}
|
|
11140
11269
|
/**
|
|
@@ -11146,7 +11275,7 @@ class ReportStorage {
|
|
|
11146
11275
|
* @param backtest - True if backtest mode
|
|
11147
11276
|
*/
|
|
11148
11277
|
addLossEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11149
|
-
this._eventList.
|
|
11278
|
+
this._eventList.unshift({
|
|
11150
11279
|
timestamp,
|
|
11151
11280
|
action: "loss",
|
|
11152
11281
|
symbol: data.symbol,
|
|
@@ -11158,8 +11287,8 @@ class ReportStorage {
|
|
|
11158
11287
|
backtest,
|
|
11159
11288
|
});
|
|
11160
11289
|
// Trim queue if exceeded MAX_EVENTS
|
|
11161
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11162
|
-
this._eventList.
|
|
11290
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11291
|
+
this._eventList.pop();
|
|
11163
11292
|
}
|
|
11164
11293
|
}
|
|
11165
11294
|
/**
|
|
@@ -11201,9 +11330,9 @@ class ReportStorage {
|
|
|
11201
11330
|
"No partial profit/loss events recorded yet."
|
|
11202
11331
|
].join("\n");
|
|
11203
11332
|
}
|
|
11204
|
-
const header = columns.map((col) => col.label);
|
|
11205
|
-
const separator = columns.map(() => "---");
|
|
11206
|
-
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)));
|
|
11207
11336
|
const tableData = [header, separator, ...rows];
|
|
11208
11337
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
11209
11338
|
return [
|
|
@@ -11237,7 +11366,7 @@ class ReportStorage {
|
|
|
11237
11366
|
console.error(`Failed to save markdown report:`, error);
|
|
11238
11367
|
}
|
|
11239
11368
|
}
|
|
11240
|
-
}
|
|
11369
|
+
};
|
|
11241
11370
|
/**
|
|
11242
11371
|
* Service for generating and saving partial profit/loss markdown reports.
|
|
11243
11372
|
*
|
|
@@ -11267,7 +11396,7 @@ class PartialMarkdownService {
|
|
|
11267
11396
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
11268
11397
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
11269
11398
|
*/
|
|
11270
|
-
this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
|
|
11399
|
+
this.getStorage = memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
|
|
11271
11400
|
/**
|
|
11272
11401
|
* Processes profit events and accumulates them.
|
|
11273
11402
|
* Should be called from partialProfitSubject subscription.
|
|
@@ -11577,7 +11706,7 @@ class PartialGlobalService {
|
|
|
11577
11706
|
* Warning threshold for message size in kilobytes.
|
|
11578
11707
|
* Messages exceeding this size trigger console warnings.
|
|
11579
11708
|
*/
|
|
11580
|
-
const WARN_KB =
|
|
11709
|
+
const WARN_KB = 20;
|
|
11581
11710
|
/**
|
|
11582
11711
|
* Internal function for dumping signal data to markdown files.
|
|
11583
11712
|
* Creates a directory structure with system prompts, user messages, and LLM output.
|
|
@@ -11839,35 +11968,382 @@ class ConfigValidationService {
|
|
|
11839
11968
|
}
|
|
11840
11969
|
}
|
|
11841
11970
|
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
}
|
|
11858
|
-
{
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
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
|
+
}
|
|
11866
12145
|
}
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
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
|
+
|
|
12318
|
+
{
|
|
12319
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
12320
|
+
}
|
|
12321
|
+
{
|
|
12322
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
12323
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
12324
|
+
}
|
|
12325
|
+
{
|
|
12326
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
12327
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
12328
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
12329
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
12330
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
12331
|
+
provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
|
|
12332
|
+
provide(TYPES.partialConnectionService, () => new PartialConnectionService());
|
|
12333
|
+
}
|
|
12334
|
+
{
|
|
12335
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
12336
|
+
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
12337
|
+
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
12338
|
+
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
|
|
12339
|
+
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
|
|
12340
|
+
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
|
|
12341
|
+
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
|
|
12342
|
+
}
|
|
12343
|
+
{
|
|
12344
|
+
provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
|
|
12345
|
+
provide(TYPES.strategyCoreService, () => new StrategyCoreService());
|
|
12346
|
+
provide(TYPES.frameCoreService, () => new FrameCoreService());
|
|
11871
12347
|
}
|
|
11872
12348
|
{
|
|
11873
12349
|
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
@@ -11899,6 +12375,7 @@ class ConfigValidationService {
|
|
|
11899
12375
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
11900
12376
|
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
11901
12377
|
provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
|
|
12378
|
+
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
11902
12379
|
}
|
|
11903
12380
|
{
|
|
11904
12381
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -11974,6 +12451,7 @@ const markdownServices = {
|
|
|
11974
12451
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
11975
12452
|
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
11976
12453
|
outlineMarkdownService: inject(TYPES.outlineMarkdownService),
|
|
12454
|
+
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
11977
12455
|
};
|
|
11978
12456
|
const validationServices = {
|
|
11979
12457
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -12748,6 +13226,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
|
|
|
12748
13226
|
const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
|
|
12749
13227
|
const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
|
|
12750
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";
|
|
12751
13231
|
/**
|
|
12752
13232
|
* Subscribes to all signal events with queued async processing.
|
|
12753
13233
|
*
|
|
@@ -13546,6 +14026,75 @@ function listenPartialLossOnce(filterFn, fn) {
|
|
|
13546
14026
|
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
|
|
13547
14027
|
return partialLossSubject.filter(filterFn).once(fn);
|
|
13548
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
|
+
}
|
|
13549
14098
|
|
|
13550
14099
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
13551
14100
|
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
@@ -16170,4 +16719,184 @@ class ConstantUtils {
|
|
|
16170
16719
|
*/
|
|
16171
16720
|
const Constant = new ConstantUtils();
|
|
16172
16721
|
|
|
16173
|
-
|
|
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 };
|