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.cjs
CHANGED
|
@@ -202,6 +202,7 @@ const markdownServices$1 = {
|
|
|
202
202
|
heatMarkdownService: Symbol('heatMarkdownService'),
|
|
203
203
|
partialMarkdownService: Symbol('partialMarkdownService'),
|
|
204
204
|
outlineMarkdownService: Symbol('outlineMarkdownService'),
|
|
205
|
+
riskMarkdownService: Symbol('riskMarkdownService'),
|
|
205
206
|
};
|
|
206
207
|
const validationServices$1 = {
|
|
207
208
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -442,11 +443,12 @@ const GET_CANDLES_FN = async (dto, since, self) => {
|
|
|
442
443
|
}
|
|
443
444
|
catch (err) {
|
|
444
445
|
const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
|
|
445
|
-
|
|
446
|
+
const payload = {
|
|
446
447
|
error: functoolsKit.errorData(err),
|
|
447
448
|
message: functoolsKit.getErrorMessage(err),
|
|
448
|
-
}
|
|
449
|
-
|
|
449
|
+
};
|
|
450
|
+
self.params.logger.warn(message, payload);
|
|
451
|
+
console.warn(message, payload);
|
|
450
452
|
lastError = err;
|
|
451
453
|
await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
|
|
452
454
|
}
|
|
@@ -1725,6 +1727,12 @@ const partialProfitSubject = new functoolsKit.Subject();
|
|
|
1725
1727
|
* Emits when a signal reaches a loss level (10%, 20%, 30%, etc).
|
|
1726
1728
|
*/
|
|
1727
1729
|
const partialLossSubject = new functoolsKit.Subject();
|
|
1730
|
+
/**
|
|
1731
|
+
* Risk rejection emitter for risk management violations.
|
|
1732
|
+
* Emits ONLY when a signal is rejected due to risk validation failure.
|
|
1733
|
+
* Does not emit for allowed signals (prevents spam).
|
|
1734
|
+
*/
|
|
1735
|
+
const riskSubject = new functoolsKit.Subject();
|
|
1728
1736
|
|
|
1729
1737
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
1730
1738
|
__proto__: null,
|
|
@@ -1739,6 +1747,7 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
1739
1747
|
progressBacktestEmitter: progressBacktestEmitter,
|
|
1740
1748
|
progressOptimizerEmitter: progressOptimizerEmitter,
|
|
1741
1749
|
progressWalkerEmitter: progressWalkerEmitter,
|
|
1750
|
+
riskSubject: riskSubject,
|
|
1742
1751
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
1743
1752
|
signalEmitter: signalEmitter,
|
|
1744
1753
|
signalLiveEmitter: signalLiveEmitter,
|
|
@@ -1825,6 +1834,9 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
|
1825
1834
|
if (signal.position === undefined || signal.position === null) {
|
|
1826
1835
|
errors.push('position is required and must be "long" or "short"');
|
|
1827
1836
|
}
|
|
1837
|
+
if (signal.position !== "long" && signal.position !== "short") {
|
|
1838
|
+
errors.push(`position must be "long" or "short", got "${signal.position}"`);
|
|
1839
|
+
}
|
|
1828
1840
|
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
1829
1841
|
if (!isFinite(currentPrice)) {
|
|
1830
1842
|
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
@@ -2111,10 +2123,13 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
2111
2123
|
}, {
|
|
2112
2124
|
defaultValue: null,
|
|
2113
2125
|
fallback: (error) => {
|
|
2114
|
-
|
|
2126
|
+
const message = "ClientStrategy exception thrown";
|
|
2127
|
+
const payload = {
|
|
2115
2128
|
error: functoolsKit.errorData(error),
|
|
2116
2129
|
message: functoolsKit.getErrorMessage(error),
|
|
2117
|
-
}
|
|
2130
|
+
};
|
|
2131
|
+
backtest$1.loggerService.warn(message, payload);
|
|
2132
|
+
console.warn(message, payload);
|
|
2118
2133
|
errorEmitter.next(error);
|
|
2119
2134
|
},
|
|
2120
2135
|
});
|
|
@@ -2805,7 +2820,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2805
2820
|
// Moving towards TP
|
|
2806
2821
|
const tpDistance = signal.priceTakeProfit - signal.priceOpen;
|
|
2807
2822
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2808
|
-
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2823
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2809
2824
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2810
2825
|
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2811
2826
|
}
|
|
@@ -2814,7 +2829,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2814
2829
|
// Moving towards SL
|
|
2815
2830
|
const slDistance = signal.priceOpen - signal.priceStopLoss;
|
|
2816
2831
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2817
|
-
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2832
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2818
2833
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2819
2834
|
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2820
2835
|
}
|
|
@@ -2827,7 +2842,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2827
2842
|
// Moving towards TP
|
|
2828
2843
|
const tpDistance = signal.priceOpen - signal.priceTakeProfit;
|
|
2829
2844
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
2830
|
-
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2845
|
+
await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2831
2846
|
if (self.params.callbacks?.onPartialProfit) {
|
|
2832
2847
|
self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2833
2848
|
}
|
|
@@ -2836,7 +2851,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
2836
2851
|
// Moving towards SL
|
|
2837
2852
|
const slDistance = signal.priceStopLoss - signal.priceOpen;
|
|
2838
2853
|
const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
|
|
2839
|
-
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest,
|
|
2854
|
+
await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, new Date(currentCandleTimestamp));
|
|
2840
2855
|
if (self.params.callbacks?.onPartialLoss) {
|
|
2841
2856
|
self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
|
|
2842
2857
|
}
|
|
@@ -3791,10 +3806,13 @@ const DO_VALIDATION_FN = functoolsKit.trycatch(async (validation, params) => {
|
|
|
3791
3806
|
}, {
|
|
3792
3807
|
defaultValue: false,
|
|
3793
3808
|
fallback: (error) => {
|
|
3794
|
-
|
|
3809
|
+
const message = "ClientRisk exception thrown";
|
|
3810
|
+
const payload = {
|
|
3795
3811
|
error: functoolsKit.errorData(error),
|
|
3796
3812
|
message: functoolsKit.getErrorMessage(error),
|
|
3797
|
-
}
|
|
3813
|
+
};
|
|
3814
|
+
backtest$1.loggerService.warn(message, payload);
|
|
3815
|
+
console.warn(message, payload);
|
|
3798
3816
|
validationSubject.next(error);
|
|
3799
3817
|
},
|
|
3800
3818
|
});
|
|
@@ -3864,17 +3882,25 @@ class ClientRisk {
|
|
|
3864
3882
|
};
|
|
3865
3883
|
// Execute custom validations
|
|
3866
3884
|
let isValid = true;
|
|
3885
|
+
let rejectionNote = "N/A";
|
|
3867
3886
|
if (this.params.validations) {
|
|
3868
3887
|
for (const validation of this.params.validations) {
|
|
3869
3888
|
if (functoolsKit.not(await DO_VALIDATION_FN(typeof validation === "function"
|
|
3870
3889
|
? validation
|
|
3871
3890
|
: validation.validate, payload))) {
|
|
3872
3891
|
isValid = false;
|
|
3892
|
+
// Capture note from validation if available
|
|
3893
|
+
if (typeof validation !== "function" && validation.note) {
|
|
3894
|
+
rejectionNote = validation.note;
|
|
3895
|
+
}
|
|
3873
3896
|
break;
|
|
3874
3897
|
}
|
|
3875
3898
|
}
|
|
3876
3899
|
}
|
|
3877
3900
|
if (!isValid) {
|
|
3901
|
+
// Call params.onRejected for riskSubject emission
|
|
3902
|
+
await this.params.onRejected(params.symbol, params, riskMap.size, rejectionNote, Date.now());
|
|
3903
|
+
// Call schema callbacks.onRejected if defined
|
|
3878
3904
|
if (this.params.callbacks?.onRejected) {
|
|
3879
3905
|
this.params.callbacks.onRejected(params.symbol, params);
|
|
3880
3906
|
}
|
|
@@ -3937,6 +3963,28 @@ class ClientRisk {
|
|
|
3937
3963
|
}
|
|
3938
3964
|
}
|
|
3939
3965
|
|
|
3966
|
+
/**
|
|
3967
|
+
* Callback function for emitting risk rejection events to riskSubject.
|
|
3968
|
+
*
|
|
3969
|
+
* Called by ClientRisk when a signal is rejected due to risk validation failure.
|
|
3970
|
+
* Emits RiskContract event to all subscribers.
|
|
3971
|
+
*
|
|
3972
|
+
* @param symbol - Trading pair symbol
|
|
3973
|
+
* @param params - Risk check arguments
|
|
3974
|
+
* @param activePositionCount - Number of active positions at rejection time
|
|
3975
|
+
* @param comment - Rejection reason from validation note or "N/A"
|
|
3976
|
+
* @param timestamp - Event timestamp in milliseconds
|
|
3977
|
+
*/
|
|
3978
|
+
const COMMIT_REJECTION_FN = async (symbol, params, activePositionCount, comment, timestamp) => await riskSubject.next({
|
|
3979
|
+
symbol,
|
|
3980
|
+
pendingSignal: params.pendingSignal,
|
|
3981
|
+
strategyName: params.strategyName,
|
|
3982
|
+
exchangeName: params.exchangeName,
|
|
3983
|
+
currentPrice: params.currentPrice,
|
|
3984
|
+
activePositionCount,
|
|
3985
|
+
comment,
|
|
3986
|
+
timestamp,
|
|
3987
|
+
});
|
|
3940
3988
|
/**
|
|
3941
3989
|
* Connection service routing risk operations to correct ClientRisk instance.
|
|
3942
3990
|
*
|
|
@@ -3987,6 +4035,7 @@ class RiskConnectionService {
|
|
|
3987
4035
|
return new ClientRisk({
|
|
3988
4036
|
...schema,
|
|
3989
4037
|
logger: this.loggerService,
|
|
4038
|
+
onRejected: COMMIT_REJECTION_FN,
|
|
3990
4039
|
});
|
|
3991
4040
|
});
|
|
3992
4041
|
/**
|
|
@@ -3994,6 +4043,7 @@ class RiskConnectionService {
|
|
|
3994
4043
|
*
|
|
3995
4044
|
* Routes to appropriate ClientRisk instance based on provided context.
|
|
3996
4045
|
* Validates portfolio drawdown, symbol exposure, position count, and daily loss limits.
|
|
4046
|
+
* ClientRisk will emit riskSubject event via onRejected callback when signal is rejected.
|
|
3997
4047
|
*
|
|
3998
4048
|
* @param params - Risk check arguments (portfolio state, position details)
|
|
3999
4049
|
* @param context - Execution context with risk name
|
|
@@ -5982,7 +6032,7 @@ function isUnsafe$3(value) {
|
|
|
5982
6032
|
}
|
|
5983
6033
|
return false;
|
|
5984
6034
|
}
|
|
5985
|
-
const columns$
|
|
6035
|
+
const columns$5 = [
|
|
5986
6036
|
{
|
|
5987
6037
|
key: "signalId",
|
|
5988
6038
|
label: "Signal ID",
|
|
@@ -6056,11 +6106,13 @@ const columns$4 = [
|
|
|
6056
6106
|
format: (data) => new Date(data.closeTimestamp).toISOString(),
|
|
6057
6107
|
},
|
|
6058
6108
|
];
|
|
6109
|
+
/** Maximum number of signals to store in backtest reports */
|
|
6110
|
+
const MAX_EVENTS$6 = 250;
|
|
6059
6111
|
/**
|
|
6060
6112
|
* Storage class for accumulating closed signals per strategy.
|
|
6061
6113
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
6062
6114
|
*/
|
|
6063
|
-
let ReportStorage$
|
|
6115
|
+
let ReportStorage$5 = class ReportStorage {
|
|
6064
6116
|
constructor() {
|
|
6065
6117
|
/** Internal list of all closed signals for this strategy */
|
|
6066
6118
|
this._signalList = [];
|
|
@@ -6071,7 +6123,11 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
6071
6123
|
* @param data - Closed signal data with PNL and close reason
|
|
6072
6124
|
*/
|
|
6073
6125
|
addSignal(data) {
|
|
6074
|
-
this._signalList.
|
|
6126
|
+
this._signalList.unshift(data);
|
|
6127
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
6128
|
+
if (this._signalList.length > MAX_EVENTS$6) {
|
|
6129
|
+
this._signalList.pop();
|
|
6130
|
+
}
|
|
6075
6131
|
}
|
|
6076
6132
|
/**
|
|
6077
6133
|
* Calculates statistical data from closed signals (Controller).
|
|
@@ -6154,9 +6210,9 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
6154
6210
|
"No signals closed yet."
|
|
6155
6211
|
].join("\n");
|
|
6156
6212
|
}
|
|
6157
|
-
const header = columns$
|
|
6158
|
-
const separator = columns$
|
|
6159
|
-
const rows = this._signalList.map((closedSignal) => columns$
|
|
6213
|
+
const header = columns$5.map((col) => col.label);
|
|
6214
|
+
const separator = columns$5.map(() => "---");
|
|
6215
|
+
const rows = this._signalList.map((closedSignal) => columns$5.map((col) => col.format(closedSignal)));
|
|
6160
6216
|
const tableData = [header, separator, ...rows];
|
|
6161
6217
|
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6162
6218
|
return [
|
|
@@ -6232,7 +6288,7 @@ class BacktestMarkdownService {
|
|
|
6232
6288
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
6233
6289
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
6234
6290
|
*/
|
|
6235
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
6291
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$5());
|
|
6236
6292
|
/**
|
|
6237
6293
|
* Processes tick events and accumulates closed signals.
|
|
6238
6294
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -6403,7 +6459,7 @@ function isUnsafe$2(value) {
|
|
|
6403
6459
|
}
|
|
6404
6460
|
return false;
|
|
6405
6461
|
}
|
|
6406
|
-
const columns$
|
|
6462
|
+
const columns$4 = [
|
|
6407
6463
|
{
|
|
6408
6464
|
key: "timestamp",
|
|
6409
6465
|
label: "Timestamp",
|
|
@@ -6487,12 +6543,12 @@ const columns$3 = [
|
|
|
6487
6543
|
},
|
|
6488
6544
|
];
|
|
6489
6545
|
/** Maximum number of events to store in live trading reports */
|
|
6490
|
-
const MAX_EVENTS$
|
|
6546
|
+
const MAX_EVENTS$5 = 250;
|
|
6491
6547
|
/**
|
|
6492
6548
|
* Storage class for accumulating all tick events per strategy.
|
|
6493
6549
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
6494
6550
|
*/
|
|
6495
|
-
let ReportStorage$
|
|
6551
|
+
let ReportStorage$4 = class ReportStorage {
|
|
6496
6552
|
constructor() {
|
|
6497
6553
|
/** Internal list of all tick events for this strategy */
|
|
6498
6554
|
this._eventList = [];
|
|
@@ -6519,9 +6575,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6519
6575
|
return;
|
|
6520
6576
|
}
|
|
6521
6577
|
{
|
|
6522
|
-
this._eventList.
|
|
6523
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6524
|
-
this._eventList.
|
|
6578
|
+
this._eventList.unshift(newEvent);
|
|
6579
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6580
|
+
this._eventList.pop();
|
|
6525
6581
|
}
|
|
6526
6582
|
}
|
|
6527
6583
|
}
|
|
@@ -6531,7 +6587,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6531
6587
|
* @param data - Opened tick result
|
|
6532
6588
|
*/
|
|
6533
6589
|
addOpenedEvent(data) {
|
|
6534
|
-
this._eventList.
|
|
6590
|
+
this._eventList.unshift({
|
|
6535
6591
|
timestamp: data.signal.pendingAt,
|
|
6536
6592
|
action: "opened",
|
|
6537
6593
|
symbol: data.signal.symbol,
|
|
@@ -6544,12 +6600,13 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6544
6600
|
stopLoss: data.signal.priceStopLoss,
|
|
6545
6601
|
});
|
|
6546
6602
|
// Trim queue if exceeded MAX_EVENTS
|
|
6547
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6548
|
-
this._eventList.
|
|
6603
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6604
|
+
this._eventList.pop();
|
|
6549
6605
|
}
|
|
6550
6606
|
}
|
|
6551
6607
|
/**
|
|
6552
6608
|
* Adds an active event to the storage.
|
|
6609
|
+
* Replaces the last active event with the same signalId.
|
|
6553
6610
|
*
|
|
6554
6611
|
* @param data - Active tick result
|
|
6555
6612
|
*/
|
|
@@ -6568,10 +6625,18 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6568
6625
|
percentTp: data.percentTp,
|
|
6569
6626
|
percentSl: data.percentSl,
|
|
6570
6627
|
};
|
|
6571
|
-
|
|
6628
|
+
// Find the last active event with the same signalId
|
|
6629
|
+
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
6630
|
+
// Replace the last active event with the same signalId
|
|
6631
|
+
if (lastActiveIndex !== -1) {
|
|
6632
|
+
this._eventList[lastActiveIndex] = newEvent;
|
|
6633
|
+
return;
|
|
6634
|
+
}
|
|
6635
|
+
// If no previous active event found, add new event
|
|
6636
|
+
this._eventList.unshift(newEvent);
|
|
6572
6637
|
// Trim queue if exceeded MAX_EVENTS
|
|
6573
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6574
|
-
this._eventList.
|
|
6638
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6639
|
+
this._eventList.pop();
|
|
6575
6640
|
}
|
|
6576
6641
|
}
|
|
6577
6642
|
/**
|
|
@@ -6597,10 +6662,10 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6597
6662
|
closeReason: data.closeReason,
|
|
6598
6663
|
duration: durationMin,
|
|
6599
6664
|
};
|
|
6600
|
-
this._eventList.
|
|
6665
|
+
this._eventList.unshift(newEvent);
|
|
6601
6666
|
// Trim queue if exceeded MAX_EVENTS
|
|
6602
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
6603
|
-
this._eventList.
|
|
6667
|
+
if (this._eventList.length > MAX_EVENTS$5) {
|
|
6668
|
+
this._eventList.pop();
|
|
6604
6669
|
}
|
|
6605
6670
|
}
|
|
6606
6671
|
/**
|
|
@@ -6699,9 +6764,9 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
6699
6764
|
"No events recorded yet."
|
|
6700
6765
|
].join("\n");
|
|
6701
6766
|
}
|
|
6702
|
-
const header = columns$
|
|
6703
|
-
const separator = columns$
|
|
6704
|
-
const rows = this._eventList.map((event) => columns$
|
|
6767
|
+
const header = columns$4.map((col) => col.label);
|
|
6768
|
+
const separator = columns$4.map(() => "---");
|
|
6769
|
+
const rows = this._eventList.map((event) => columns$4.map((col) => col.format(event)));
|
|
6705
6770
|
const tableData = [header, separator, ...rows];
|
|
6706
6771
|
const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
|
|
6707
6772
|
return [
|
|
@@ -6780,7 +6845,7 @@ class LiveMarkdownService {
|
|
|
6780
6845
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
6781
6846
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
6782
6847
|
*/
|
|
6783
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
6848
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$4());
|
|
6784
6849
|
/**
|
|
6785
6850
|
* Processes tick events and accumulates all event types.
|
|
6786
6851
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -6943,7 +7008,7 @@ class LiveMarkdownService {
|
|
|
6943
7008
|
}
|
|
6944
7009
|
}
|
|
6945
7010
|
|
|
6946
|
-
const columns$
|
|
7011
|
+
const columns$3 = [
|
|
6947
7012
|
{
|
|
6948
7013
|
key: "timestamp",
|
|
6949
7014
|
label: "Timestamp",
|
|
@@ -7001,12 +7066,12 @@ const columns$2 = [
|
|
|
7001
7066
|
},
|
|
7002
7067
|
];
|
|
7003
7068
|
/** Maximum number of events to store in schedule reports */
|
|
7004
|
-
const MAX_EVENTS$
|
|
7069
|
+
const MAX_EVENTS$4 = 250;
|
|
7005
7070
|
/**
|
|
7006
7071
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
7007
7072
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
7008
7073
|
*/
|
|
7009
|
-
let ReportStorage$
|
|
7074
|
+
let ReportStorage$3 = class ReportStorage {
|
|
7010
7075
|
constructor() {
|
|
7011
7076
|
/** Internal list of all scheduled events for this strategy */
|
|
7012
7077
|
this._eventList = [];
|
|
@@ -7017,7 +7082,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7017
7082
|
* @param data - Scheduled tick result
|
|
7018
7083
|
*/
|
|
7019
7084
|
addScheduledEvent(data) {
|
|
7020
|
-
this._eventList.
|
|
7085
|
+
this._eventList.unshift({
|
|
7021
7086
|
timestamp: data.signal.scheduledAt,
|
|
7022
7087
|
action: "scheduled",
|
|
7023
7088
|
symbol: data.signal.symbol,
|
|
@@ -7030,8 +7095,35 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7030
7095
|
stopLoss: data.signal.priceStopLoss,
|
|
7031
7096
|
});
|
|
7032
7097
|
// Trim queue if exceeded MAX_EVENTS
|
|
7033
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7034
|
-
this._eventList.
|
|
7098
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7099
|
+
this._eventList.pop();
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
/**
|
|
7103
|
+
* Adds an opened event to the storage.
|
|
7104
|
+
*
|
|
7105
|
+
* @param data - Opened tick result
|
|
7106
|
+
*/
|
|
7107
|
+
addOpenedEvent(data) {
|
|
7108
|
+
const durationMs = data.signal.pendingAt - data.signal.scheduledAt;
|
|
7109
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
7110
|
+
const newEvent = {
|
|
7111
|
+
timestamp: data.signal.pendingAt,
|
|
7112
|
+
action: "opened",
|
|
7113
|
+
symbol: data.signal.symbol,
|
|
7114
|
+
signalId: data.signal.id,
|
|
7115
|
+
position: data.signal.position,
|
|
7116
|
+
note: data.signal.note,
|
|
7117
|
+
currentPrice: data.currentPrice,
|
|
7118
|
+
priceOpen: data.signal.priceOpen,
|
|
7119
|
+
takeProfit: data.signal.priceTakeProfit,
|
|
7120
|
+
stopLoss: data.signal.priceStopLoss,
|
|
7121
|
+
duration: durationMin,
|
|
7122
|
+
};
|
|
7123
|
+
this._eventList.unshift(newEvent);
|
|
7124
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
7125
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7126
|
+
this._eventList.pop();
|
|
7035
7127
|
}
|
|
7036
7128
|
}
|
|
7037
7129
|
/**
|
|
@@ -7056,10 +7148,10 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7056
7148
|
closeTimestamp: data.closeTimestamp,
|
|
7057
7149
|
duration: durationMin,
|
|
7058
7150
|
};
|
|
7059
|
-
this._eventList.
|
|
7151
|
+
this._eventList.unshift(newEvent);
|
|
7060
7152
|
// Trim queue if exceeded MAX_EVENTS
|
|
7061
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
7062
|
-
this._eventList.
|
|
7153
|
+
if (this._eventList.length > MAX_EVENTS$4) {
|
|
7154
|
+
this._eventList.pop();
|
|
7063
7155
|
}
|
|
7064
7156
|
}
|
|
7065
7157
|
/**
|
|
@@ -7073,29 +7165,44 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7073
7165
|
eventList: [],
|
|
7074
7166
|
totalEvents: 0,
|
|
7075
7167
|
totalScheduled: 0,
|
|
7168
|
+
totalOpened: 0,
|
|
7076
7169
|
totalCancelled: 0,
|
|
7077
7170
|
cancellationRate: null,
|
|
7171
|
+
activationRate: null,
|
|
7078
7172
|
avgWaitTime: null,
|
|
7173
|
+
avgActivationTime: null,
|
|
7079
7174
|
};
|
|
7080
7175
|
}
|
|
7081
7176
|
const scheduledEvents = this._eventList.filter((e) => e.action === "scheduled");
|
|
7177
|
+
const openedEvents = this._eventList.filter((e) => e.action === "opened");
|
|
7082
7178
|
const cancelledEvents = this._eventList.filter((e) => e.action === "cancelled");
|
|
7083
7179
|
const totalScheduled = scheduledEvents.length;
|
|
7180
|
+
const totalOpened = openedEvents.length;
|
|
7084
7181
|
const totalCancelled = cancelledEvents.length;
|
|
7085
7182
|
// Calculate cancellation rate
|
|
7086
7183
|
const cancellationRate = totalScheduled > 0 ? (totalCancelled / totalScheduled) * 100 : null;
|
|
7184
|
+
// Calculate activation rate
|
|
7185
|
+
const activationRate = totalScheduled > 0 ? (totalOpened / totalScheduled) * 100 : null;
|
|
7087
7186
|
// Calculate average wait time for cancelled signals
|
|
7088
7187
|
const avgWaitTime = totalCancelled > 0
|
|
7089
7188
|
? cancelledEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
7090
7189
|
totalCancelled
|
|
7091
7190
|
: null;
|
|
7191
|
+
// Calculate average activation time for opened signals
|
|
7192
|
+
const avgActivationTime = totalOpened > 0
|
|
7193
|
+
? openedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) /
|
|
7194
|
+
totalOpened
|
|
7195
|
+
: null;
|
|
7092
7196
|
return {
|
|
7093
7197
|
eventList: this._eventList,
|
|
7094
7198
|
totalEvents: this._eventList.length,
|
|
7095
7199
|
totalScheduled,
|
|
7200
|
+
totalOpened,
|
|
7096
7201
|
totalCancelled,
|
|
7097
7202
|
cancellationRate,
|
|
7203
|
+
activationRate,
|
|
7098
7204
|
avgWaitTime,
|
|
7205
|
+
avgActivationTime,
|
|
7099
7206
|
};
|
|
7100
7207
|
}
|
|
7101
7208
|
/**
|
|
@@ -7113,9 +7220,9 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7113
7220
|
"No scheduled signals recorded yet."
|
|
7114
7221
|
].join("\n");
|
|
7115
7222
|
}
|
|
7116
|
-
const header = columns$
|
|
7117
|
-
const separator = columns$
|
|
7118
|
-
const rows = this._eventList.map((event) => columns$
|
|
7223
|
+
const header = columns$3.map((col) => col.label);
|
|
7224
|
+
const separator = columns$3.map(() => "---");
|
|
7225
|
+
const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
|
|
7119
7226
|
const tableData = [header, separator, ...rows];
|
|
7120
7227
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
7121
7228
|
return [
|
|
@@ -7125,8 +7232,11 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
7125
7232
|
"",
|
|
7126
7233
|
`**Total events:** ${stats.totalEvents}`,
|
|
7127
7234
|
`**Scheduled signals:** ${stats.totalScheduled}`,
|
|
7235
|
+
`**Opened signals:** ${stats.totalOpened}`,
|
|
7128
7236
|
`**Cancelled signals:** ${stats.totalCancelled}`,
|
|
7237
|
+
`**Activation rate:** ${stats.activationRate === null ? "N/A" : `${stats.activationRate.toFixed(2)}% (higher is better)`}`,
|
|
7129
7238
|
`**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
|
|
7239
|
+
`**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
|
|
7130
7240
|
`**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
|
|
7131
7241
|
].join("\n");
|
|
7132
7242
|
}
|
|
@@ -7180,12 +7290,12 @@ class ScheduleMarkdownService {
|
|
|
7180
7290
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
7181
7291
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
7182
7292
|
*/
|
|
7183
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$
|
|
7293
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$3());
|
|
7184
7294
|
/**
|
|
7185
|
-
* Processes tick events and accumulates scheduled/cancelled events.
|
|
7186
|
-
* Should be called from
|
|
7295
|
+
* Processes tick events and accumulates scheduled/opened/cancelled events.
|
|
7296
|
+
* Should be called from signalEmitter subscription.
|
|
7187
7297
|
*
|
|
7188
|
-
* Processes only scheduled and cancelled event types.
|
|
7298
|
+
* Processes only scheduled, opened and cancelled event types.
|
|
7189
7299
|
*
|
|
7190
7300
|
* @param data - Tick result from strategy execution
|
|
7191
7301
|
*
|
|
@@ -7203,6 +7313,13 @@ class ScheduleMarkdownService {
|
|
|
7203
7313
|
if (data.action === "scheduled") {
|
|
7204
7314
|
storage.addScheduledEvent(data);
|
|
7205
7315
|
}
|
|
7316
|
+
else if (data.action === "opened") {
|
|
7317
|
+
// Check if this opened signal was previously scheduled
|
|
7318
|
+
// by checking if signal has scheduledAt != pendingAt
|
|
7319
|
+
if (data.signal.scheduledAt !== data.signal.pendingAt) {
|
|
7320
|
+
storage.addOpenedEvent(data);
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7206
7323
|
else if (data.action === "cancelled") {
|
|
7207
7324
|
storage.addCancelledEvent(data);
|
|
7208
7325
|
}
|
|
@@ -7340,7 +7457,7 @@ function percentile(sortedArray, p) {
|
|
|
7340
7457
|
return sortedArray[Math.max(0, index)];
|
|
7341
7458
|
}
|
|
7342
7459
|
/** Maximum number of performance events to store per strategy */
|
|
7343
|
-
const MAX_EVENTS$
|
|
7460
|
+
const MAX_EVENTS$3 = 10000;
|
|
7344
7461
|
/**
|
|
7345
7462
|
* Storage class for accumulating performance metrics per strategy.
|
|
7346
7463
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -7356,10 +7473,10 @@ class PerformanceStorage {
|
|
|
7356
7473
|
* @param event - Performance event with timing data
|
|
7357
7474
|
*/
|
|
7358
7475
|
addEvent(event) {
|
|
7359
|
-
this._events.
|
|
7476
|
+
this._events.unshift(event);
|
|
7360
7477
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
7361
|
-
if (this._events.length > MAX_EVENTS$
|
|
7362
|
-
this._events.
|
|
7478
|
+
if (this._events.length > MAX_EVENTS$3) {
|
|
7479
|
+
this._events.pop();
|
|
7363
7480
|
}
|
|
7364
7481
|
}
|
|
7365
7482
|
/**
|
|
@@ -7825,7 +7942,7 @@ const pnlColumns = [
|
|
|
7825
7942
|
* Storage class for accumulating walker results.
|
|
7826
7943
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
7827
7944
|
*/
|
|
7828
|
-
let ReportStorage$
|
|
7945
|
+
let ReportStorage$2 = class ReportStorage {
|
|
7829
7946
|
constructor(walkerName) {
|
|
7830
7947
|
this.walkerName = walkerName;
|
|
7831
7948
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -7843,17 +7960,13 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
7843
7960
|
* @param data - Walker contract with strategy result
|
|
7844
7961
|
*/
|
|
7845
7962
|
addResult(data) {
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
this._totalStrategies = data.totalStrategies;
|
|
7850
|
-
}
|
|
7851
|
-
// Update best stats only if this strategy is the current best
|
|
7963
|
+
this._totalStrategies = data.totalStrategies;
|
|
7964
|
+
this._bestMetric = data.bestMetric;
|
|
7965
|
+
this._bestStrategy = data.bestStrategy;
|
|
7852
7966
|
if (data.strategyName === data.bestStrategy) {
|
|
7853
7967
|
this._bestStats = data.stats;
|
|
7854
7968
|
}
|
|
7855
|
-
|
|
7856
|
-
this._strategyResults.push({
|
|
7969
|
+
this._strategyResults.unshift({
|
|
7857
7970
|
strategyName: data.strategyName,
|
|
7858
7971
|
stats: data.stats,
|
|
7859
7972
|
metricValue: data.metricValue,
|
|
@@ -8037,7 +8150,7 @@ class WalkerMarkdownService {
|
|
|
8037
8150
|
* Memoized function to get or create ReportStorage for a walker.
|
|
8038
8151
|
* Each walker gets its own isolated storage instance.
|
|
8039
8152
|
*/
|
|
8040
|
-
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$
|
|
8153
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$2(walkerName));
|
|
8041
8154
|
/**
|
|
8042
8155
|
* Processes walker progress events and accumulates strategy results.
|
|
8043
8156
|
* Should be called from walkerEmitter.
|
|
@@ -8208,7 +8321,7 @@ function isUnsafe(value) {
|
|
|
8208
8321
|
}
|
|
8209
8322
|
return false;
|
|
8210
8323
|
}
|
|
8211
|
-
const columns$
|
|
8324
|
+
const columns$2 = [
|
|
8212
8325
|
{
|
|
8213
8326
|
key: "symbol",
|
|
8214
8327
|
label: "Symbol",
|
|
@@ -8270,6 +8383,8 @@ const columns$1 = [
|
|
|
8270
8383
|
format: (data) => data.totalTrades.toString(),
|
|
8271
8384
|
},
|
|
8272
8385
|
];
|
|
8386
|
+
/** Maximum number of signals to store per symbol in heatmap reports */
|
|
8387
|
+
const MAX_EVENTS$2 = 250;
|
|
8273
8388
|
/**
|
|
8274
8389
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
8275
8390
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -8289,7 +8404,12 @@ class HeatmapStorage {
|
|
|
8289
8404
|
if (!this.symbolData.has(symbol)) {
|
|
8290
8405
|
this.symbolData.set(symbol, []);
|
|
8291
8406
|
}
|
|
8292
|
-
this.symbolData.get(symbol)
|
|
8407
|
+
const signals = this.symbolData.get(symbol);
|
|
8408
|
+
signals.unshift(data);
|
|
8409
|
+
// Trim queue if exceeded MAX_EVENTS per symbol
|
|
8410
|
+
if (signals.length > MAX_EVENTS$2) {
|
|
8411
|
+
signals.pop();
|
|
8412
|
+
}
|
|
8293
8413
|
}
|
|
8294
8414
|
/**
|
|
8295
8415
|
* Calculates statistics for a single symbol.
|
|
@@ -8508,9 +8628,9 @@ class HeatmapStorage {
|
|
|
8508
8628
|
"*No data available*"
|
|
8509
8629
|
].join("\n");
|
|
8510
8630
|
}
|
|
8511
|
-
const header = columns$
|
|
8512
|
-
const separator = columns$
|
|
8513
|
-
const rows = data.symbols.map((row) => columns$
|
|
8631
|
+
const header = columns$2.map((col) => col.label);
|
|
8632
|
+
const separator = columns$2.map(() => "---");
|
|
8633
|
+
const rows = data.symbols.map((row) => columns$2.map((col) => col.format(row)));
|
|
8514
8634
|
const tableData = [header, separator, ...rows];
|
|
8515
8635
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
8516
8636
|
return [
|
|
@@ -10548,7 +10668,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
|
|
|
10548
10668
|
revenuePercent,
|
|
10549
10669
|
backtest,
|
|
10550
10670
|
});
|
|
10551
|
-
await self.params.onProfit(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
10671
|
+
await self.params.onProfit(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
|
|
10552
10672
|
}
|
|
10553
10673
|
}
|
|
10554
10674
|
if (shouldPersist) {
|
|
@@ -10595,7 +10715,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
|
|
|
10595
10715
|
lossPercent,
|
|
10596
10716
|
backtest,
|
|
10597
10717
|
});
|
|
10598
|
-
await self.params.onLoss(symbol, data, currentPrice, level, backtest, when.getTime());
|
|
10718
|
+
await self.params.onLoss(symbol, data.strategyName, data.exchangeName, data, currentPrice, level, backtest, when.getTime());
|
|
10599
10719
|
}
|
|
10600
10720
|
}
|
|
10601
10721
|
if (shouldPersist) {
|
|
@@ -10866,6 +10986,7 @@ class ClientPartial {
|
|
|
10866
10986
|
symbol,
|
|
10867
10987
|
data,
|
|
10868
10988
|
priceClose,
|
|
10989
|
+
backtest,
|
|
10869
10990
|
});
|
|
10870
10991
|
if (this._states === NEED_FETCH) {
|
|
10871
10992
|
throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
|
|
@@ -10882,14 +11003,18 @@ class ClientPartial {
|
|
|
10882
11003
|
* Emits PartialProfitContract event to all subscribers.
|
|
10883
11004
|
*
|
|
10884
11005
|
* @param symbol - Trading pair symbol
|
|
11006
|
+
* @param strategyName - Strategy name that generated this signal
|
|
11007
|
+
* @param exchangeName - Exchange name where this signal is being executed
|
|
10885
11008
|
* @param data - Signal row data
|
|
10886
11009
|
* @param currentPrice - Current market price
|
|
10887
11010
|
* @param level - Profit level reached
|
|
10888
11011
|
* @param backtest - True if backtest mode
|
|
10889
11012
|
* @param timestamp - Event timestamp in milliseconds
|
|
10890
11013
|
*/
|
|
10891
|
-
const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
11014
|
+
const COMMIT_PROFIT_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialProfitSubject.next({
|
|
10892
11015
|
symbol,
|
|
11016
|
+
strategyName,
|
|
11017
|
+
exchangeName,
|
|
10893
11018
|
data,
|
|
10894
11019
|
currentPrice,
|
|
10895
11020
|
level,
|
|
@@ -10903,14 +11028,18 @@ const COMMIT_PROFIT_FN = async (symbol, data, currentPrice, level, backtest, tim
|
|
|
10903
11028
|
* Emits PartialLossContract event to all subscribers.
|
|
10904
11029
|
*
|
|
10905
11030
|
* @param symbol - Trading pair symbol
|
|
11031
|
+
* @param strategyName - Strategy name that generated this signal
|
|
11032
|
+
* @param exchangeName - Exchange name where this signal is being executed
|
|
10906
11033
|
* @param data - Signal row data
|
|
10907
11034
|
* @param currentPrice - Current market price
|
|
10908
11035
|
* @param level - Loss level reached
|
|
10909
11036
|
* @param backtest - True if backtest mode
|
|
10910
11037
|
* @param timestamp - Event timestamp in milliseconds
|
|
10911
11038
|
*/
|
|
10912
|
-
const COMMIT_LOSS_FN = async (symbol, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
11039
|
+
const COMMIT_LOSS_FN = async (symbol, strategyName, exchangeName, data, currentPrice, level, backtest, timestamp) => await partialLossSubject.next({
|
|
10913
11040
|
symbol,
|
|
11041
|
+
strategyName,
|
|
11042
|
+
exchangeName,
|
|
10914
11043
|
data,
|
|
10915
11044
|
currentPrice,
|
|
10916
11045
|
level,
|
|
@@ -11056,7 +11185,7 @@ class PartialConnectionService {
|
|
|
11056
11185
|
}
|
|
11057
11186
|
}
|
|
11058
11187
|
|
|
11059
|
-
const columns = [
|
|
11188
|
+
const columns$1 = [
|
|
11060
11189
|
{
|
|
11061
11190
|
key: "action",
|
|
11062
11191
|
label: "Action",
|
|
@@ -11104,12 +11233,12 @@ const columns = [
|
|
|
11104
11233
|
},
|
|
11105
11234
|
];
|
|
11106
11235
|
/** Maximum number of events to store in partial reports */
|
|
11107
|
-
const MAX_EVENTS = 250;
|
|
11236
|
+
const MAX_EVENTS$1 = 250;
|
|
11108
11237
|
/**
|
|
11109
11238
|
* Storage class for accumulating partial profit/loss events per symbol-strategy pair.
|
|
11110
11239
|
* Maintains a chronological list of profit and loss level events.
|
|
11111
11240
|
*/
|
|
11112
|
-
class ReportStorage {
|
|
11241
|
+
let ReportStorage$1 = class ReportStorage {
|
|
11113
11242
|
constructor() {
|
|
11114
11243
|
/** Internal list of all partial events for this symbol */
|
|
11115
11244
|
this._eventList = [];
|
|
@@ -11123,7 +11252,7 @@ class ReportStorage {
|
|
|
11123
11252
|
* @param backtest - True if backtest mode
|
|
11124
11253
|
*/
|
|
11125
11254
|
addProfitEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11126
|
-
this._eventList.
|
|
11255
|
+
this._eventList.unshift({
|
|
11127
11256
|
timestamp,
|
|
11128
11257
|
action: "profit",
|
|
11129
11258
|
symbol: data.symbol,
|
|
@@ -11135,8 +11264,8 @@ class ReportStorage {
|
|
|
11135
11264
|
backtest,
|
|
11136
11265
|
});
|
|
11137
11266
|
// Trim queue if exceeded MAX_EVENTS
|
|
11138
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11139
|
-
this._eventList.
|
|
11267
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11268
|
+
this._eventList.pop();
|
|
11140
11269
|
}
|
|
11141
11270
|
}
|
|
11142
11271
|
/**
|
|
@@ -11148,7 +11277,7 @@ class ReportStorage {
|
|
|
11148
11277
|
* @param backtest - True if backtest mode
|
|
11149
11278
|
*/
|
|
11150
11279
|
addLossEvent(data, currentPrice, level, backtest, timestamp) {
|
|
11151
|
-
this._eventList.
|
|
11280
|
+
this._eventList.unshift({
|
|
11152
11281
|
timestamp,
|
|
11153
11282
|
action: "loss",
|
|
11154
11283
|
symbol: data.symbol,
|
|
@@ -11160,8 +11289,8 @@ class ReportStorage {
|
|
|
11160
11289
|
backtest,
|
|
11161
11290
|
});
|
|
11162
11291
|
// Trim queue if exceeded MAX_EVENTS
|
|
11163
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
11164
|
-
this._eventList.
|
|
11292
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
11293
|
+
this._eventList.pop();
|
|
11165
11294
|
}
|
|
11166
11295
|
}
|
|
11167
11296
|
/**
|
|
@@ -11203,9 +11332,9 @@ class ReportStorage {
|
|
|
11203
11332
|
"No partial profit/loss events recorded yet."
|
|
11204
11333
|
].join("\n");
|
|
11205
11334
|
}
|
|
11206
|
-
const header = columns.map((col) => col.label);
|
|
11207
|
-
const separator = columns.map(() => "---");
|
|
11208
|
-
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
11335
|
+
const header = columns$1.map((col) => col.label);
|
|
11336
|
+
const separator = columns$1.map(() => "---");
|
|
11337
|
+
const rows = this._eventList.map((event) => columns$1.map((col) => col.format(event)));
|
|
11209
11338
|
const tableData = [header, separator, ...rows];
|
|
11210
11339
|
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
11211
11340
|
return [
|
|
@@ -11239,7 +11368,7 @@ class ReportStorage {
|
|
|
11239
11368
|
console.error(`Failed to save markdown report:`, error);
|
|
11240
11369
|
}
|
|
11241
11370
|
}
|
|
11242
|
-
}
|
|
11371
|
+
};
|
|
11243
11372
|
/**
|
|
11244
11373
|
* Service for generating and saving partial profit/loss markdown reports.
|
|
11245
11374
|
*
|
|
@@ -11269,7 +11398,7 @@ class PartialMarkdownService {
|
|
|
11269
11398
|
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
11270
11399
|
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
11271
11400
|
*/
|
|
11272
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
|
|
11401
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage$1());
|
|
11273
11402
|
/**
|
|
11274
11403
|
* Processes profit events and accumulates them.
|
|
11275
11404
|
* Should be called from partialProfitSubject subscription.
|
|
@@ -11579,7 +11708,7 @@ class PartialGlobalService {
|
|
|
11579
11708
|
* Warning threshold for message size in kilobytes.
|
|
11580
11709
|
* Messages exceeding this size trigger console warnings.
|
|
11581
11710
|
*/
|
|
11582
|
-
const WARN_KB =
|
|
11711
|
+
const WARN_KB = 20;
|
|
11583
11712
|
/**
|
|
11584
11713
|
* Internal function for dumping signal data to markdown files.
|
|
11585
11714
|
* Creates a directory structure with system prompts, user messages, and LLM output.
|
|
@@ -11841,35 +11970,382 @@ class ConfigValidationService {
|
|
|
11841
11970
|
}
|
|
11842
11971
|
}
|
|
11843
11972
|
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
}
|
|
11860
|
-
{
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11973
|
+
const columns = [
|
|
11974
|
+
{
|
|
11975
|
+
key: "symbol",
|
|
11976
|
+
label: "Symbol",
|
|
11977
|
+
format: (data) => data.symbol,
|
|
11978
|
+
},
|
|
11979
|
+
{
|
|
11980
|
+
key: "strategyName",
|
|
11981
|
+
label: "Strategy",
|
|
11982
|
+
format: (data) => data.strategyName,
|
|
11983
|
+
},
|
|
11984
|
+
{
|
|
11985
|
+
key: "signalId",
|
|
11986
|
+
label: "Signal ID",
|
|
11987
|
+
format: (data) => data.pendingSignal.id || "N/A",
|
|
11988
|
+
},
|
|
11989
|
+
{
|
|
11990
|
+
key: "position",
|
|
11991
|
+
label: "Position",
|
|
11992
|
+
format: (data) => data.pendingSignal.position.toUpperCase(),
|
|
11993
|
+
},
|
|
11994
|
+
{
|
|
11995
|
+
key: "exchangeName",
|
|
11996
|
+
label: "Exchange",
|
|
11997
|
+
format: (data) => data.exchangeName,
|
|
11998
|
+
},
|
|
11999
|
+
{
|
|
12000
|
+
key: "openPrice",
|
|
12001
|
+
label: "Open Price",
|
|
12002
|
+
format: (data) => data.pendingSignal.priceOpen !== undefined
|
|
12003
|
+
? `${data.pendingSignal.priceOpen.toFixed(8)} USD`
|
|
12004
|
+
: "N/A",
|
|
12005
|
+
},
|
|
12006
|
+
{
|
|
12007
|
+
key: "takeProfit",
|
|
12008
|
+
label: "Take Profit",
|
|
12009
|
+
format: (data) => data.pendingSignal.priceTakeProfit !== undefined
|
|
12010
|
+
? `${data.pendingSignal.priceTakeProfit.toFixed(8)} USD`
|
|
12011
|
+
: "N/A",
|
|
12012
|
+
},
|
|
12013
|
+
{
|
|
12014
|
+
key: "stopLoss",
|
|
12015
|
+
label: "Stop Loss",
|
|
12016
|
+
format: (data) => data.pendingSignal.priceStopLoss !== undefined
|
|
12017
|
+
? `${data.pendingSignal.priceStopLoss.toFixed(8)} USD`
|
|
12018
|
+
: "N/A",
|
|
12019
|
+
},
|
|
12020
|
+
{
|
|
12021
|
+
key: "currentPrice",
|
|
12022
|
+
label: "Current Price",
|
|
12023
|
+
format: (data) => `${data.currentPrice.toFixed(8)} USD`,
|
|
12024
|
+
},
|
|
12025
|
+
{
|
|
12026
|
+
key: "activePositionCount",
|
|
12027
|
+
label: "Active Positions",
|
|
12028
|
+
format: (data) => data.activePositionCount.toString(),
|
|
12029
|
+
},
|
|
12030
|
+
{
|
|
12031
|
+
key: "comment",
|
|
12032
|
+
label: "Reason",
|
|
12033
|
+
format: (data) => data.comment,
|
|
12034
|
+
},
|
|
12035
|
+
{
|
|
12036
|
+
key: "timestamp",
|
|
12037
|
+
label: "Timestamp",
|
|
12038
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
12039
|
+
},
|
|
12040
|
+
];
|
|
12041
|
+
/** Maximum number of events to store in risk reports */
|
|
12042
|
+
const MAX_EVENTS = 250;
|
|
12043
|
+
/**
|
|
12044
|
+
* Storage class for accumulating risk rejection events per symbol-strategy pair.
|
|
12045
|
+
* Maintains a chronological list of rejected signals due to risk limits.
|
|
12046
|
+
*/
|
|
12047
|
+
class ReportStorage {
|
|
12048
|
+
constructor() {
|
|
12049
|
+
/** Internal list of all risk rejection events for this symbol */
|
|
12050
|
+
this._eventList = [];
|
|
12051
|
+
}
|
|
12052
|
+
/**
|
|
12053
|
+
* Adds a risk rejection event to the storage.
|
|
12054
|
+
*
|
|
12055
|
+
* @param event - Risk rejection event data
|
|
12056
|
+
*/
|
|
12057
|
+
addRejectionEvent(event) {
|
|
12058
|
+
this._eventList.unshift(event);
|
|
12059
|
+
// Trim queue if exceeded MAX_EVENTS
|
|
12060
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
12061
|
+
this._eventList.pop();
|
|
12062
|
+
}
|
|
12063
|
+
}
|
|
12064
|
+
/**
|
|
12065
|
+
* Calculates statistical data from risk rejection events (Controller).
|
|
12066
|
+
*
|
|
12067
|
+
* @returns Statistical data (empty object if no events)
|
|
12068
|
+
*/
|
|
12069
|
+
async getData() {
|
|
12070
|
+
if (this._eventList.length === 0) {
|
|
12071
|
+
return {
|
|
12072
|
+
eventList: [],
|
|
12073
|
+
totalRejections: 0,
|
|
12074
|
+
bySymbol: {},
|
|
12075
|
+
byStrategy: {},
|
|
12076
|
+
};
|
|
12077
|
+
}
|
|
12078
|
+
const bySymbol = {};
|
|
12079
|
+
const byStrategy = {};
|
|
12080
|
+
for (const event of this._eventList) {
|
|
12081
|
+
bySymbol[event.symbol] = (bySymbol[event.symbol] || 0) + 1;
|
|
12082
|
+
byStrategy[event.strategyName] = (byStrategy[event.strategyName] || 0) + 1;
|
|
12083
|
+
}
|
|
12084
|
+
return {
|
|
12085
|
+
eventList: this._eventList,
|
|
12086
|
+
totalRejections: this._eventList.length,
|
|
12087
|
+
bySymbol,
|
|
12088
|
+
byStrategy,
|
|
12089
|
+
};
|
|
12090
|
+
}
|
|
12091
|
+
/**
|
|
12092
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair (View).
|
|
12093
|
+
*
|
|
12094
|
+
* @param symbol - Trading pair symbol
|
|
12095
|
+
* @param strategyName - Strategy name
|
|
12096
|
+
* @returns Markdown formatted report with all events
|
|
12097
|
+
*/
|
|
12098
|
+
async getReport(symbol, strategyName) {
|
|
12099
|
+
const stats = await this.getData();
|
|
12100
|
+
if (stats.totalRejections === 0) {
|
|
12101
|
+
return [
|
|
12102
|
+
`# Risk Rejection Report: ${symbol}:${strategyName}`,
|
|
12103
|
+
"",
|
|
12104
|
+
"No risk rejections recorded yet.",
|
|
12105
|
+
].join("\n");
|
|
12106
|
+
}
|
|
12107
|
+
const header = columns.map((col) => col.label);
|
|
12108
|
+
const separator = columns.map(() => "---");
|
|
12109
|
+
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
12110
|
+
const tableData = [header, separator, ...rows];
|
|
12111
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
12112
|
+
return [
|
|
12113
|
+
`# Risk Rejection Report: ${symbol}:${strategyName}`,
|
|
12114
|
+
"",
|
|
12115
|
+
table,
|
|
12116
|
+
"",
|
|
12117
|
+
`**Total rejections:** ${stats.totalRejections}`,
|
|
12118
|
+
"",
|
|
12119
|
+
"## Rejections by Symbol",
|
|
12120
|
+
...Object.entries(stats.bySymbol).map(([sym, count]) => `- ${sym}: ${count}`),
|
|
12121
|
+
"",
|
|
12122
|
+
"## Rejections by Strategy",
|
|
12123
|
+
...Object.entries(stats.byStrategy).map(([strat, count]) => `- ${strat}: ${count}`),
|
|
12124
|
+
].join("\n");
|
|
12125
|
+
}
|
|
12126
|
+
/**
|
|
12127
|
+
* Saves symbol-strategy report to disk.
|
|
12128
|
+
*
|
|
12129
|
+
* @param symbol - Trading pair symbol
|
|
12130
|
+
* @param strategyName - Strategy name
|
|
12131
|
+
* @param path - Directory path to save report (default: "./dump/risk")
|
|
12132
|
+
*/
|
|
12133
|
+
async dump(symbol, strategyName, path$1 = "./dump/risk") {
|
|
12134
|
+
const markdown = await this.getReport(symbol, strategyName);
|
|
12135
|
+
try {
|
|
12136
|
+
const dir = path.join(process.cwd(), path$1);
|
|
12137
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12138
|
+
const filename = `${symbol}_${strategyName}.md`;
|
|
12139
|
+
const filepath = path.join(dir, filename);
|
|
12140
|
+
await fs.writeFile(filepath, markdown, "utf-8");
|
|
12141
|
+
console.log(`Risk rejection report saved: ${filepath}`);
|
|
12142
|
+
}
|
|
12143
|
+
catch (error) {
|
|
12144
|
+
console.error(`Failed to save markdown report:`, error);
|
|
12145
|
+
}
|
|
12146
|
+
}
|
|
11868
12147
|
}
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
12148
|
+
/**
|
|
12149
|
+
* Service for generating and saving risk rejection markdown reports.
|
|
12150
|
+
*
|
|
12151
|
+
* Features:
|
|
12152
|
+
* - Listens to risk rejection events via riskSubject
|
|
12153
|
+
* - Accumulates all rejection events per symbol-strategy pair
|
|
12154
|
+
* - Generates markdown tables with detailed rejection information
|
|
12155
|
+
* - Provides statistics (total rejections, by symbol, by strategy)
|
|
12156
|
+
* - Saves reports to disk in dump/risk/{symbol}_{strategyName}.md
|
|
12157
|
+
*
|
|
12158
|
+
* @example
|
|
12159
|
+
* ```typescript
|
|
12160
|
+
* const service = new RiskMarkdownService();
|
|
12161
|
+
*
|
|
12162
|
+
* // Service automatically subscribes to subjects on init
|
|
12163
|
+
* // No manual callback setup needed
|
|
12164
|
+
*
|
|
12165
|
+
* // Later: generate and save report
|
|
12166
|
+
* await service.dump("BTCUSDT", "my-strategy");
|
|
12167
|
+
* ```
|
|
12168
|
+
*/
|
|
12169
|
+
class RiskMarkdownService {
|
|
12170
|
+
constructor() {
|
|
12171
|
+
/** Logger service for debug output */
|
|
12172
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
12173
|
+
/**
|
|
12174
|
+
* Memoized function to get or create ReportStorage for a symbol-strategy pair.
|
|
12175
|
+
* Each symbol-strategy combination gets its own isolated storage instance.
|
|
12176
|
+
*/
|
|
12177
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName]) => `${symbol}:${strategyName}`, () => new ReportStorage());
|
|
12178
|
+
/**
|
|
12179
|
+
* Processes risk rejection events and accumulates them.
|
|
12180
|
+
* Should be called from riskSubject subscription.
|
|
12181
|
+
*
|
|
12182
|
+
* @param data - Risk rejection event data
|
|
12183
|
+
*
|
|
12184
|
+
* @example
|
|
12185
|
+
* ```typescript
|
|
12186
|
+
* const service = new RiskMarkdownService();
|
|
12187
|
+
* // Service automatically subscribes in init()
|
|
12188
|
+
* ```
|
|
12189
|
+
*/
|
|
12190
|
+
this.tickRejection = async (data) => {
|
|
12191
|
+
this.loggerService.log("riskMarkdownService tickRejection", {
|
|
12192
|
+
data,
|
|
12193
|
+
});
|
|
12194
|
+
const storage = this.getStorage(data.symbol, data.strategyName);
|
|
12195
|
+
storage.addRejectionEvent(data);
|
|
12196
|
+
};
|
|
12197
|
+
/**
|
|
12198
|
+
* Gets statistical data from all risk rejection events for a symbol-strategy pair.
|
|
12199
|
+
* Delegates to ReportStorage.getData().
|
|
12200
|
+
*
|
|
12201
|
+
* @param symbol - Trading pair symbol to get data for
|
|
12202
|
+
* @param strategyName - Strategy name to get data for
|
|
12203
|
+
* @returns Statistical data object with all metrics
|
|
12204
|
+
*
|
|
12205
|
+
* @example
|
|
12206
|
+
* ```typescript
|
|
12207
|
+
* const service = new RiskMarkdownService();
|
|
12208
|
+
* const stats = await service.getData("BTCUSDT", "my-strategy");
|
|
12209
|
+
* console.log(stats.totalRejections, stats.bySymbol);
|
|
12210
|
+
* ```
|
|
12211
|
+
*/
|
|
12212
|
+
this.getData = async (symbol, strategyName) => {
|
|
12213
|
+
this.loggerService.log("riskMarkdownService getData", {
|
|
12214
|
+
symbol,
|
|
12215
|
+
strategyName,
|
|
12216
|
+
});
|
|
12217
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12218
|
+
return storage.getData();
|
|
12219
|
+
};
|
|
12220
|
+
/**
|
|
12221
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair.
|
|
12222
|
+
* Delegates to ReportStorage.getReport().
|
|
12223
|
+
*
|
|
12224
|
+
* @param symbol - Trading pair symbol to generate report for
|
|
12225
|
+
* @param strategyName - Strategy name to generate report for
|
|
12226
|
+
* @returns Markdown formatted report string with table of all events
|
|
12227
|
+
*
|
|
12228
|
+
* @example
|
|
12229
|
+
* ```typescript
|
|
12230
|
+
* const service = new RiskMarkdownService();
|
|
12231
|
+
* const markdown = await service.getReport("BTCUSDT", "my-strategy");
|
|
12232
|
+
* console.log(markdown);
|
|
12233
|
+
* ```
|
|
12234
|
+
*/
|
|
12235
|
+
this.getReport = async (symbol, strategyName) => {
|
|
12236
|
+
this.loggerService.log("riskMarkdownService getReport", {
|
|
12237
|
+
symbol,
|
|
12238
|
+
strategyName,
|
|
12239
|
+
});
|
|
12240
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12241
|
+
return storage.getReport(symbol, strategyName);
|
|
12242
|
+
};
|
|
12243
|
+
/**
|
|
12244
|
+
* Saves symbol-strategy report to disk.
|
|
12245
|
+
* Creates directory if it doesn't exist.
|
|
12246
|
+
* Delegates to ReportStorage.dump().
|
|
12247
|
+
*
|
|
12248
|
+
* @param symbol - Trading pair symbol to save report for
|
|
12249
|
+
* @param strategyName - Strategy name to save report for
|
|
12250
|
+
* @param path - Directory path to save report (default: "./dump/risk")
|
|
12251
|
+
*
|
|
12252
|
+
* @example
|
|
12253
|
+
* ```typescript
|
|
12254
|
+
* const service = new RiskMarkdownService();
|
|
12255
|
+
*
|
|
12256
|
+
* // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
|
|
12257
|
+
* await service.dump("BTCUSDT", "my-strategy");
|
|
12258
|
+
*
|
|
12259
|
+
* // Save to custom path: ./custom/path/BTCUSDT_my-strategy.md
|
|
12260
|
+
* await service.dump("BTCUSDT", "my-strategy", "./custom/path");
|
|
12261
|
+
* ```
|
|
12262
|
+
*/
|
|
12263
|
+
this.dump = async (symbol, strategyName, path = "./dump/risk") => {
|
|
12264
|
+
this.loggerService.log("riskMarkdownService dump", {
|
|
12265
|
+
symbol,
|
|
12266
|
+
strategyName,
|
|
12267
|
+
path,
|
|
12268
|
+
});
|
|
12269
|
+
const storage = this.getStorage(symbol, strategyName);
|
|
12270
|
+
await storage.dump(symbol, strategyName, path);
|
|
12271
|
+
};
|
|
12272
|
+
/**
|
|
12273
|
+
* Clears accumulated event data from storage.
|
|
12274
|
+
* If ctx is provided, clears only that specific symbol-strategy pair's data.
|
|
12275
|
+
* If nothing is provided, clears all data.
|
|
12276
|
+
*
|
|
12277
|
+
* @param ctx - Optional context with symbol and strategyName
|
|
12278
|
+
*
|
|
12279
|
+
* @example
|
|
12280
|
+
* ```typescript
|
|
12281
|
+
* const service = new RiskMarkdownService();
|
|
12282
|
+
*
|
|
12283
|
+
* // Clear specific symbol-strategy pair
|
|
12284
|
+
* await service.clear({ symbol: "BTCUSDT", strategyName: "my-strategy" });
|
|
12285
|
+
*
|
|
12286
|
+
* // Clear all data
|
|
12287
|
+
* await service.clear();
|
|
12288
|
+
* ```
|
|
12289
|
+
*/
|
|
12290
|
+
this.clear = async (ctx) => {
|
|
12291
|
+
this.loggerService.log("riskMarkdownService clear", {
|
|
12292
|
+
ctx,
|
|
12293
|
+
});
|
|
12294
|
+
if (ctx) {
|
|
12295
|
+
const key = `${ctx.symbol}:${ctx.strategyName}`;
|
|
12296
|
+
this.getStorage.clear(key);
|
|
12297
|
+
}
|
|
12298
|
+
else {
|
|
12299
|
+
this.getStorage.clear();
|
|
12300
|
+
}
|
|
12301
|
+
};
|
|
12302
|
+
/**
|
|
12303
|
+
* Initializes the service by subscribing to risk rejection events.
|
|
12304
|
+
* Uses singleshot to ensure initialization happens only once.
|
|
12305
|
+
* Automatically called on first use.
|
|
12306
|
+
*
|
|
12307
|
+
* @example
|
|
12308
|
+
* ```typescript
|
|
12309
|
+
* const service = new RiskMarkdownService();
|
|
12310
|
+
* await service.init(); // Subscribe to rejection events
|
|
12311
|
+
* ```
|
|
12312
|
+
*/
|
|
12313
|
+
this.init = functoolsKit.singleshot(async () => {
|
|
12314
|
+
this.loggerService.log("riskMarkdownService init");
|
|
12315
|
+
riskSubject.subscribe(this.tickRejection);
|
|
12316
|
+
});
|
|
12317
|
+
}
|
|
12318
|
+
}
|
|
12319
|
+
|
|
12320
|
+
{
|
|
12321
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
12322
|
+
}
|
|
12323
|
+
{
|
|
12324
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
12325
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
12326
|
+
}
|
|
12327
|
+
{
|
|
12328
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
12329
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
12330
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
12331
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
12332
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
12333
|
+
provide(TYPES.optimizerConnectionService, () => new OptimizerConnectionService());
|
|
12334
|
+
provide(TYPES.partialConnectionService, () => new PartialConnectionService());
|
|
12335
|
+
}
|
|
12336
|
+
{
|
|
12337
|
+
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
12338
|
+
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
|
|
12339
|
+
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
|
|
12340
|
+
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
|
|
12341
|
+
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
|
|
12342
|
+
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
|
|
12343
|
+
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
|
|
12344
|
+
}
|
|
12345
|
+
{
|
|
12346
|
+
provide(TYPES.exchangeCoreService, () => new ExchangeCoreService());
|
|
12347
|
+
provide(TYPES.strategyCoreService, () => new StrategyCoreService());
|
|
12348
|
+
provide(TYPES.frameCoreService, () => new FrameCoreService());
|
|
11873
12349
|
}
|
|
11874
12350
|
{
|
|
11875
12351
|
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
@@ -11901,6 +12377,7 @@ class ConfigValidationService {
|
|
|
11901
12377
|
provide(TYPES.heatMarkdownService, () => new HeatMarkdownService());
|
|
11902
12378
|
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
11903
12379
|
provide(TYPES.outlineMarkdownService, () => new OutlineMarkdownService());
|
|
12380
|
+
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
11904
12381
|
}
|
|
11905
12382
|
{
|
|
11906
12383
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -11976,6 +12453,7 @@ const markdownServices = {
|
|
|
11976
12453
|
heatMarkdownService: inject(TYPES.heatMarkdownService),
|
|
11977
12454
|
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
11978
12455
|
outlineMarkdownService: inject(TYPES.outlineMarkdownService),
|
|
12456
|
+
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
11979
12457
|
};
|
|
11980
12458
|
const validationServices = {
|
|
11981
12459
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -12750,6 +13228,8 @@ const LISTEN_PARTIAL_PROFIT_METHOD_NAME = "event.listenPartialProfit";
|
|
|
12750
13228
|
const LISTEN_PARTIAL_PROFIT_ONCE_METHOD_NAME = "event.listenPartialProfitOnce";
|
|
12751
13229
|
const LISTEN_PARTIAL_LOSS_METHOD_NAME = "event.listenPartialLoss";
|
|
12752
13230
|
const LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME = "event.listenPartialLossOnce";
|
|
13231
|
+
const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
|
|
13232
|
+
const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
|
|
12753
13233
|
/**
|
|
12754
13234
|
* Subscribes to all signal events with queued async processing.
|
|
12755
13235
|
*
|
|
@@ -13548,6 +14028,75 @@ function listenPartialLossOnce(filterFn, fn) {
|
|
|
13548
14028
|
backtest$1.loggerService.log(LISTEN_PARTIAL_LOSS_ONCE_METHOD_NAME);
|
|
13549
14029
|
return partialLossSubject.filter(filterFn).once(fn);
|
|
13550
14030
|
}
|
|
14031
|
+
/**
|
|
14032
|
+
* Subscribes to risk rejection events with queued async processing.
|
|
14033
|
+
*
|
|
14034
|
+
* Emits ONLY when a signal is rejected due to risk validation failure.
|
|
14035
|
+
* Does not emit for allowed signals (prevents spam).
|
|
14036
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
14037
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
14038
|
+
*
|
|
14039
|
+
* @param fn - Callback function to handle risk rejection events
|
|
14040
|
+
* @returns Unsubscribe function to stop listening to events
|
|
14041
|
+
*
|
|
14042
|
+
* @example
|
|
14043
|
+
* ```typescript
|
|
14044
|
+
* import { listenRisk } from "./function/event";
|
|
14045
|
+
*
|
|
14046
|
+
* const unsubscribe = listenRisk((event) => {
|
|
14047
|
+
* console.log(`[RISK REJECTED] Signal for ${event.symbol}`);
|
|
14048
|
+
* console.log(`Strategy: ${event.strategyName}`);
|
|
14049
|
+
* console.log(`Position: ${event.pendingSignal.position}`);
|
|
14050
|
+
* console.log(`Active positions: ${event.activePositionCount}`);
|
|
14051
|
+
* console.log(`Reason: ${event.comment}`);
|
|
14052
|
+
* console.log(`Price: ${event.currentPrice}`);
|
|
14053
|
+
* });
|
|
14054
|
+
*
|
|
14055
|
+
* // Later: stop listening
|
|
14056
|
+
* unsubscribe();
|
|
14057
|
+
* ```
|
|
14058
|
+
*/
|
|
14059
|
+
function listenRisk(fn) {
|
|
14060
|
+
backtest$1.loggerService.log(LISTEN_RISK_METHOD_NAME);
|
|
14061
|
+
return riskSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
14062
|
+
}
|
|
14063
|
+
/**
|
|
14064
|
+
* Subscribes to filtered risk rejection events with one-time execution.
|
|
14065
|
+
*
|
|
14066
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
14067
|
+
* and automatically unsubscribes. Useful for waiting for specific risk rejection conditions.
|
|
14068
|
+
*
|
|
14069
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
14070
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
14071
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
14072
|
+
*
|
|
14073
|
+
* @example
|
|
14074
|
+
* ```typescript
|
|
14075
|
+
* import { listenRiskOnce } from "./function/event";
|
|
14076
|
+
*
|
|
14077
|
+
* // Wait for first risk rejection on BTCUSDT
|
|
14078
|
+
* listenRiskOnce(
|
|
14079
|
+
* (event) => event.symbol === "BTCUSDT",
|
|
14080
|
+
* (event) => {
|
|
14081
|
+
* console.log("BTCUSDT signal rejected!");
|
|
14082
|
+
* console.log("Reason:", event.comment);
|
|
14083
|
+
* }
|
|
14084
|
+
* );
|
|
14085
|
+
*
|
|
14086
|
+
* // Wait for rejection due to position limit
|
|
14087
|
+
* const cancel = listenRiskOnce(
|
|
14088
|
+
* (event) => event.comment.includes("Max") && event.activePositionCount >= 3,
|
|
14089
|
+
* (event) => console.log("Position limit reached:", event.activePositionCount)
|
|
14090
|
+
* );
|
|
14091
|
+
*
|
|
14092
|
+
* // Cancel if needed before event fires
|
|
14093
|
+
* cancel();
|
|
14094
|
+
* ```
|
|
14095
|
+
*/
|
|
14096
|
+
function listenRiskOnce(filterFn, fn) {
|
|
14097
|
+
backtest$1.loggerService.log(LISTEN_RISK_ONCE_METHOD_NAME);
|
|
14098
|
+
return riskSubject.filter(filterFn).once(fn);
|
|
14099
|
+
}
|
|
13551
14100
|
|
|
13552
14101
|
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
13553
14102
|
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
@@ -16172,6 +16721,186 @@ class ConstantUtils {
|
|
|
16172
16721
|
*/
|
|
16173
16722
|
const Constant = new ConstantUtils();
|
|
16174
16723
|
|
|
16724
|
+
const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
|
|
16725
|
+
const RISK_METHOD_NAME_GET_REPORT = "RiskUtils.getReport";
|
|
16726
|
+
const RISK_METHOD_NAME_DUMP = "RiskUtils.dump";
|
|
16727
|
+
/**
|
|
16728
|
+
* Utility class for accessing risk rejection reports and statistics.
|
|
16729
|
+
*
|
|
16730
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
16731
|
+
* accumulated by RiskMarkdownService from risk rejection events.
|
|
16732
|
+
*
|
|
16733
|
+
* Features:
|
|
16734
|
+
* - Statistical data extraction (total rejections count, by symbol, by strategy)
|
|
16735
|
+
* - Markdown report generation with event tables
|
|
16736
|
+
* - File export to disk
|
|
16737
|
+
*
|
|
16738
|
+
* Data source:
|
|
16739
|
+
* - RiskMarkdownService listens to riskSubject
|
|
16740
|
+
* - Accumulates rejection events in ReportStorage (max 250 events per symbol-strategy pair)
|
|
16741
|
+
* - Events include: timestamp, symbol, strategyName, position, exchangeName, price, activePositionCount, comment
|
|
16742
|
+
*
|
|
16743
|
+
* @example
|
|
16744
|
+
* ```typescript
|
|
16745
|
+
* import { Risk } from "./classes/Risk";
|
|
16746
|
+
*
|
|
16747
|
+
* // Get statistical data for BTCUSDT:my-strategy
|
|
16748
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16749
|
+
* console.log(`Total rejections: ${stats.totalRejections}`);
|
|
16750
|
+
* console.log(`By symbol:`, stats.bySymbol);
|
|
16751
|
+
* console.log(`By strategy:`, stats.byStrategy);
|
|
16752
|
+
*
|
|
16753
|
+
* // Generate markdown report
|
|
16754
|
+
* const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16755
|
+
* console.log(markdown); // Formatted table with all rejection events
|
|
16756
|
+
*
|
|
16757
|
+
* // Export report to file
|
|
16758
|
+
* await Risk.dump("BTCUSDT", "my-strategy"); // Saves to ./dump/risk/BTCUSDT_my-strategy.md
|
|
16759
|
+
* await Risk.dump("BTCUSDT", "my-strategy", "./custom/path"); // Custom directory
|
|
16760
|
+
* ```
|
|
16761
|
+
*/
|
|
16762
|
+
class RiskUtils {
|
|
16763
|
+
constructor() {
|
|
16764
|
+
/**
|
|
16765
|
+
* Retrieves statistical data from accumulated risk rejection events.
|
|
16766
|
+
*
|
|
16767
|
+
* Delegates to RiskMarkdownService.getData() which reads from ReportStorage.
|
|
16768
|
+
* Returns aggregated metrics calculated from all rejection events.
|
|
16769
|
+
*
|
|
16770
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16771
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16772
|
+
* @returns Promise resolving to RiskStatistics object with counts and event list
|
|
16773
|
+
*
|
|
16774
|
+
* @example
|
|
16775
|
+
* ```typescript
|
|
16776
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16777
|
+
*
|
|
16778
|
+
* console.log(`Total rejections: ${stats.totalRejections}`);
|
|
16779
|
+
* console.log(`Rejections by symbol:`, stats.bySymbol);
|
|
16780
|
+
* console.log(`Rejections by strategy:`, stats.byStrategy);
|
|
16781
|
+
*
|
|
16782
|
+
* // Iterate through all rejection events
|
|
16783
|
+
* for (const event of stats.eventList) {
|
|
16784
|
+
* console.log(`REJECTED: ${event.symbol} - ${event.comment} (${event.activePositionCount} active)`);
|
|
16785
|
+
* }
|
|
16786
|
+
* ```
|
|
16787
|
+
*/
|
|
16788
|
+
this.getData = async (symbol, strategyName) => {
|
|
16789
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_GET_DATA, { symbol, strategyName });
|
|
16790
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_DATA);
|
|
16791
|
+
{
|
|
16792
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16793
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_DATA);
|
|
16794
|
+
}
|
|
16795
|
+
return await backtest$1.riskMarkdownService.getData(symbol, strategyName);
|
|
16796
|
+
};
|
|
16797
|
+
/**
|
|
16798
|
+
* Generates markdown report with all risk rejection events for a symbol-strategy pair.
|
|
16799
|
+
*
|
|
16800
|
+
* Creates formatted table containing:
|
|
16801
|
+
* - Symbol
|
|
16802
|
+
* - Strategy
|
|
16803
|
+
* - Position (LONG/SHORT)
|
|
16804
|
+
* - Exchange
|
|
16805
|
+
* - Price
|
|
16806
|
+
* - Active Positions (at rejection time)
|
|
16807
|
+
* - Reason (from validation note)
|
|
16808
|
+
* - Timestamp (ISO 8601)
|
|
16809
|
+
*
|
|
16810
|
+
* Also includes summary statistics at the end (total rejections, by symbol, by strategy).
|
|
16811
|
+
*
|
|
16812
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16813
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16814
|
+
* @returns Promise resolving to markdown formatted report string
|
|
16815
|
+
*
|
|
16816
|
+
* @example
|
|
16817
|
+
* ```typescript
|
|
16818
|
+
* const markdown = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16819
|
+
* console.log(markdown);
|
|
16820
|
+
*
|
|
16821
|
+
* // Output:
|
|
16822
|
+
* // # Risk Rejection Report: BTCUSDT:my-strategy
|
|
16823
|
+
* //
|
|
16824
|
+
* // | Symbol | Strategy | Position | Exchange | Price | Active Positions | Reason | Timestamp |
|
|
16825
|
+
* // | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
16826
|
+
* // | BTCUSDT | my-strategy | LONG | binance | 50000.00000000 USD | 3 | Max 3 positions allowed | 2024-01-15T10:30:00.000Z |
|
|
16827
|
+
* //
|
|
16828
|
+
* // **Total rejections:** 1
|
|
16829
|
+
* //
|
|
16830
|
+
* // ## Rejections by Symbol
|
|
16831
|
+
* // - BTCUSDT: 1
|
|
16832
|
+
* //
|
|
16833
|
+
* // ## Rejections by Strategy
|
|
16834
|
+
* // - my-strategy: 1
|
|
16835
|
+
* ```
|
|
16836
|
+
*/
|
|
16837
|
+
this.getReport = async (symbol, strategyName) => {
|
|
16838
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_GET_REPORT, { symbol, strategyName });
|
|
16839
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_GET_REPORT);
|
|
16840
|
+
{
|
|
16841
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16842
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_GET_REPORT);
|
|
16843
|
+
}
|
|
16844
|
+
return await backtest$1.riskMarkdownService.getReport(symbol, strategyName);
|
|
16845
|
+
};
|
|
16846
|
+
/**
|
|
16847
|
+
* Generates and saves markdown report to file.
|
|
16848
|
+
*
|
|
16849
|
+
* Creates directory if it doesn't exist.
|
|
16850
|
+
* Filename format: {symbol}_{strategyName}.md (e.g., "BTCUSDT_my-strategy.md")
|
|
16851
|
+
*
|
|
16852
|
+
* Delegates to RiskMarkdownService.dump() which:
|
|
16853
|
+
* 1. Generates markdown report via getReport()
|
|
16854
|
+
* 2. Creates output directory (recursive mkdir)
|
|
16855
|
+
* 3. Writes file with UTF-8 encoding
|
|
16856
|
+
* 4. Logs success/failure to console
|
|
16857
|
+
*
|
|
16858
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
16859
|
+
* @param strategyName - Strategy name (e.g., "my-strategy")
|
|
16860
|
+
* @param path - Output directory path (default: "./dump/risk")
|
|
16861
|
+
* @returns Promise that resolves when file is written
|
|
16862
|
+
*
|
|
16863
|
+
* @example
|
|
16864
|
+
* ```typescript
|
|
16865
|
+
* // Save to default path: ./dump/risk/BTCUSDT_my-strategy.md
|
|
16866
|
+
* await Risk.dump("BTCUSDT", "my-strategy");
|
|
16867
|
+
*
|
|
16868
|
+
* // Save to custom path: ./reports/risk/BTCUSDT_my-strategy.md
|
|
16869
|
+
* await Risk.dump("BTCUSDT", "my-strategy", "./reports/risk");
|
|
16870
|
+
*
|
|
16871
|
+
* // After multiple symbols backtested, export all risk reports
|
|
16872
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
|
|
16873
|
+
* await Risk.dump(symbol, "my-strategy", "./backtest-results");
|
|
16874
|
+
* }
|
|
16875
|
+
* ```
|
|
16876
|
+
*/
|
|
16877
|
+
this.dump = async (symbol, strategyName, path) => {
|
|
16878
|
+
backtest$1.loggerService.info(RISK_METHOD_NAME_DUMP, { symbol, strategyName, path });
|
|
16879
|
+
backtest$1.strategyValidationService.validate(strategyName, RISK_METHOD_NAME_DUMP);
|
|
16880
|
+
{
|
|
16881
|
+
const { riskName } = backtest$1.strategySchemaService.get(strategyName);
|
|
16882
|
+
riskName && backtest$1.riskValidationService.validate(riskName, RISK_METHOD_NAME_DUMP);
|
|
16883
|
+
}
|
|
16884
|
+
await backtest$1.riskMarkdownService.dump(symbol, strategyName, path);
|
|
16885
|
+
};
|
|
16886
|
+
}
|
|
16887
|
+
}
|
|
16888
|
+
/**
|
|
16889
|
+
* Global singleton instance of RiskUtils.
|
|
16890
|
+
* Provides static-like access to risk rejection reporting methods.
|
|
16891
|
+
*
|
|
16892
|
+
* @example
|
|
16893
|
+
* ```typescript
|
|
16894
|
+
* import { Risk } from "backtest-kit";
|
|
16895
|
+
*
|
|
16896
|
+
* // Usage same as RiskUtils methods
|
|
16897
|
+
* const stats = await Risk.getData("BTCUSDT", "my-strategy");
|
|
16898
|
+
* const report = await Risk.getReport("BTCUSDT", "my-strategy");
|
|
16899
|
+
* await Risk.dump("BTCUSDT", "my-strategy");
|
|
16900
|
+
* ```
|
|
16901
|
+
*/
|
|
16902
|
+
const Risk = new RiskUtils();
|
|
16903
|
+
|
|
16175
16904
|
exports.Backtest = Backtest;
|
|
16176
16905
|
exports.Constant = Constant;
|
|
16177
16906
|
exports.ExecutionContextService = ExecutionContextService;
|
|
@@ -16187,6 +16916,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
|
16187
16916
|
exports.PersistScheduleAdapter = PersistScheduleAdapter;
|
|
16188
16917
|
exports.PersistSignalAdapter = PersistSignalAdapter;
|
|
16189
16918
|
exports.PositionSize = PositionSize;
|
|
16919
|
+
exports.Risk = Risk;
|
|
16190
16920
|
exports.Schedule = Schedule;
|
|
16191
16921
|
exports.Walker = Walker;
|
|
16192
16922
|
exports.addExchange = addExchange;
|
|
@@ -16229,6 +16959,8 @@ exports.listenPartialLossOnce = listenPartialLossOnce;
|
|
|
16229
16959
|
exports.listenPartialProfit = listenPartialProfit;
|
|
16230
16960
|
exports.listenPartialProfitOnce = listenPartialProfitOnce;
|
|
16231
16961
|
exports.listenPerformance = listenPerformance;
|
|
16962
|
+
exports.listenRisk = listenRisk;
|
|
16963
|
+
exports.listenRiskOnce = listenRiskOnce;
|
|
16232
16964
|
exports.listenSignal = listenSignal;
|
|
16233
16965
|
exports.listenSignalBacktest = listenSignalBacktest;
|
|
16234
16966
|
exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|