backtest-kit 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/build/index.cjs +1891 -132
- package/build/index.mjs +1890 -134
- package/package.json +2 -1
- package/types.d.ts +1709 -865
package/build/index.cjs
CHANGED
|
@@ -121,6 +121,7 @@ const markdownServices$1 = {
|
|
|
121
121
|
breakevenMarkdownService: Symbol('breakevenMarkdownService'),
|
|
122
122
|
outlineMarkdownService: Symbol('outlineMarkdownService'),
|
|
123
123
|
riskMarkdownService: Symbol('riskMarkdownService'),
|
|
124
|
+
strategyMarkdownService: Symbol('strategyMarkdownService'),
|
|
124
125
|
};
|
|
125
126
|
const reportServices$1 = {
|
|
126
127
|
backtestReportService: Symbol('backtestReportService'),
|
|
@@ -132,6 +133,7 @@ const reportServices$1 = {
|
|
|
132
133
|
partialReportService: Symbol('partialReportService'),
|
|
133
134
|
breakevenReportService: Symbol('breakevenReportService'),
|
|
134
135
|
riskReportService: Symbol('riskReportService'),
|
|
136
|
+
strategyReportService: Symbol('strategyReportService'),
|
|
135
137
|
};
|
|
136
138
|
const validationServices$1 = {
|
|
137
139
|
exchangeValidationService: Symbol('exchangeValidationService'),
|
|
@@ -540,6 +542,20 @@ const schedulePingSubject = new functoolsKit.Subject();
|
|
|
540
542
|
* Allows users to track active signal lifecycle and implement custom dynamic management logic.
|
|
541
543
|
*/
|
|
542
544
|
const activePingSubject = new functoolsKit.Subject();
|
|
545
|
+
/**
|
|
546
|
+
* Strategy management signal emitter.
|
|
547
|
+
* Emits when strategy management actions are executed:
|
|
548
|
+
* - cancel-scheduled: Scheduled signal cancelled
|
|
549
|
+
* - close-pending: Pending signal closed
|
|
550
|
+
* - partial-profit: Partial close at profit level
|
|
551
|
+
* - partial-loss: Partial close at loss level
|
|
552
|
+
* - trailing-stop: Stop-loss adjusted
|
|
553
|
+
* - trailing-take: Take-profit adjusted
|
|
554
|
+
* - breakeven: Stop-loss moved to entry price
|
|
555
|
+
*
|
|
556
|
+
* Used by StrategyReportService and StrategyMarkdownService for event logging and reporting.
|
|
557
|
+
*/
|
|
558
|
+
const strategyCommitSubject = new functoolsKit.Subject();
|
|
543
559
|
|
|
544
560
|
var emitters = /*#__PURE__*/Object.freeze({
|
|
545
561
|
__proto__: null,
|
|
@@ -560,6 +576,7 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
560
576
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
561
577
|
signalEmitter: signalEmitter,
|
|
562
578
|
signalLiveEmitter: signalLiveEmitter,
|
|
579
|
+
strategyCommitSubject: strategyCommitSubject,
|
|
563
580
|
validationSubject: validationSubject,
|
|
564
581
|
walkerCompleteSubject: walkerCompleteSubject,
|
|
565
582
|
walkerEmitter: walkerEmitter,
|
|
@@ -1565,8 +1582,15 @@ class PersistCandleUtils {
|
|
|
1565
1582
|
const candle = await stateStorage.readValue(timestamp);
|
|
1566
1583
|
cachedCandles.push(candle);
|
|
1567
1584
|
}
|
|
1568
|
-
catch {
|
|
1569
|
-
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${timestamp}`;
|
|
1587
|
+
const payload = {
|
|
1588
|
+
error: functoolsKit.errorData(error),
|
|
1589
|
+
message: functoolsKit.getErrorMessage(error),
|
|
1590
|
+
};
|
|
1591
|
+
bt.loggerService.warn(message, payload);
|
|
1592
|
+
console.warn(message, payload);
|
|
1593
|
+
errorEmitter.next(error);
|
|
1570
1594
|
continue;
|
|
1571
1595
|
}
|
|
1572
1596
|
}
|
|
@@ -6533,7 +6557,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
|
|
|
6533
6557
|
* @param backtest - Whether running in backtest mode
|
|
6534
6558
|
* @returns Unique string key for memoization
|
|
6535
6559
|
*/
|
|
6536
|
-
const CREATE_KEY_FN$
|
|
6560
|
+
const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
6537
6561
|
const parts = [symbol, strategyName, exchangeName];
|
|
6538
6562
|
if (frameName)
|
|
6539
6563
|
parts.push(frameName);
|
|
@@ -6698,7 +6722,7 @@ class StrategyConnectionService {
|
|
|
6698
6722
|
* @param backtest - Whether running in backtest mode
|
|
6699
6723
|
* @returns Configured ClientStrategy instance
|
|
6700
6724
|
*/
|
|
6701
|
-
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
6725
|
+
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
6702
6726
|
const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
6703
6727
|
return new ClientStrategy({
|
|
6704
6728
|
symbol,
|
|
@@ -6953,7 +6977,7 @@ class StrategyConnectionService {
|
|
|
6953
6977
|
}
|
|
6954
6978
|
return;
|
|
6955
6979
|
}
|
|
6956
|
-
const key = CREATE_KEY_FN$
|
|
6980
|
+
const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
6957
6981
|
if (!this.getStrategy.has(key)) {
|
|
6958
6982
|
return;
|
|
6959
6983
|
}
|
|
@@ -7922,7 +7946,7 @@ class ClientRisk {
|
|
|
7922
7946
|
* @param backtest - Whether running in backtest mode
|
|
7923
7947
|
* @returns Unique string key for memoization
|
|
7924
7948
|
*/
|
|
7925
|
-
const CREATE_KEY_FN$
|
|
7949
|
+
const CREATE_KEY_FN$k = (riskName, exchangeName, frameName, backtest) => {
|
|
7926
7950
|
const parts = [riskName, exchangeName];
|
|
7927
7951
|
if (frameName)
|
|
7928
7952
|
parts.push(frameName);
|
|
@@ -8021,7 +8045,7 @@ class RiskConnectionService {
|
|
|
8021
8045
|
* @param backtest - True if backtest mode, false if live mode
|
|
8022
8046
|
* @returns Configured ClientRisk instance
|
|
8023
8047
|
*/
|
|
8024
|
-
this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
8048
|
+
this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
|
|
8025
8049
|
const schema = this.riskSchemaService.get(riskName);
|
|
8026
8050
|
return new ClientRisk({
|
|
8027
8051
|
...schema,
|
|
@@ -8089,7 +8113,7 @@ class RiskConnectionService {
|
|
|
8089
8113
|
payload,
|
|
8090
8114
|
});
|
|
8091
8115
|
if (payload) {
|
|
8092
|
-
const key = CREATE_KEY_FN$
|
|
8116
|
+
const key = CREATE_KEY_FN$k(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
8093
8117
|
this.getRisk.clear(key);
|
|
8094
8118
|
}
|
|
8095
8119
|
else {
|
|
@@ -9512,7 +9536,7 @@ class ClientAction {
|
|
|
9512
9536
|
* @param backtest - Whether running in backtest mode
|
|
9513
9537
|
* @returns Unique string key for memoization
|
|
9514
9538
|
*/
|
|
9515
|
-
const CREATE_KEY_FN$
|
|
9539
|
+
const CREATE_KEY_FN$j = (actionName, strategyName, exchangeName, frameName, backtest) => {
|
|
9516
9540
|
const parts = [actionName, strategyName, exchangeName];
|
|
9517
9541
|
if (frameName)
|
|
9518
9542
|
parts.push(frameName);
|
|
@@ -9563,7 +9587,7 @@ class ActionConnectionService {
|
|
|
9563
9587
|
* @param backtest - True if backtest mode, false if live mode
|
|
9564
9588
|
* @returns Configured ClientAction instance
|
|
9565
9589
|
*/
|
|
9566
|
-
this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
9590
|
+
this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$j(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
|
|
9567
9591
|
const schema = this.actionSchemaService.get(actionName);
|
|
9568
9592
|
return new ClientAction({
|
|
9569
9593
|
...schema,
|
|
@@ -9756,7 +9780,7 @@ class ActionConnectionService {
|
|
|
9756
9780
|
await Promise.all(actions.map(async (action) => await action.dispose()));
|
|
9757
9781
|
return;
|
|
9758
9782
|
}
|
|
9759
|
-
const key = CREATE_KEY_FN$
|
|
9783
|
+
const key = CREATE_KEY_FN$j(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
9760
9784
|
if (!this.getAction.has(key)) {
|
|
9761
9785
|
return;
|
|
9762
9786
|
}
|
|
@@ -9774,7 +9798,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
|
|
|
9774
9798
|
* @param exchangeName - Exchange name
|
|
9775
9799
|
* @returns Unique string key for memoization
|
|
9776
9800
|
*/
|
|
9777
|
-
const CREATE_KEY_FN$
|
|
9801
|
+
const CREATE_KEY_FN$i = (exchangeName) => {
|
|
9778
9802
|
return exchangeName;
|
|
9779
9803
|
};
|
|
9780
9804
|
/**
|
|
@@ -9798,7 +9822,7 @@ class ExchangeCoreService {
|
|
|
9798
9822
|
* @param exchangeName - Name of the exchange to validate
|
|
9799
9823
|
* @returns Promise that resolves when validation is complete
|
|
9800
9824
|
*/
|
|
9801
|
-
this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$
|
|
9825
|
+
this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$i(exchangeName), async (exchangeName) => {
|
|
9802
9826
|
this.loggerService.log(METHOD_NAME_VALIDATE$2, {
|
|
9803
9827
|
exchangeName,
|
|
9804
9828
|
});
|
|
@@ -10022,12 +10046,32 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
|
|
|
10022
10046
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10023
10047
|
* @returns Unique string key for memoization
|
|
10024
10048
|
*/
|
|
10025
|
-
const CREATE_KEY_FN$
|
|
10049
|
+
const CREATE_KEY_FN$h = (context) => {
|
|
10026
10050
|
const parts = [context.strategyName, context.exchangeName];
|
|
10027
10051
|
if (context.frameName)
|
|
10028
10052
|
parts.push(context.frameName);
|
|
10029
10053
|
return parts.join(":");
|
|
10030
10054
|
};
|
|
10055
|
+
/**
|
|
10056
|
+
* Broadcasts StrategyCommitContract event to strategyCommitSubject.
|
|
10057
|
+
*
|
|
10058
|
+
* @param event - The signal commit event to broadcast
|
|
10059
|
+
*/
|
|
10060
|
+
const CALL_STRATEGY_COMMIT_FN = functoolsKit.trycatch(async (event) => {
|
|
10061
|
+
await strategyCommitSubject.next(event);
|
|
10062
|
+
}, {
|
|
10063
|
+
fallback: (error) => {
|
|
10064
|
+
const message = "StrategyCoreService CALL_STRATEGY_COMMIT_FN thrown";
|
|
10065
|
+
const payload = {
|
|
10066
|
+
error: functoolsKit.errorData(error),
|
|
10067
|
+
message: functoolsKit.getErrorMessage(error),
|
|
10068
|
+
};
|
|
10069
|
+
bt.loggerService.warn(message, payload);
|
|
10070
|
+
console.warn(message, payload);
|
|
10071
|
+
errorEmitter.next(error);
|
|
10072
|
+
},
|
|
10073
|
+
defaultValue: null,
|
|
10074
|
+
});
|
|
10031
10075
|
/**
|
|
10032
10076
|
* Global service for strategy operations with execution context injection.
|
|
10033
10077
|
*
|
|
@@ -10054,7 +10098,7 @@ class StrategyCoreService {
|
|
|
10054
10098
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10055
10099
|
* @returns Promise that resolves when validation is complete
|
|
10056
10100
|
*/
|
|
10057
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
10101
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$h(context), async (context) => {
|
|
10058
10102
|
this.loggerService.log(METHOD_NAME_VALIDATE$1, {
|
|
10059
10103
|
context,
|
|
10060
10104
|
});
|
|
@@ -10260,7 +10304,19 @@ class StrategyCoreService {
|
|
|
10260
10304
|
cancelId,
|
|
10261
10305
|
});
|
|
10262
10306
|
await this.validate(context);
|
|
10263
|
-
|
|
10307
|
+
const result = await this.strategyConnectionService.cancelScheduled(backtest, symbol, context, cancelId);
|
|
10308
|
+
{
|
|
10309
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10310
|
+
action: "cancel-scheduled",
|
|
10311
|
+
symbol,
|
|
10312
|
+
strategyName: context.strategyName,
|
|
10313
|
+
exchangeName: context.exchangeName,
|
|
10314
|
+
frameName: context.frameName,
|
|
10315
|
+
backtest,
|
|
10316
|
+
cancelId,
|
|
10317
|
+
});
|
|
10318
|
+
}
|
|
10319
|
+
return result;
|
|
10264
10320
|
};
|
|
10265
10321
|
/**
|
|
10266
10322
|
* Closes the pending signal without stopping the strategy.
|
|
@@ -10287,7 +10343,19 @@ class StrategyCoreService {
|
|
|
10287
10343
|
closeId,
|
|
10288
10344
|
});
|
|
10289
10345
|
await this.validate(context);
|
|
10290
|
-
|
|
10346
|
+
const result = await this.strategyConnectionService.closePending(backtest, symbol, context, closeId);
|
|
10347
|
+
{
|
|
10348
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10349
|
+
action: "close-pending",
|
|
10350
|
+
symbol,
|
|
10351
|
+
strategyName: context.strategyName,
|
|
10352
|
+
exchangeName: context.exchangeName,
|
|
10353
|
+
frameName: context.frameName,
|
|
10354
|
+
backtest,
|
|
10355
|
+
closeId,
|
|
10356
|
+
});
|
|
10357
|
+
}
|
|
10358
|
+
return result;
|
|
10291
10359
|
};
|
|
10292
10360
|
/**
|
|
10293
10361
|
* Disposes the ClientStrategy instance for the given context.
|
|
@@ -10368,7 +10436,20 @@ class StrategyCoreService {
|
|
|
10368
10436
|
backtest,
|
|
10369
10437
|
});
|
|
10370
10438
|
await this.validate(context);
|
|
10371
|
-
|
|
10439
|
+
const result = await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
|
|
10440
|
+
if (result) {
|
|
10441
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10442
|
+
action: "partial-profit",
|
|
10443
|
+
symbol,
|
|
10444
|
+
strategyName: context.strategyName,
|
|
10445
|
+
exchangeName: context.exchangeName,
|
|
10446
|
+
frameName: context.frameName,
|
|
10447
|
+
backtest,
|
|
10448
|
+
percentToClose,
|
|
10449
|
+
currentPrice,
|
|
10450
|
+
});
|
|
10451
|
+
}
|
|
10452
|
+
return result;
|
|
10372
10453
|
};
|
|
10373
10454
|
/**
|
|
10374
10455
|
* Executes partial close at loss level (moving toward SL).
|
|
@@ -10409,7 +10490,20 @@ class StrategyCoreService {
|
|
|
10409
10490
|
backtest,
|
|
10410
10491
|
});
|
|
10411
10492
|
await this.validate(context);
|
|
10412
|
-
|
|
10493
|
+
const result = await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
|
|
10494
|
+
if (result) {
|
|
10495
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10496
|
+
action: "partial-loss",
|
|
10497
|
+
symbol,
|
|
10498
|
+
strategyName: context.strategyName,
|
|
10499
|
+
exchangeName: context.exchangeName,
|
|
10500
|
+
frameName: context.frameName,
|
|
10501
|
+
backtest,
|
|
10502
|
+
percentToClose,
|
|
10503
|
+
currentPrice,
|
|
10504
|
+
});
|
|
10505
|
+
}
|
|
10506
|
+
return result;
|
|
10413
10507
|
};
|
|
10414
10508
|
/**
|
|
10415
10509
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
@@ -10448,7 +10542,20 @@ class StrategyCoreService {
|
|
|
10448
10542
|
backtest,
|
|
10449
10543
|
});
|
|
10450
10544
|
await this.validate(context);
|
|
10451
|
-
|
|
10545
|
+
const result = await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, currentPrice, context);
|
|
10546
|
+
if (result) {
|
|
10547
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10548
|
+
action: "trailing-stop",
|
|
10549
|
+
symbol,
|
|
10550
|
+
strategyName: context.strategyName,
|
|
10551
|
+
exchangeName: context.exchangeName,
|
|
10552
|
+
frameName: context.frameName,
|
|
10553
|
+
backtest,
|
|
10554
|
+
percentShift,
|
|
10555
|
+
currentPrice,
|
|
10556
|
+
});
|
|
10557
|
+
}
|
|
10558
|
+
return result;
|
|
10452
10559
|
};
|
|
10453
10560
|
/**
|
|
10454
10561
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
@@ -10483,7 +10590,20 @@ class StrategyCoreService {
|
|
|
10483
10590
|
backtest,
|
|
10484
10591
|
});
|
|
10485
10592
|
await this.validate(context);
|
|
10486
|
-
|
|
10593
|
+
const result = await this.strategyConnectionService.trailingTake(backtest, symbol, percentShift, currentPrice, context);
|
|
10594
|
+
if (result) {
|
|
10595
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10596
|
+
action: "trailing-take",
|
|
10597
|
+
symbol,
|
|
10598
|
+
strategyName: context.strategyName,
|
|
10599
|
+
exchangeName: context.exchangeName,
|
|
10600
|
+
frameName: context.frameName,
|
|
10601
|
+
backtest,
|
|
10602
|
+
percentShift,
|
|
10603
|
+
currentPrice,
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10606
|
+
return result;
|
|
10487
10607
|
};
|
|
10488
10608
|
/**
|
|
10489
10609
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
@@ -10513,7 +10633,19 @@ class StrategyCoreService {
|
|
|
10513
10633
|
backtest,
|
|
10514
10634
|
});
|
|
10515
10635
|
await this.validate(context);
|
|
10516
|
-
|
|
10636
|
+
const result = await this.strategyConnectionService.breakeven(backtest, symbol, currentPrice, context);
|
|
10637
|
+
if (result) {
|
|
10638
|
+
await CALL_STRATEGY_COMMIT_FN({
|
|
10639
|
+
action: "breakeven",
|
|
10640
|
+
symbol,
|
|
10641
|
+
strategyName: context.strategyName,
|
|
10642
|
+
exchangeName: context.exchangeName,
|
|
10643
|
+
frameName: context.frameName,
|
|
10644
|
+
backtest,
|
|
10645
|
+
currentPrice,
|
|
10646
|
+
});
|
|
10647
|
+
}
|
|
10648
|
+
return result;
|
|
10517
10649
|
};
|
|
10518
10650
|
}
|
|
10519
10651
|
}
|
|
@@ -10587,7 +10719,7 @@ class SizingGlobalService {
|
|
|
10587
10719
|
* @param context - Context with riskName, exchangeName, frameName
|
|
10588
10720
|
* @returns Unique string key for memoization
|
|
10589
10721
|
*/
|
|
10590
|
-
const CREATE_KEY_FN$
|
|
10722
|
+
const CREATE_KEY_FN$g = (context) => {
|
|
10591
10723
|
const parts = [context.riskName, context.exchangeName];
|
|
10592
10724
|
if (context.frameName)
|
|
10593
10725
|
parts.push(context.frameName);
|
|
@@ -10613,7 +10745,7 @@ class RiskGlobalService {
|
|
|
10613
10745
|
* @param payload - Payload with riskName, exchangeName and frameName
|
|
10614
10746
|
* @returns Promise that resolves when validation is complete
|
|
10615
10747
|
*/
|
|
10616
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
10748
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$g(context), async (context) => {
|
|
10617
10749
|
this.loggerService.log("riskGlobalService validate", {
|
|
10618
10750
|
context,
|
|
10619
10751
|
});
|
|
@@ -10691,7 +10823,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
|
|
|
10691
10823
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
10692
10824
|
* @returns Unique string key for memoization
|
|
10693
10825
|
*/
|
|
10694
|
-
const CREATE_KEY_FN$
|
|
10826
|
+
const CREATE_KEY_FN$f = (context) => {
|
|
10695
10827
|
const parts = [context.strategyName, context.exchangeName];
|
|
10696
10828
|
if (context.frameName)
|
|
10697
10829
|
parts.push(context.frameName);
|
|
@@ -10735,7 +10867,7 @@ class ActionCoreService {
|
|
|
10735
10867
|
* @param context - Strategy execution context with strategyName, exchangeName and frameName
|
|
10736
10868
|
* @returns Promise that resolves when all validations complete
|
|
10737
10869
|
*/
|
|
10738
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
10870
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$f(context), async (context) => {
|
|
10739
10871
|
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
10740
10872
|
context,
|
|
10741
10873
|
});
|
|
@@ -13985,6 +14117,116 @@ const schedule_columns = [
|
|
|
13985
14117
|
},
|
|
13986
14118
|
];
|
|
13987
14119
|
|
|
14120
|
+
/**
|
|
14121
|
+
* Column configuration for strategy markdown reports.
|
|
14122
|
+
*
|
|
14123
|
+
* Defines the table structure for displaying strategy management events in trading reports.
|
|
14124
|
+
* Each column specifies how to format and display strategy actions like partial profit/loss,
|
|
14125
|
+
* trailing stop/take, breakeven, cancel scheduled, and close pending.
|
|
14126
|
+
*
|
|
14127
|
+
* Used by {@link StrategyMarkdownService} to generate markdown tables showing:
|
|
14128
|
+
* - Signal identification (symbol, strategy name, signal ID)
|
|
14129
|
+
* - Action information (action type, percent values, prices)
|
|
14130
|
+
* - Timing information (timestamp, mode: backtest or live)
|
|
14131
|
+
*
|
|
14132
|
+
* @remarks
|
|
14133
|
+
* This configuration tracks all strategy management events - when signals are
|
|
14134
|
+
* modified, cancelled, or closed during their lifecycle.
|
|
14135
|
+
*
|
|
14136
|
+
* @example
|
|
14137
|
+
* ```typescript
|
|
14138
|
+
* import { strategy_columns } from "./assets/strategy.columns";
|
|
14139
|
+
*
|
|
14140
|
+
* // Use with StrategyMarkdownService
|
|
14141
|
+
* const service = new StrategyMarkdownService();
|
|
14142
|
+
* await service.getReport("BTCUSDT", "my-strategy", "binance", "1h", false, strategy_columns);
|
|
14143
|
+
*
|
|
14144
|
+
* // Or customize to show only key fields
|
|
14145
|
+
* const customColumns = strategy_columns.filter(col =>
|
|
14146
|
+
* ["symbol", "action", "currentPrice", "timestamp"].includes(col.key)
|
|
14147
|
+
* );
|
|
14148
|
+
* await service.getReport("BTCUSDT", "my-strategy", "binance", "1h", false, customColumns);
|
|
14149
|
+
* ```
|
|
14150
|
+
*
|
|
14151
|
+
* @see {@link StrategyMarkdownService} for usage in report generation
|
|
14152
|
+
* @see {@link ColumnModel} for column interface definition
|
|
14153
|
+
* @see {@link StrategyEvent} for data structure
|
|
14154
|
+
*/
|
|
14155
|
+
const strategy_columns = [
|
|
14156
|
+
{
|
|
14157
|
+
key: "symbol",
|
|
14158
|
+
label: "Symbol",
|
|
14159
|
+
format: (data) => data.symbol,
|
|
14160
|
+
isVisible: () => true,
|
|
14161
|
+
},
|
|
14162
|
+
{
|
|
14163
|
+
key: "strategyName",
|
|
14164
|
+
label: "Strategy",
|
|
14165
|
+
format: (data) => data.strategyName,
|
|
14166
|
+
isVisible: () => true,
|
|
14167
|
+
},
|
|
14168
|
+
{
|
|
14169
|
+
key: "signalId",
|
|
14170
|
+
label: "Signal ID",
|
|
14171
|
+
format: (data) => data.signalId,
|
|
14172
|
+
isVisible: () => true,
|
|
14173
|
+
},
|
|
14174
|
+
{
|
|
14175
|
+
key: "action",
|
|
14176
|
+
label: "Action",
|
|
14177
|
+
format: (data) => data.action,
|
|
14178
|
+
isVisible: () => true,
|
|
14179
|
+
},
|
|
14180
|
+
{
|
|
14181
|
+
key: "currentPrice",
|
|
14182
|
+
label: "Price",
|
|
14183
|
+
format: (data) => (data.currentPrice !== undefined ? `${data.currentPrice.toFixed(8)} USD` : "N/A"),
|
|
14184
|
+
isVisible: () => true,
|
|
14185
|
+
},
|
|
14186
|
+
{
|
|
14187
|
+
key: "percentToClose",
|
|
14188
|
+
label: "% To Close",
|
|
14189
|
+
format: (data) => (data.percentToClose !== undefined ? `${data.percentToClose.toFixed(2)}%` : "N/A"),
|
|
14190
|
+
isVisible: () => true,
|
|
14191
|
+
},
|
|
14192
|
+
{
|
|
14193
|
+
key: "percentShift",
|
|
14194
|
+
label: "% Shift",
|
|
14195
|
+
format: (data) => (data.percentShift !== undefined ? `${data.percentShift.toFixed(2)}%` : "N/A"),
|
|
14196
|
+
isVisible: () => true,
|
|
14197
|
+
},
|
|
14198
|
+
{
|
|
14199
|
+
key: "cancelId",
|
|
14200
|
+
label: "Cancel ID",
|
|
14201
|
+
format: (data) => data.cancelId || "N/A",
|
|
14202
|
+
isVisible: () => true,
|
|
14203
|
+
},
|
|
14204
|
+
{
|
|
14205
|
+
key: "closeId",
|
|
14206
|
+
label: "Close ID",
|
|
14207
|
+
format: (data) => data.closeId || "N/A",
|
|
14208
|
+
isVisible: () => true,
|
|
14209
|
+
},
|
|
14210
|
+
{
|
|
14211
|
+
key: "createdAt",
|
|
14212
|
+
label: "Created At",
|
|
14213
|
+
format: (data) => data.createdAt || "N/A",
|
|
14214
|
+
isVisible: () => true,
|
|
14215
|
+
},
|
|
14216
|
+
{
|
|
14217
|
+
key: "timestamp",
|
|
14218
|
+
label: "Timestamp",
|
|
14219
|
+
format: (data) => new Date(data.timestamp).toISOString(),
|
|
14220
|
+
isVisible: () => true,
|
|
14221
|
+
},
|
|
14222
|
+
{
|
|
14223
|
+
key: "mode",
|
|
14224
|
+
label: "Mode",
|
|
14225
|
+
format: (data) => (data.backtest ? "Backtest" : "Live"),
|
|
14226
|
+
isVisible: () => true,
|
|
14227
|
+
},
|
|
14228
|
+
];
|
|
14229
|
+
|
|
13988
14230
|
/**
|
|
13989
14231
|
* Column configuration for walker strategy comparison table in markdown reports.
|
|
13990
14232
|
*
|
|
@@ -14198,6 +14440,8 @@ const COLUMN_CONFIG = {
|
|
|
14198
14440
|
risk_columns,
|
|
14199
14441
|
/** Columns for scheduled report output */
|
|
14200
14442
|
schedule_columns,
|
|
14443
|
+
/** Columns for strategy management events */
|
|
14444
|
+
strategy_columns,
|
|
14201
14445
|
/** Walker: PnL summary columns */
|
|
14202
14446
|
walker_pnl_columns,
|
|
14203
14447
|
/** Walker: strategy-level summary columns */
|
|
@@ -14234,6 +14478,7 @@ const WILDCARD_TARGET$1 = {
|
|
|
14234
14478
|
partial: true,
|
|
14235
14479
|
performance: true,
|
|
14236
14480
|
risk: true,
|
|
14481
|
+
strategy: true,
|
|
14237
14482
|
schedule: true,
|
|
14238
14483
|
walker: true,
|
|
14239
14484
|
};
|
|
@@ -14463,7 +14708,7 @@ class MarkdownUtils {
|
|
|
14463
14708
|
*
|
|
14464
14709
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
14465
14710
|
*/
|
|
14466
|
-
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
|
|
14711
|
+
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, strategy = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
|
|
14467
14712
|
bt.loggerService.debug(MARKDOWN_METHOD_NAME_ENABLE, {
|
|
14468
14713
|
backtest: bt$1,
|
|
14469
14714
|
breakeven,
|
|
@@ -14472,6 +14717,7 @@ class MarkdownUtils {
|
|
|
14472
14717
|
partial,
|
|
14473
14718
|
performance,
|
|
14474
14719
|
risk,
|
|
14720
|
+
strategy,
|
|
14475
14721
|
schedule,
|
|
14476
14722
|
walker,
|
|
14477
14723
|
});
|
|
@@ -14497,6 +14743,9 @@ class MarkdownUtils {
|
|
|
14497
14743
|
if (risk) {
|
|
14498
14744
|
unList.push(bt.riskMarkdownService.subscribe());
|
|
14499
14745
|
}
|
|
14746
|
+
if (strategy) {
|
|
14747
|
+
unList.push(bt.strategyMarkdownService.subscribe());
|
|
14748
|
+
}
|
|
14500
14749
|
if (schedule) {
|
|
14501
14750
|
unList.push(bt.scheduleMarkdownService.subscribe());
|
|
14502
14751
|
}
|
|
@@ -14542,7 +14791,7 @@ class MarkdownUtils {
|
|
|
14542
14791
|
* Markdown.disable();
|
|
14543
14792
|
* ```
|
|
14544
14793
|
*/
|
|
14545
|
-
this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
|
|
14794
|
+
this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, strategy = false, schedule = false, walker = false, } = WILDCARD_TARGET$1) => {
|
|
14546
14795
|
bt.loggerService.debug(MARKDOWN_METHOD_NAME_DISABLE, {
|
|
14547
14796
|
backtest: bt$1,
|
|
14548
14797
|
breakeven,
|
|
@@ -14551,6 +14800,7 @@ class MarkdownUtils {
|
|
|
14551
14800
|
partial,
|
|
14552
14801
|
performance,
|
|
14553
14802
|
risk,
|
|
14803
|
+
strategy,
|
|
14554
14804
|
schedule,
|
|
14555
14805
|
walker,
|
|
14556
14806
|
});
|
|
@@ -14575,6 +14825,9 @@ class MarkdownUtils {
|
|
|
14575
14825
|
if (risk) {
|
|
14576
14826
|
bt.riskMarkdownService.unsubscribe();
|
|
14577
14827
|
}
|
|
14828
|
+
if (strategy) {
|
|
14829
|
+
bt.strategyMarkdownService.unsubscribe();
|
|
14830
|
+
}
|
|
14578
14831
|
if (schedule) {
|
|
14579
14832
|
bt.scheduleMarkdownService.unsubscribe();
|
|
14580
14833
|
}
|
|
@@ -14687,7 +14940,7 @@ const Markdown = new MarkdownAdapter();
|
|
|
14687
14940
|
* @param backtest - Whether running in backtest mode
|
|
14688
14941
|
* @returns Unique string key for memoization
|
|
14689
14942
|
*/
|
|
14690
|
-
const CREATE_KEY_FN$
|
|
14943
|
+
const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
14691
14944
|
const parts = [symbol, strategyName, exchangeName];
|
|
14692
14945
|
if (frameName)
|
|
14693
14946
|
parts.push(frameName);
|
|
@@ -14704,7 +14957,7 @@ const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
14704
14957
|
* @param timestamp - Unix timestamp in milliseconds
|
|
14705
14958
|
* @returns Filename string
|
|
14706
14959
|
*/
|
|
14707
|
-
const CREATE_FILE_NAME_FN$
|
|
14960
|
+
const CREATE_FILE_NAME_FN$9 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
14708
14961
|
const parts = [symbol, strategyName, exchangeName];
|
|
14709
14962
|
if (frameName) {
|
|
14710
14963
|
parts.push(frameName);
|
|
@@ -14733,12 +14986,12 @@ function isUnsafe$3(value) {
|
|
|
14733
14986
|
return false;
|
|
14734
14987
|
}
|
|
14735
14988
|
/** Maximum number of signals to store in backtest reports */
|
|
14736
|
-
const MAX_EVENTS$
|
|
14989
|
+
const MAX_EVENTS$8 = 250;
|
|
14737
14990
|
/**
|
|
14738
14991
|
* Storage class for accumulating closed signals per strategy.
|
|
14739
14992
|
* Maintains a list of all closed signals and provides methods to generate reports.
|
|
14740
14993
|
*/
|
|
14741
|
-
let ReportStorage$
|
|
14994
|
+
let ReportStorage$7 = class ReportStorage {
|
|
14742
14995
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
14743
14996
|
this.symbol = symbol;
|
|
14744
14997
|
this.strategyName = strategyName;
|
|
@@ -14755,7 +15008,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
14755
15008
|
addSignal(data) {
|
|
14756
15009
|
this._signalList.unshift(data);
|
|
14757
15010
|
// Trim queue if exceeded MAX_EVENTS
|
|
14758
|
-
if (this._signalList.length > MAX_EVENTS$
|
|
15011
|
+
if (this._signalList.length > MAX_EVENTS$8) {
|
|
14759
15012
|
this._signalList.pop();
|
|
14760
15013
|
}
|
|
14761
15014
|
}
|
|
@@ -14879,7 +15132,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
14879
15132
|
async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
|
|
14880
15133
|
const markdown = await this.getReport(strategyName, columns);
|
|
14881
15134
|
const timestamp = Date.now();
|
|
14882
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
15135
|
+
const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
14883
15136
|
await Markdown.writeData("backtest", markdown, {
|
|
14884
15137
|
path,
|
|
14885
15138
|
file: filename,
|
|
@@ -14926,7 +15179,7 @@ class BacktestMarkdownService {
|
|
|
14926
15179
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
14927
15180
|
* Each combination gets its own isolated storage instance.
|
|
14928
15181
|
*/
|
|
14929
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
15182
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
|
|
14930
15183
|
/**
|
|
14931
15184
|
* Processes tick events and accumulates closed signals.
|
|
14932
15185
|
* Should be called from IStrategyCallbacks.onTick.
|
|
@@ -15083,7 +15336,7 @@ class BacktestMarkdownService {
|
|
|
15083
15336
|
payload,
|
|
15084
15337
|
});
|
|
15085
15338
|
if (payload) {
|
|
15086
|
-
const key = CREATE_KEY_FN$
|
|
15339
|
+
const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
15087
15340
|
this.getStorage.clear(key);
|
|
15088
15341
|
}
|
|
15089
15342
|
else {
|
|
@@ -15145,7 +15398,7 @@ class BacktestMarkdownService {
|
|
|
15145
15398
|
* @param backtest - Whether running in backtest mode
|
|
15146
15399
|
* @returns Unique string key for memoization
|
|
15147
15400
|
*/
|
|
15148
|
-
const CREATE_KEY_FN$
|
|
15401
|
+
const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
15149
15402
|
const parts = [symbol, strategyName, exchangeName];
|
|
15150
15403
|
if (frameName)
|
|
15151
15404
|
parts.push(frameName);
|
|
@@ -15162,7 +15415,7 @@ const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
15162
15415
|
* @param timestamp - Unix timestamp in milliseconds
|
|
15163
15416
|
* @returns Filename string
|
|
15164
15417
|
*/
|
|
15165
|
-
const CREATE_FILE_NAME_FN$
|
|
15418
|
+
const CREATE_FILE_NAME_FN$8 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
15166
15419
|
const parts = [symbol, strategyName, exchangeName];
|
|
15167
15420
|
if (frameName) {
|
|
15168
15421
|
parts.push(frameName);
|
|
@@ -15191,12 +15444,12 @@ function isUnsafe$2(value) {
|
|
|
15191
15444
|
return false;
|
|
15192
15445
|
}
|
|
15193
15446
|
/** Maximum number of events to store in live trading reports */
|
|
15194
|
-
const MAX_EVENTS$
|
|
15447
|
+
const MAX_EVENTS$7 = 250;
|
|
15195
15448
|
/**
|
|
15196
15449
|
* Storage class for accumulating all tick events per strategy.
|
|
15197
15450
|
* Maintains a chronological list of all events (idle, opened, active, closed).
|
|
15198
15451
|
*/
|
|
15199
|
-
let ReportStorage$
|
|
15452
|
+
let ReportStorage$6 = class ReportStorage {
|
|
15200
15453
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
15201
15454
|
this.symbol = symbol;
|
|
15202
15455
|
this.strategyName = strategyName;
|
|
@@ -15228,7 +15481,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15228
15481
|
}
|
|
15229
15482
|
{
|
|
15230
15483
|
this._eventList.unshift(newEvent);
|
|
15231
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15484
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15232
15485
|
this._eventList.pop();
|
|
15233
15486
|
}
|
|
15234
15487
|
}
|
|
@@ -15255,7 +15508,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15255
15508
|
totalExecuted: data.signal.totalExecuted,
|
|
15256
15509
|
});
|
|
15257
15510
|
// Trim queue if exceeded MAX_EVENTS
|
|
15258
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15511
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15259
15512
|
this._eventList.pop();
|
|
15260
15513
|
}
|
|
15261
15514
|
}
|
|
@@ -15294,7 +15547,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15294
15547
|
// If no previous active event found, add new event
|
|
15295
15548
|
this._eventList.unshift(newEvent);
|
|
15296
15549
|
// Trim queue if exceeded MAX_EVENTS
|
|
15297
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15550
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15298
15551
|
this._eventList.pop();
|
|
15299
15552
|
}
|
|
15300
15553
|
}
|
|
@@ -15326,7 +15579,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15326
15579
|
};
|
|
15327
15580
|
this._eventList.unshift(newEvent);
|
|
15328
15581
|
// Trim queue if exceeded MAX_EVENTS
|
|
15329
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15582
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15330
15583
|
this._eventList.pop();
|
|
15331
15584
|
}
|
|
15332
15585
|
}
|
|
@@ -15352,7 +15605,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15352
15605
|
totalExecuted: data.signal.totalExecuted,
|
|
15353
15606
|
});
|
|
15354
15607
|
// Trim queue if exceeded MAX_EVENTS
|
|
15355
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15608
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15356
15609
|
this._eventList.pop();
|
|
15357
15610
|
}
|
|
15358
15611
|
}
|
|
@@ -15391,7 +15644,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15391
15644
|
// If no previous waiting event found, add new event
|
|
15392
15645
|
this._eventList.unshift(newEvent);
|
|
15393
15646
|
// Trim queue if exceeded MAX_EVENTS
|
|
15394
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15647
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15395
15648
|
this._eventList.pop();
|
|
15396
15649
|
}
|
|
15397
15650
|
}
|
|
@@ -15418,7 +15671,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15418
15671
|
cancelReason: data.reason,
|
|
15419
15672
|
});
|
|
15420
15673
|
// Trim queue if exceeded MAX_EVENTS
|
|
15421
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
15674
|
+
if (this._eventList.length > MAX_EVENTS$7) {
|
|
15422
15675
|
this._eventList.pop();
|
|
15423
15676
|
}
|
|
15424
15677
|
}
|
|
@@ -15557,7 +15810,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
15557
15810
|
async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
|
|
15558
15811
|
const markdown = await this.getReport(strategyName, columns);
|
|
15559
15812
|
const timestamp = Date.now();
|
|
15560
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
15813
|
+
const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
15561
15814
|
await Markdown.writeData("live", markdown, {
|
|
15562
15815
|
path,
|
|
15563
15816
|
signalId: "",
|
|
@@ -15607,7 +15860,7 @@ class LiveMarkdownService {
|
|
|
15607
15860
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
15608
15861
|
* Each combination gets its own isolated storage instance.
|
|
15609
15862
|
*/
|
|
15610
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
15863
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
|
|
15611
15864
|
/**
|
|
15612
15865
|
* Subscribes to live signal emitter to receive tick events.
|
|
15613
15866
|
* Protected against multiple subscriptions.
|
|
@@ -15825,7 +16078,7 @@ class LiveMarkdownService {
|
|
|
15825
16078
|
payload,
|
|
15826
16079
|
});
|
|
15827
16080
|
if (payload) {
|
|
15828
|
-
const key = CREATE_KEY_FN$
|
|
16081
|
+
const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
15829
16082
|
this.getStorage.clear(key);
|
|
15830
16083
|
}
|
|
15831
16084
|
else {
|
|
@@ -15845,7 +16098,7 @@ class LiveMarkdownService {
|
|
|
15845
16098
|
* @param backtest - Whether running in backtest mode
|
|
15846
16099
|
* @returns Unique string key for memoization
|
|
15847
16100
|
*/
|
|
15848
|
-
const CREATE_KEY_FN$
|
|
16101
|
+
const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
15849
16102
|
const parts = [symbol, strategyName, exchangeName];
|
|
15850
16103
|
if (frameName)
|
|
15851
16104
|
parts.push(frameName);
|
|
@@ -15862,7 +16115,7 @@ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
15862
16115
|
* @param timestamp - Unix timestamp in milliseconds
|
|
15863
16116
|
* @returns Filename string
|
|
15864
16117
|
*/
|
|
15865
|
-
const CREATE_FILE_NAME_FN$
|
|
16118
|
+
const CREATE_FILE_NAME_FN$7 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
15866
16119
|
const parts = [symbol, strategyName, exchangeName];
|
|
15867
16120
|
if (frameName) {
|
|
15868
16121
|
parts.push(frameName);
|
|
@@ -15873,12 +16126,12 @@ const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
15873
16126
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
15874
16127
|
};
|
|
15875
16128
|
/** Maximum number of events to store in schedule reports */
|
|
15876
|
-
const MAX_EVENTS$
|
|
16129
|
+
const MAX_EVENTS$6 = 250;
|
|
15877
16130
|
/**
|
|
15878
16131
|
* Storage class for accumulating scheduled signal events per strategy.
|
|
15879
16132
|
* Maintains a chronological list of scheduled and cancelled events.
|
|
15880
16133
|
*/
|
|
15881
|
-
let ReportStorage$
|
|
16134
|
+
let ReportStorage$5 = class ReportStorage {
|
|
15882
16135
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
15883
16136
|
this.symbol = symbol;
|
|
15884
16137
|
this.strategyName = strategyName;
|
|
@@ -15909,7 +16162,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
15909
16162
|
totalExecuted: data.signal.totalExecuted,
|
|
15910
16163
|
});
|
|
15911
16164
|
// Trim queue if exceeded MAX_EVENTS
|
|
15912
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
16165
|
+
if (this._eventList.length > MAX_EVENTS$6) {
|
|
15913
16166
|
this._eventList.pop();
|
|
15914
16167
|
}
|
|
15915
16168
|
}
|
|
@@ -15939,7 +16192,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
15939
16192
|
};
|
|
15940
16193
|
this._eventList.unshift(newEvent);
|
|
15941
16194
|
// Trim queue if exceeded MAX_EVENTS
|
|
15942
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
16195
|
+
if (this._eventList.length > MAX_EVENTS$6) {
|
|
15943
16196
|
this._eventList.pop();
|
|
15944
16197
|
}
|
|
15945
16198
|
}
|
|
@@ -15972,7 +16225,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
15972
16225
|
};
|
|
15973
16226
|
this._eventList.unshift(newEvent);
|
|
15974
16227
|
// Trim queue if exceeded MAX_EVENTS
|
|
15975
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
16228
|
+
if (this._eventList.length > MAX_EVENTS$6) {
|
|
15976
16229
|
this._eventList.pop();
|
|
15977
16230
|
}
|
|
15978
16231
|
}
|
|
@@ -16079,7 +16332,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
16079
16332
|
async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
|
|
16080
16333
|
const markdown = await this.getReport(strategyName, columns);
|
|
16081
16334
|
const timestamp = Date.now();
|
|
16082
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
16335
|
+
const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
16083
16336
|
await Markdown.writeData("schedule", markdown, {
|
|
16084
16337
|
path,
|
|
16085
16338
|
file: filename,
|
|
@@ -16120,7 +16373,7 @@ class ScheduleMarkdownService {
|
|
|
16120
16373
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
16121
16374
|
* Each combination gets its own isolated storage instance.
|
|
16122
16375
|
*/
|
|
16123
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
16376
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$c(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
|
|
16124
16377
|
/**
|
|
16125
16378
|
* Subscribes to signal emitter to receive scheduled signal events.
|
|
16126
16379
|
* Protected against multiple subscriptions.
|
|
@@ -16323,7 +16576,7 @@ class ScheduleMarkdownService {
|
|
|
16323
16576
|
payload,
|
|
16324
16577
|
});
|
|
16325
16578
|
if (payload) {
|
|
16326
|
-
const key = CREATE_KEY_FN$
|
|
16579
|
+
const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
16327
16580
|
this.getStorage.clear(key);
|
|
16328
16581
|
}
|
|
16329
16582
|
else {
|
|
@@ -16343,7 +16596,7 @@ class ScheduleMarkdownService {
|
|
|
16343
16596
|
* @param backtest - Whether running in backtest mode
|
|
16344
16597
|
* @returns Unique string key for memoization
|
|
16345
16598
|
*/
|
|
16346
|
-
const CREATE_KEY_FN$
|
|
16599
|
+
const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
16347
16600
|
const parts = [symbol, strategyName, exchangeName];
|
|
16348
16601
|
if (frameName)
|
|
16349
16602
|
parts.push(frameName);
|
|
@@ -16360,7 +16613,7 @@ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
16360
16613
|
* @param timestamp - Unix timestamp in milliseconds
|
|
16361
16614
|
* @returns Filename string
|
|
16362
16615
|
*/
|
|
16363
|
-
const CREATE_FILE_NAME_FN$
|
|
16616
|
+
const CREATE_FILE_NAME_FN$6 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
16364
16617
|
const parts = [symbol, strategyName, exchangeName];
|
|
16365
16618
|
if (frameName) {
|
|
16366
16619
|
parts.push(frameName);
|
|
@@ -16380,7 +16633,7 @@ function percentile(sortedArray, p) {
|
|
|
16380
16633
|
return sortedArray[Math.max(0, index)];
|
|
16381
16634
|
}
|
|
16382
16635
|
/** Maximum number of performance events to store per strategy */
|
|
16383
|
-
const MAX_EVENTS$
|
|
16636
|
+
const MAX_EVENTS$5 = 10000;
|
|
16384
16637
|
/**
|
|
16385
16638
|
* Storage class for accumulating performance metrics per strategy.
|
|
16386
16639
|
* Maintains a list of all performance events and provides aggregated statistics.
|
|
@@ -16402,7 +16655,7 @@ class PerformanceStorage {
|
|
|
16402
16655
|
addEvent(event) {
|
|
16403
16656
|
this._events.unshift(event);
|
|
16404
16657
|
// Trim queue if exceeded MAX_EVENTS (keep most recent)
|
|
16405
|
-
if (this._events.length > MAX_EVENTS$
|
|
16658
|
+
if (this._events.length > MAX_EVENTS$5) {
|
|
16406
16659
|
this._events.pop();
|
|
16407
16660
|
}
|
|
16408
16661
|
}
|
|
@@ -16544,7 +16797,7 @@ class PerformanceStorage {
|
|
|
16544
16797
|
async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
|
|
16545
16798
|
const markdown = await this.getReport(strategyName, columns);
|
|
16546
16799
|
const timestamp = Date.now();
|
|
16547
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
16800
|
+
const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
16548
16801
|
await Markdown.writeData("performance", markdown, {
|
|
16549
16802
|
path,
|
|
16550
16803
|
file: filename,
|
|
@@ -16591,7 +16844,7 @@ class PerformanceMarkdownService {
|
|
|
16591
16844
|
* Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
16592
16845
|
* Each combination gets its own isolated storage instance.
|
|
16593
16846
|
*/
|
|
16594
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
16847
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
|
|
16595
16848
|
/**
|
|
16596
16849
|
* Subscribes to performance emitter to receive performance events.
|
|
16597
16850
|
* Protected against multiple subscriptions.
|
|
@@ -16758,7 +17011,7 @@ class PerformanceMarkdownService {
|
|
|
16758
17011
|
payload,
|
|
16759
17012
|
});
|
|
16760
17013
|
if (payload) {
|
|
16761
|
-
const key = CREATE_KEY_FN$
|
|
17014
|
+
const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
16762
17015
|
this.getStorage.clear(key);
|
|
16763
17016
|
}
|
|
16764
17017
|
else {
|
|
@@ -16772,7 +17025,7 @@ class PerformanceMarkdownService {
|
|
|
16772
17025
|
* Creates a filename for markdown report based on walker name.
|
|
16773
17026
|
* Filename format: "walkerName-timestamp.md"
|
|
16774
17027
|
*/
|
|
16775
|
-
const CREATE_FILE_NAME_FN$
|
|
17028
|
+
const CREATE_FILE_NAME_FN$5 = (walkerName, timestamp) => {
|
|
16776
17029
|
return `${walkerName}-${timestamp}.md`;
|
|
16777
17030
|
};
|
|
16778
17031
|
/**
|
|
@@ -16807,7 +17060,7 @@ function formatMetric(value) {
|
|
|
16807
17060
|
* Storage class for accumulating walker results.
|
|
16808
17061
|
* Maintains a list of all strategy results and provides methods to generate reports.
|
|
16809
17062
|
*/
|
|
16810
|
-
let ReportStorage$
|
|
17063
|
+
let ReportStorage$4 = class ReportStorage {
|
|
16811
17064
|
constructor(walkerName) {
|
|
16812
17065
|
this.walkerName = walkerName;
|
|
16813
17066
|
/** Walker metadata (set from first addResult call) */
|
|
@@ -16995,7 +17248,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
16995
17248
|
async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
|
|
16996
17249
|
const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
|
|
16997
17250
|
const timestamp = Date.now();
|
|
16998
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
17251
|
+
const filename = CREATE_FILE_NAME_FN$5(this.walkerName, timestamp);
|
|
16999
17252
|
await Markdown.writeData("walker", markdown, {
|
|
17000
17253
|
path,
|
|
17001
17254
|
file: filename,
|
|
@@ -17031,7 +17284,7 @@ class WalkerMarkdownService {
|
|
|
17031
17284
|
* Memoized function to get or create ReportStorage for a walker.
|
|
17032
17285
|
* Each walker gets its own isolated storage instance.
|
|
17033
17286
|
*/
|
|
17034
|
-
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$
|
|
17287
|
+
this.getStorage = functoolsKit.memoize(([walkerName]) => `${walkerName}`, (walkerName) => new ReportStorage$4(walkerName));
|
|
17035
17288
|
/**
|
|
17036
17289
|
* Subscribes to walker emitter to receive walker progress events.
|
|
17037
17290
|
* Protected against multiple subscriptions.
|
|
@@ -17228,7 +17481,7 @@ class WalkerMarkdownService {
|
|
|
17228
17481
|
* @param backtest - Whether running in backtest mode
|
|
17229
17482
|
* @returns Unique string key for memoization
|
|
17230
17483
|
*/
|
|
17231
|
-
const CREATE_KEY_FN$
|
|
17484
|
+
const CREATE_KEY_FN$a = (exchangeName, frameName, backtest) => {
|
|
17232
17485
|
const parts = [exchangeName];
|
|
17233
17486
|
if (frameName)
|
|
17234
17487
|
parts.push(frameName);
|
|
@@ -17239,7 +17492,7 @@ const CREATE_KEY_FN$9 = (exchangeName, frameName, backtest) => {
|
|
|
17239
17492
|
* Creates a filename for markdown report based on memoization key components.
|
|
17240
17493
|
* Filename format: "strategyName_exchangeName_frameName-timestamp.md"
|
|
17241
17494
|
*/
|
|
17242
|
-
const CREATE_FILE_NAME_FN$
|
|
17495
|
+
const CREATE_FILE_NAME_FN$4 = (strategyName, exchangeName, frameName, timestamp) => {
|
|
17243
17496
|
const parts = [strategyName, exchangeName];
|
|
17244
17497
|
if (frameName) {
|
|
17245
17498
|
parts.push(frameName);
|
|
@@ -17272,7 +17525,7 @@ function isUnsafe(value) {
|
|
|
17272
17525
|
return false;
|
|
17273
17526
|
}
|
|
17274
17527
|
/** Maximum number of signals to store per symbol in heatmap reports */
|
|
17275
|
-
const MAX_EVENTS$
|
|
17528
|
+
const MAX_EVENTS$4 = 250;
|
|
17276
17529
|
/**
|
|
17277
17530
|
* Storage class for accumulating closed signals per strategy and generating heatmap.
|
|
17278
17531
|
* Maintains symbol-level statistics and provides portfolio-wide metrics.
|
|
@@ -17298,7 +17551,7 @@ class HeatmapStorage {
|
|
|
17298
17551
|
const signals = this.symbolData.get(symbol);
|
|
17299
17552
|
signals.unshift(data);
|
|
17300
17553
|
// Trim queue if exceeded MAX_EVENTS per symbol
|
|
17301
|
-
if (signals.length > MAX_EVENTS$
|
|
17554
|
+
if (signals.length > MAX_EVENTS$4) {
|
|
17302
17555
|
signals.pop();
|
|
17303
17556
|
}
|
|
17304
17557
|
}
|
|
@@ -17549,7 +17802,7 @@ class HeatmapStorage {
|
|
|
17549
17802
|
async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
|
|
17550
17803
|
const markdown = await this.getReport(strategyName, columns);
|
|
17551
17804
|
const timestamp = Date.now();
|
|
17552
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
17805
|
+
const filename = CREATE_FILE_NAME_FN$4(strategyName, this.exchangeName, this.frameName, timestamp);
|
|
17553
17806
|
await Markdown.writeData("heat", markdown, {
|
|
17554
17807
|
path,
|
|
17555
17808
|
file: filename,
|
|
@@ -17595,7 +17848,7 @@ class HeatMarkdownService {
|
|
|
17595
17848
|
* Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
|
|
17596
17849
|
* Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
|
|
17597
17850
|
*/
|
|
17598
|
-
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
17851
|
+
this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
|
|
17599
17852
|
/**
|
|
17600
17853
|
* Subscribes to signal emitter to receive tick events.
|
|
17601
17854
|
* Protected against multiple subscriptions.
|
|
@@ -17790,7 +18043,7 @@ class HeatMarkdownService {
|
|
|
17790
18043
|
payload,
|
|
17791
18044
|
});
|
|
17792
18045
|
if (payload) {
|
|
17793
|
-
const key = CREATE_KEY_FN$
|
|
18046
|
+
const key = CREATE_KEY_FN$a(payload.exchangeName, payload.frameName, payload.backtest);
|
|
17794
18047
|
this.getStorage.clear(key);
|
|
17795
18048
|
}
|
|
17796
18049
|
else {
|
|
@@ -18821,7 +19074,7 @@ class ClientPartial {
|
|
|
18821
19074
|
* @param backtest - Whether running in backtest mode
|
|
18822
19075
|
* @returns Unique string key for memoization
|
|
18823
19076
|
*/
|
|
18824
|
-
const CREATE_KEY_FN$
|
|
19077
|
+
const CREATE_KEY_FN$9 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
|
|
18825
19078
|
/**
|
|
18826
19079
|
* Creates a callback function for emitting profit events to partialProfitSubject.
|
|
18827
19080
|
*
|
|
@@ -18943,7 +19196,7 @@ class PartialConnectionService {
|
|
|
18943
19196
|
* Key format: "signalId:backtest" or "signalId:live"
|
|
18944
19197
|
* Value: ClientPartial instance with logger and event emitters
|
|
18945
19198
|
*/
|
|
18946
|
-
this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$
|
|
19199
|
+
this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$9(signalId, backtest), (signalId, backtest) => {
|
|
18947
19200
|
return new ClientPartial({
|
|
18948
19201
|
signalId,
|
|
18949
19202
|
logger: this.loggerService,
|
|
@@ -19033,7 +19286,7 @@ class PartialConnectionService {
|
|
|
19033
19286
|
const partial = this.getPartial(data.id, backtest);
|
|
19034
19287
|
await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
|
|
19035
19288
|
await partial.clear(symbol, data, priceClose, backtest);
|
|
19036
|
-
const key = CREATE_KEY_FN$
|
|
19289
|
+
const key = CREATE_KEY_FN$9(data.id, backtest);
|
|
19037
19290
|
this.getPartial.clear(key);
|
|
19038
19291
|
};
|
|
19039
19292
|
}
|
|
@@ -19049,7 +19302,7 @@ class PartialConnectionService {
|
|
|
19049
19302
|
* @param backtest - Whether running in backtest mode
|
|
19050
19303
|
* @returns Unique string key for memoization
|
|
19051
19304
|
*/
|
|
19052
|
-
const CREATE_KEY_FN$
|
|
19305
|
+
const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
19053
19306
|
const parts = [symbol, strategyName, exchangeName];
|
|
19054
19307
|
if (frameName)
|
|
19055
19308
|
parts.push(frameName);
|
|
@@ -19060,7 +19313,7 @@ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
19060
19313
|
* Creates a filename for markdown report based on memoization key components.
|
|
19061
19314
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
19062
19315
|
*/
|
|
19063
|
-
const CREATE_FILE_NAME_FN$
|
|
19316
|
+
const CREATE_FILE_NAME_FN$3 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
19064
19317
|
const parts = [symbol, strategyName, exchangeName];
|
|
19065
19318
|
if (frameName) {
|
|
19066
19319
|
parts.push(frameName);
|
|
@@ -19071,12 +19324,12 @@ const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
19071
19324
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
19072
19325
|
};
|
|
19073
19326
|
/** Maximum number of events to store in partial reports */
|
|
19074
|
-
const MAX_EVENTS$
|
|
19327
|
+
const MAX_EVENTS$3 = 250;
|
|
19075
19328
|
/**
|
|
19076
19329
|
* Storage class for accumulating partial profit/loss events per symbol-strategy pair.
|
|
19077
19330
|
* Maintains a chronological list of profit and loss level events.
|
|
19078
19331
|
*/
|
|
19079
|
-
let ReportStorage$
|
|
19332
|
+
let ReportStorage$3 = class ReportStorage {
|
|
19080
19333
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
19081
19334
|
this.symbol = symbol;
|
|
19082
19335
|
this.strategyName = strategyName;
|
|
@@ -19113,7 +19366,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
19113
19366
|
backtest,
|
|
19114
19367
|
});
|
|
19115
19368
|
// Trim queue if exceeded MAX_EVENTS
|
|
19116
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19369
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
19117
19370
|
this._eventList.pop();
|
|
19118
19371
|
}
|
|
19119
19372
|
}
|
|
@@ -19145,7 +19398,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
19145
19398
|
backtest,
|
|
19146
19399
|
});
|
|
19147
19400
|
// Trim queue if exceeded MAX_EVENTS
|
|
19148
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
19401
|
+
if (this._eventList.length > MAX_EVENTS$3) {
|
|
19149
19402
|
this._eventList.pop();
|
|
19150
19403
|
}
|
|
19151
19404
|
}
|
|
@@ -19221,7 +19474,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
19221
19474
|
async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
|
|
19222
19475
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
19223
19476
|
const timestamp = Date.now();
|
|
19224
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
19477
|
+
const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
19225
19478
|
await Markdown.writeData("partial", markdown, {
|
|
19226
19479
|
path,
|
|
19227
19480
|
file: filename,
|
|
@@ -19262,7 +19515,7 @@ class PartialMarkdownService {
|
|
|
19262
19515
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
19263
19516
|
* Each combination gets its own isolated storage instance.
|
|
19264
19517
|
*/
|
|
19265
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
19518
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
|
|
19266
19519
|
/**
|
|
19267
19520
|
* Subscribes to partial profit/loss signal emitters to receive events.
|
|
19268
19521
|
* Protected against multiple subscriptions.
|
|
@@ -19472,7 +19725,7 @@ class PartialMarkdownService {
|
|
|
19472
19725
|
payload,
|
|
19473
19726
|
});
|
|
19474
19727
|
if (payload) {
|
|
19475
|
-
const key = CREATE_KEY_FN$
|
|
19728
|
+
const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
19476
19729
|
this.getStorage.clear(key);
|
|
19477
19730
|
}
|
|
19478
19731
|
else {
|
|
@@ -19488,7 +19741,7 @@ class PartialMarkdownService {
|
|
|
19488
19741
|
* @param context - Context with strategyName, exchangeName, frameName
|
|
19489
19742
|
* @returns Unique string key for memoization
|
|
19490
19743
|
*/
|
|
19491
|
-
const CREATE_KEY_FN$
|
|
19744
|
+
const CREATE_KEY_FN$7 = (context) => {
|
|
19492
19745
|
const parts = [context.strategyName, context.exchangeName];
|
|
19493
19746
|
if (context.frameName)
|
|
19494
19747
|
parts.push(context.frameName);
|
|
@@ -19562,7 +19815,7 @@ class PartialGlobalService {
|
|
|
19562
19815
|
* @param context - Context with strategyName, exchangeName and frameName
|
|
19563
19816
|
* @param methodName - Name of the calling method for error tracking
|
|
19564
19817
|
*/
|
|
19565
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
19818
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$7(context), (context, methodName) => {
|
|
19566
19819
|
this.loggerService.log("partialGlobalService validate", {
|
|
19567
19820
|
context,
|
|
19568
19821
|
methodName,
|
|
@@ -20017,7 +20270,7 @@ class ClientBreakeven {
|
|
|
20017
20270
|
* @param backtest - Whether running in backtest mode
|
|
20018
20271
|
* @returns Unique string key for memoization
|
|
20019
20272
|
*/
|
|
20020
|
-
const CREATE_KEY_FN$
|
|
20273
|
+
const CREATE_KEY_FN$6 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
|
|
20021
20274
|
/**
|
|
20022
20275
|
* Creates a callback function for emitting breakeven events to breakevenSubject.
|
|
20023
20276
|
*
|
|
@@ -20103,7 +20356,7 @@ class BreakevenConnectionService {
|
|
|
20103
20356
|
* Key format: "signalId:backtest" or "signalId:live"
|
|
20104
20357
|
* Value: ClientBreakeven instance with logger and event emitter
|
|
20105
20358
|
*/
|
|
20106
|
-
this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$
|
|
20359
|
+
this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$6(signalId, backtest), (signalId, backtest) => {
|
|
20107
20360
|
return new ClientBreakeven({
|
|
20108
20361
|
signalId,
|
|
20109
20362
|
logger: this.loggerService,
|
|
@@ -20164,7 +20417,7 @@ class BreakevenConnectionService {
|
|
|
20164
20417
|
const breakeven = this.getBreakeven(data.id, backtest);
|
|
20165
20418
|
await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
|
|
20166
20419
|
await breakeven.clear(symbol, data, priceClose, backtest);
|
|
20167
|
-
const key = CREATE_KEY_FN$
|
|
20420
|
+
const key = CREATE_KEY_FN$6(data.id, backtest);
|
|
20168
20421
|
this.getBreakeven.clear(key);
|
|
20169
20422
|
};
|
|
20170
20423
|
}
|
|
@@ -20180,7 +20433,7 @@ class BreakevenConnectionService {
|
|
|
20180
20433
|
* @param backtest - Whether running in backtest mode
|
|
20181
20434
|
* @returns Unique string key for memoization
|
|
20182
20435
|
*/
|
|
20183
|
-
const CREATE_KEY_FN$
|
|
20436
|
+
const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
20184
20437
|
const parts = [symbol, strategyName, exchangeName];
|
|
20185
20438
|
if (frameName)
|
|
20186
20439
|
parts.push(frameName);
|
|
@@ -20191,7 +20444,7 @@ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
20191
20444
|
* Creates a filename for markdown report based on memoization key components.
|
|
20192
20445
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
20193
20446
|
*/
|
|
20194
|
-
const CREATE_FILE_NAME_FN$
|
|
20447
|
+
const CREATE_FILE_NAME_FN$2 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
20195
20448
|
const parts = [symbol, strategyName, exchangeName];
|
|
20196
20449
|
if (frameName) {
|
|
20197
20450
|
parts.push(frameName);
|
|
@@ -20202,12 +20455,12 @@ const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, ti
|
|
|
20202
20455
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
20203
20456
|
};
|
|
20204
20457
|
/** Maximum number of events to store in breakeven reports */
|
|
20205
|
-
const MAX_EVENTS$
|
|
20458
|
+
const MAX_EVENTS$2 = 250;
|
|
20206
20459
|
/**
|
|
20207
20460
|
* Storage class for accumulating breakeven events per symbol-strategy pair.
|
|
20208
20461
|
* Maintains a chronological list of breakeven events.
|
|
20209
20462
|
*/
|
|
20210
|
-
let ReportStorage$
|
|
20463
|
+
let ReportStorage$2 = class ReportStorage {
|
|
20211
20464
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
20212
20465
|
this.symbol = symbol;
|
|
20213
20466
|
this.strategyName = strategyName;
|
|
@@ -20242,7 +20495,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
20242
20495
|
backtest,
|
|
20243
20496
|
});
|
|
20244
20497
|
// Trim queue if exceeded MAX_EVENTS
|
|
20245
|
-
if (this._eventList.length > MAX_EVENTS$
|
|
20498
|
+
if (this._eventList.length > MAX_EVENTS$2) {
|
|
20246
20499
|
this._eventList.pop();
|
|
20247
20500
|
}
|
|
20248
20501
|
}
|
|
@@ -20310,7 +20563,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
20310
20563
|
async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
|
|
20311
20564
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
20312
20565
|
const timestamp = Date.now();
|
|
20313
|
-
const filename = CREATE_FILE_NAME_FN$
|
|
20566
|
+
const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
20314
20567
|
await Markdown.writeData("breakeven", markdown, {
|
|
20315
20568
|
path,
|
|
20316
20569
|
file: filename,
|
|
@@ -20351,7 +20604,7 @@ class BreakevenMarkdownService {
|
|
|
20351
20604
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
20352
20605
|
* Each combination gets its own isolated storage instance.
|
|
20353
20606
|
*/
|
|
20354
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
20607
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
|
|
20355
20608
|
/**
|
|
20356
20609
|
* Subscribes to breakeven signal emitter to receive events.
|
|
20357
20610
|
* Protected against multiple subscriptions.
|
|
@@ -20540,7 +20793,7 @@ class BreakevenMarkdownService {
|
|
|
20540
20793
|
payload,
|
|
20541
20794
|
});
|
|
20542
20795
|
if (payload) {
|
|
20543
|
-
const key = CREATE_KEY_FN$
|
|
20796
|
+
const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
20544
20797
|
this.getStorage.clear(key);
|
|
20545
20798
|
}
|
|
20546
20799
|
else {
|
|
@@ -20556,7 +20809,7 @@ class BreakevenMarkdownService {
|
|
|
20556
20809
|
* @param context - Context with strategyName, exchangeName, frameName
|
|
20557
20810
|
* @returns Unique string key for memoization
|
|
20558
20811
|
*/
|
|
20559
|
-
const CREATE_KEY_FN$
|
|
20812
|
+
const CREATE_KEY_FN$4 = (context) => {
|
|
20560
20813
|
const parts = [context.strategyName, context.exchangeName];
|
|
20561
20814
|
if (context.frameName)
|
|
20562
20815
|
parts.push(context.frameName);
|
|
@@ -20630,7 +20883,7 @@ class BreakevenGlobalService {
|
|
|
20630
20883
|
* @param context - Context with strategyName, exchangeName and frameName
|
|
20631
20884
|
* @param methodName - Name of the calling method for error tracking
|
|
20632
20885
|
*/
|
|
20633
|
-
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$
|
|
20886
|
+
this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$4(context), (context, methodName) => {
|
|
20634
20887
|
this.loggerService.log("breakevenGlobalService validate", {
|
|
20635
20888
|
context,
|
|
20636
20889
|
methodName,
|
|
@@ -20843,7 +21096,7 @@ class ConfigValidationService {
|
|
|
20843
21096
|
* @param backtest - Whether running in backtest mode
|
|
20844
21097
|
* @returns Unique string key for memoization
|
|
20845
21098
|
*/
|
|
20846
|
-
const CREATE_KEY_FN$
|
|
21099
|
+
const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
20847
21100
|
const parts = [symbol, strategyName, exchangeName];
|
|
20848
21101
|
if (frameName)
|
|
20849
21102
|
parts.push(frameName);
|
|
@@ -20854,7 +21107,7 @@ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest
|
|
|
20854
21107
|
* Creates a filename for markdown report based on memoization key components.
|
|
20855
21108
|
* Filename format: "symbol_strategyName_exchangeName_frameName-timestamp.md"
|
|
20856
21109
|
*/
|
|
20857
|
-
const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
21110
|
+
const CREATE_FILE_NAME_FN$1 = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
20858
21111
|
const parts = [symbol, strategyName, exchangeName];
|
|
20859
21112
|
if (frameName) {
|
|
20860
21113
|
parts.push(frameName);
|
|
@@ -20865,12 +21118,12 @@ const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, time
|
|
|
20865
21118
|
return `${parts.join("_")}-${timestamp}.md`;
|
|
20866
21119
|
};
|
|
20867
21120
|
/** Maximum number of events to store in risk reports */
|
|
20868
|
-
const MAX_EVENTS = 250;
|
|
21121
|
+
const MAX_EVENTS$1 = 250;
|
|
20869
21122
|
/**
|
|
20870
21123
|
* Storage class for accumulating risk rejection events per symbol-strategy pair.
|
|
20871
21124
|
* Maintains a chronological list of rejected signals due to risk limits.
|
|
20872
21125
|
*/
|
|
20873
|
-
class ReportStorage {
|
|
21126
|
+
let ReportStorage$1 = class ReportStorage {
|
|
20874
21127
|
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
20875
21128
|
this.symbol = symbol;
|
|
20876
21129
|
this.strategyName = strategyName;
|
|
@@ -20887,7 +21140,7 @@ class ReportStorage {
|
|
|
20887
21140
|
addRejectionEvent(event) {
|
|
20888
21141
|
this._eventList.unshift(event);
|
|
20889
21142
|
// Trim queue if exceeded MAX_EVENTS
|
|
20890
|
-
if (this._eventList.length > MAX_EVENTS) {
|
|
21143
|
+
if (this._eventList.length > MAX_EVENTS$1) {
|
|
20891
21144
|
this._eventList.pop();
|
|
20892
21145
|
}
|
|
20893
21146
|
}
|
|
@@ -20971,7 +21224,7 @@ class ReportStorage {
|
|
|
20971
21224
|
async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
|
|
20972
21225
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
20973
21226
|
const timestamp = Date.now();
|
|
20974
|
-
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
21227
|
+
const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
20975
21228
|
await Markdown.writeData("risk", markdown, {
|
|
20976
21229
|
path,
|
|
20977
21230
|
file: filename,
|
|
@@ -20982,7 +21235,7 @@ class ReportStorage {
|
|
|
20982
21235
|
frameName: this.frameName
|
|
20983
21236
|
});
|
|
20984
21237
|
}
|
|
20985
|
-
}
|
|
21238
|
+
};
|
|
20986
21239
|
/**
|
|
20987
21240
|
* Service for generating and saving risk rejection markdown reports.
|
|
20988
21241
|
*
|
|
@@ -21012,7 +21265,7 @@ class RiskMarkdownService {
|
|
|
21012
21265
|
* Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
|
|
21013
21266
|
* Each combination gets its own isolated storage instance.
|
|
21014
21267
|
*/
|
|
21015
|
-
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$
|
|
21268
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
|
|
21016
21269
|
/**
|
|
21017
21270
|
* Subscribes to risk rejection emitter to receive rejection events.
|
|
21018
21271
|
* Protected against multiple subscriptions.
|
|
@@ -21201,7 +21454,7 @@ class RiskMarkdownService {
|
|
|
21201
21454
|
payload,
|
|
21202
21455
|
});
|
|
21203
21456
|
if (payload) {
|
|
21204
|
-
const key = CREATE_KEY_FN$
|
|
21457
|
+
const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
21205
21458
|
this.getStorage.clear(key);
|
|
21206
21459
|
}
|
|
21207
21460
|
else {
|
|
@@ -21485,6 +21738,7 @@ class ReportDummy {
|
|
|
21485
21738
|
*/
|
|
21486
21739
|
const WILDCARD_TARGET = {
|
|
21487
21740
|
backtest: true,
|
|
21741
|
+
strategy: true,
|
|
21488
21742
|
breakeven: true,
|
|
21489
21743
|
heat: true,
|
|
21490
21744
|
live: true,
|
|
@@ -21530,7 +21784,7 @@ class ReportUtils {
|
|
|
21530
21784
|
*
|
|
21531
21785
|
* @returns Cleanup function that unsubscribes from all enabled services
|
|
21532
21786
|
*/
|
|
21533
|
-
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET) => {
|
|
21787
|
+
this.enable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, } = WILDCARD_TARGET) => {
|
|
21534
21788
|
bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_ENABLE, {
|
|
21535
21789
|
backtest: bt$1,
|
|
21536
21790
|
breakeven,
|
|
@@ -21541,6 +21795,7 @@ class ReportUtils {
|
|
|
21541
21795
|
risk,
|
|
21542
21796
|
schedule,
|
|
21543
21797
|
walker,
|
|
21798
|
+
strategy,
|
|
21544
21799
|
});
|
|
21545
21800
|
const unList = [];
|
|
21546
21801
|
if (bt$1) {
|
|
@@ -21570,6 +21825,9 @@ class ReportUtils {
|
|
|
21570
21825
|
if (walker) {
|
|
21571
21826
|
unList.push(bt.walkerReportService.subscribe());
|
|
21572
21827
|
}
|
|
21828
|
+
if (strategy) {
|
|
21829
|
+
unList.push(bt.scheduleReportService.subscribe());
|
|
21830
|
+
}
|
|
21573
21831
|
return functoolsKit.compose(...unList.map((un) => () => void un()));
|
|
21574
21832
|
};
|
|
21575
21833
|
/**
|
|
@@ -21608,7 +21866,7 @@ class ReportUtils {
|
|
|
21608
21866
|
* Report.disable();
|
|
21609
21867
|
* ```
|
|
21610
21868
|
*/
|
|
21611
|
-
this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, } = WILDCARD_TARGET) => {
|
|
21869
|
+
this.disable = ({ backtest: bt$1 = false, breakeven = false, heat = false, live = false, partial = false, performance = false, risk = false, schedule = false, walker = false, strategy = false, } = WILDCARD_TARGET) => {
|
|
21612
21870
|
bt.loggerService.debug(REPORT_UTILS_METHOD_NAME_DISABLE, {
|
|
21613
21871
|
backtest: bt$1,
|
|
21614
21872
|
breakeven,
|
|
@@ -21619,6 +21877,7 @@ class ReportUtils {
|
|
|
21619
21877
|
risk,
|
|
21620
21878
|
schedule,
|
|
21621
21879
|
walker,
|
|
21880
|
+
strategy,
|
|
21622
21881
|
});
|
|
21623
21882
|
if (bt$1) {
|
|
21624
21883
|
bt.backtestReportService.unsubscribe();
|
|
@@ -21647,6 +21906,9 @@ class ReportUtils {
|
|
|
21647
21906
|
if (walker) {
|
|
21648
21907
|
bt.walkerReportService.unsubscribe();
|
|
21649
21908
|
}
|
|
21909
|
+
if (strategy) {
|
|
21910
|
+
bt.strategyReportService.unsubscribe();
|
|
21911
|
+
}
|
|
21650
21912
|
};
|
|
21651
21913
|
}
|
|
21652
21914
|
}
|
|
@@ -23082,22 +23344,1243 @@ class RiskReportService {
|
|
|
23082
23344
|
}
|
|
23083
23345
|
}
|
|
23084
23346
|
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
|
|
23092
|
-
{
|
|
23093
|
-
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23347
|
+
/**
|
|
23348
|
+
* Extracts execution context timestamp for strategy event logging.
|
|
23349
|
+
*
|
|
23350
|
+
* @param self - The StrategyReportService instance to extract context from
|
|
23351
|
+
* @returns Object containing ISO 8601 formatted timestamp, or empty string if no context
|
|
23352
|
+
* @internal
|
|
23353
|
+
*/
|
|
23354
|
+
const GET_EXECUTION_CONTEXT_FN$1 = (self) => {
|
|
23355
|
+
if (ExecutionContextService.hasContext()) {
|
|
23356
|
+
const { when } = self.executionContextService.context;
|
|
23357
|
+
return { when: when.toISOString() };
|
|
23358
|
+
}
|
|
23359
|
+
return {
|
|
23360
|
+
when: "",
|
|
23361
|
+
};
|
|
23362
|
+
};
|
|
23363
|
+
/**
|
|
23364
|
+
* Service for persisting strategy management events to JSON report files.
|
|
23365
|
+
*
|
|
23366
|
+
* Handles logging of strategy actions (cancel-scheduled, close-pending, partial-profit,
|
|
23367
|
+
* partial-loss, trailing-stop, trailing-take, breakeven) to persistent storage via
|
|
23368
|
+
* the Report class. Each event is written as a separate JSON record.
|
|
23369
|
+
*
|
|
23370
|
+
* Unlike StrategyMarkdownService which accumulates events in memory for markdown reports,
|
|
23371
|
+
* this service writes each event immediately to disk for audit trail purposes.
|
|
23372
|
+
*
|
|
23373
|
+
* Lifecycle:
|
|
23374
|
+
* - Call subscribe() to enable event logging
|
|
23375
|
+
* - Events are written via Report.writeData() with "strategy" category
|
|
23376
|
+
* - Call unsubscribe() to disable event logging
|
|
23377
|
+
*
|
|
23378
|
+
* @example
|
|
23379
|
+
* ```typescript
|
|
23380
|
+
* // Service is typically used internally by strategy management classes
|
|
23381
|
+
* strategyReportService.subscribe();
|
|
23382
|
+
*
|
|
23383
|
+
* // Events are logged automatically when strategy actions occur
|
|
23384
|
+
* await strategyReportService.partialProfit("BTCUSDT", 50, 50100, false, {
|
|
23385
|
+
* strategyName: "my-strategy",
|
|
23386
|
+
* exchangeName: "binance",
|
|
23387
|
+
* frameName: "1h"
|
|
23388
|
+
* });
|
|
23389
|
+
*
|
|
23390
|
+
* strategyReportService.unsubscribe();
|
|
23391
|
+
* ```
|
|
23392
|
+
*
|
|
23393
|
+
* @see StrategyMarkdownService for in-memory event accumulation and markdown report generation
|
|
23394
|
+
* @see Report for the underlying persistence mechanism
|
|
23395
|
+
*/
|
|
23396
|
+
class StrategyReportService {
|
|
23397
|
+
constructor() {
|
|
23398
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
23399
|
+
this.executionContextService = inject(TYPES.executionContextService);
|
|
23400
|
+
this.strategyCoreService = inject(TYPES.strategyCoreService);
|
|
23401
|
+
/**
|
|
23402
|
+
* Logs a cancel-scheduled event when a scheduled signal is cancelled.
|
|
23403
|
+
*
|
|
23404
|
+
* Retrieves the scheduled signal from StrategyCoreService and writes
|
|
23405
|
+
* the cancellation event to the report file.
|
|
23406
|
+
*
|
|
23407
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23408
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23409
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23410
|
+
* @param cancelId - Optional identifier for the cancellation reason
|
|
23411
|
+
*/
|
|
23412
|
+
this.cancelScheduled = async (symbol, isBacktest, context, cancelId) => {
|
|
23413
|
+
this.loggerService.log("strategyReportService cancelScheduled", {
|
|
23414
|
+
symbol,
|
|
23415
|
+
isBacktest,
|
|
23416
|
+
cancelId,
|
|
23417
|
+
});
|
|
23418
|
+
if (!this.subscribe.hasValue()) {
|
|
23419
|
+
return;
|
|
23420
|
+
}
|
|
23421
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23422
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
23423
|
+
exchangeName: context.exchangeName,
|
|
23424
|
+
strategyName: context.strategyName,
|
|
23425
|
+
frameName: context.frameName,
|
|
23426
|
+
});
|
|
23427
|
+
if (!scheduledRow) {
|
|
23428
|
+
return;
|
|
23429
|
+
}
|
|
23430
|
+
await Report.writeData("strategy", {
|
|
23431
|
+
action: "cancel-scheduled",
|
|
23432
|
+
cancelId,
|
|
23433
|
+
symbol,
|
|
23434
|
+
createdAt,
|
|
23435
|
+
}, {
|
|
23436
|
+
signalId: scheduledRow.id,
|
|
23437
|
+
exchangeName: context.exchangeName,
|
|
23438
|
+
frameName: context.frameName,
|
|
23439
|
+
strategyName: context.strategyName,
|
|
23440
|
+
symbol,
|
|
23441
|
+
walkerName: "",
|
|
23442
|
+
});
|
|
23443
|
+
};
|
|
23444
|
+
/**
|
|
23445
|
+
* Logs a close-pending event when a pending signal is closed.
|
|
23446
|
+
*
|
|
23447
|
+
* Retrieves the pending signal from StrategyCoreService and writes
|
|
23448
|
+
* the close event to the report file.
|
|
23449
|
+
*
|
|
23450
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23451
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23452
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23453
|
+
* @param closeId - Optional identifier for the close reason
|
|
23454
|
+
*/
|
|
23455
|
+
this.closePending = async (symbol, isBacktest, context, closeId) => {
|
|
23456
|
+
this.loggerService.log("strategyReportService closePending", {
|
|
23457
|
+
symbol,
|
|
23458
|
+
isBacktest,
|
|
23459
|
+
closeId,
|
|
23460
|
+
});
|
|
23461
|
+
if (!this.subscribe.hasValue()) {
|
|
23462
|
+
return;
|
|
23463
|
+
}
|
|
23464
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23465
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23466
|
+
exchangeName: context.exchangeName,
|
|
23467
|
+
strategyName: context.strategyName,
|
|
23468
|
+
frameName: context.frameName,
|
|
23469
|
+
});
|
|
23470
|
+
if (!pendingRow) {
|
|
23471
|
+
return;
|
|
23472
|
+
}
|
|
23473
|
+
await Report.writeData("strategy", {
|
|
23474
|
+
action: "close-pending",
|
|
23475
|
+
closeId,
|
|
23476
|
+
symbol,
|
|
23477
|
+
createdAt,
|
|
23478
|
+
}, {
|
|
23479
|
+
signalId: pendingRow.id,
|
|
23480
|
+
exchangeName: context.exchangeName,
|
|
23481
|
+
frameName: context.frameName,
|
|
23482
|
+
strategyName: context.strategyName,
|
|
23483
|
+
symbol,
|
|
23484
|
+
walkerName: "",
|
|
23485
|
+
});
|
|
23486
|
+
};
|
|
23487
|
+
/**
|
|
23488
|
+
* Logs a partial-profit event when a portion of the position is closed at profit.
|
|
23489
|
+
*
|
|
23490
|
+
* Records the percentage closed and current price when partial profit-taking occurs.
|
|
23491
|
+
*
|
|
23492
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23493
|
+
* @param percentToClose - Percentage of position to close (0-100)
|
|
23494
|
+
* @param currentPrice - Current market price at time of partial close
|
|
23495
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23496
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23497
|
+
*/
|
|
23498
|
+
this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context) => {
|
|
23499
|
+
this.loggerService.log("strategyReportService partialProfit", {
|
|
23500
|
+
symbol,
|
|
23501
|
+
percentToClose,
|
|
23502
|
+
currentPrice,
|
|
23503
|
+
isBacktest,
|
|
23504
|
+
});
|
|
23505
|
+
if (!this.subscribe.hasValue()) {
|
|
23506
|
+
return;
|
|
23507
|
+
}
|
|
23508
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23509
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23510
|
+
exchangeName: context.exchangeName,
|
|
23511
|
+
strategyName: context.strategyName,
|
|
23512
|
+
frameName: context.frameName,
|
|
23513
|
+
});
|
|
23514
|
+
if (!pendingRow) {
|
|
23515
|
+
return;
|
|
23516
|
+
}
|
|
23517
|
+
await Report.writeData("strategy", {
|
|
23518
|
+
action: "partial-profit",
|
|
23519
|
+
percentToClose,
|
|
23520
|
+
currentPrice,
|
|
23521
|
+
symbol,
|
|
23522
|
+
createdAt,
|
|
23523
|
+
}, {
|
|
23524
|
+
signalId: pendingRow.id,
|
|
23525
|
+
exchangeName: context.exchangeName,
|
|
23526
|
+
frameName: context.frameName,
|
|
23527
|
+
strategyName: context.strategyName,
|
|
23528
|
+
symbol,
|
|
23529
|
+
walkerName: "",
|
|
23530
|
+
});
|
|
23531
|
+
};
|
|
23532
|
+
/**
|
|
23533
|
+
* Logs a partial-loss event when a portion of the position is closed at loss.
|
|
23534
|
+
*
|
|
23535
|
+
* Records the percentage closed and current price when partial loss-cutting occurs.
|
|
23536
|
+
*
|
|
23537
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23538
|
+
* @param percentToClose - Percentage of position to close (0-100)
|
|
23539
|
+
* @param currentPrice - Current market price at time of partial close
|
|
23540
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23541
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23542
|
+
*/
|
|
23543
|
+
this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context) => {
|
|
23544
|
+
this.loggerService.log("strategyReportService partialLoss", {
|
|
23545
|
+
symbol,
|
|
23546
|
+
percentToClose,
|
|
23547
|
+
currentPrice,
|
|
23548
|
+
isBacktest,
|
|
23549
|
+
});
|
|
23550
|
+
if (!this.subscribe.hasValue()) {
|
|
23551
|
+
return;
|
|
23552
|
+
}
|
|
23553
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23554
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23555
|
+
exchangeName: context.exchangeName,
|
|
23556
|
+
strategyName: context.strategyName,
|
|
23557
|
+
frameName: context.frameName,
|
|
23558
|
+
});
|
|
23559
|
+
if (!pendingRow) {
|
|
23560
|
+
return;
|
|
23561
|
+
}
|
|
23562
|
+
await Report.writeData("strategy", {
|
|
23563
|
+
action: "partial-loss",
|
|
23564
|
+
percentToClose,
|
|
23565
|
+
currentPrice,
|
|
23566
|
+
symbol,
|
|
23567
|
+
createdAt,
|
|
23568
|
+
}, {
|
|
23569
|
+
signalId: pendingRow.id,
|
|
23570
|
+
exchangeName: context.exchangeName,
|
|
23571
|
+
frameName: context.frameName,
|
|
23572
|
+
strategyName: context.strategyName,
|
|
23573
|
+
symbol,
|
|
23574
|
+
walkerName: "",
|
|
23575
|
+
});
|
|
23576
|
+
};
|
|
23577
|
+
/**
|
|
23578
|
+
* Logs a trailing-stop event when the stop-loss is adjusted.
|
|
23579
|
+
*
|
|
23580
|
+
* Records the percentage shift and current price when trailing stop moves.
|
|
23581
|
+
*
|
|
23582
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23583
|
+
* @param percentShift - Percentage the stop-loss was shifted
|
|
23584
|
+
* @param currentPrice - Current market price at time of adjustment
|
|
23585
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23586
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23587
|
+
*/
|
|
23588
|
+
this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context) => {
|
|
23589
|
+
this.loggerService.log("strategyReportService trailingStop", {
|
|
23590
|
+
symbol,
|
|
23591
|
+
percentShift,
|
|
23592
|
+
currentPrice,
|
|
23593
|
+
isBacktest,
|
|
23594
|
+
});
|
|
23595
|
+
if (!this.subscribe.hasValue()) {
|
|
23596
|
+
return;
|
|
23597
|
+
}
|
|
23598
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23599
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23600
|
+
exchangeName: context.exchangeName,
|
|
23601
|
+
strategyName: context.strategyName,
|
|
23602
|
+
frameName: context.frameName,
|
|
23603
|
+
});
|
|
23604
|
+
if (!pendingRow) {
|
|
23605
|
+
return;
|
|
23606
|
+
}
|
|
23607
|
+
await Report.writeData("strategy", {
|
|
23608
|
+
action: "trailing-stop",
|
|
23609
|
+
percentShift,
|
|
23610
|
+
currentPrice,
|
|
23611
|
+
symbol,
|
|
23612
|
+
createdAt,
|
|
23613
|
+
}, {
|
|
23614
|
+
signalId: pendingRow.id,
|
|
23615
|
+
exchangeName: context.exchangeName,
|
|
23616
|
+
frameName: context.frameName,
|
|
23617
|
+
strategyName: context.strategyName,
|
|
23618
|
+
symbol,
|
|
23619
|
+
walkerName: "",
|
|
23620
|
+
});
|
|
23621
|
+
};
|
|
23622
|
+
/**
|
|
23623
|
+
* Logs a trailing-take event when the take-profit is adjusted.
|
|
23624
|
+
*
|
|
23625
|
+
* Records the percentage shift and current price when trailing take-profit moves.
|
|
23626
|
+
*
|
|
23627
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23628
|
+
* @param percentShift - Percentage the take-profit was shifted
|
|
23629
|
+
* @param currentPrice - Current market price at time of adjustment
|
|
23630
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23631
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23632
|
+
*/
|
|
23633
|
+
this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context) => {
|
|
23634
|
+
this.loggerService.log("strategyReportService trailingTake", {
|
|
23635
|
+
symbol,
|
|
23636
|
+
percentShift,
|
|
23637
|
+
currentPrice,
|
|
23638
|
+
isBacktest,
|
|
23639
|
+
});
|
|
23640
|
+
if (!this.subscribe.hasValue()) {
|
|
23641
|
+
return;
|
|
23642
|
+
}
|
|
23643
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23644
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23645
|
+
exchangeName: context.exchangeName,
|
|
23646
|
+
strategyName: context.strategyName,
|
|
23647
|
+
frameName: context.frameName,
|
|
23648
|
+
});
|
|
23649
|
+
if (!pendingRow) {
|
|
23650
|
+
return;
|
|
23651
|
+
}
|
|
23652
|
+
await Report.writeData("strategy", {
|
|
23653
|
+
action: "trailing-take",
|
|
23654
|
+
percentShift,
|
|
23655
|
+
currentPrice,
|
|
23656
|
+
symbol,
|
|
23657
|
+
createdAt,
|
|
23658
|
+
}, {
|
|
23659
|
+
signalId: pendingRow.id,
|
|
23660
|
+
exchangeName: context.exchangeName,
|
|
23661
|
+
frameName: context.frameName,
|
|
23662
|
+
strategyName: context.strategyName,
|
|
23663
|
+
symbol,
|
|
23664
|
+
walkerName: "",
|
|
23665
|
+
});
|
|
23666
|
+
};
|
|
23667
|
+
/**
|
|
23668
|
+
* Logs a breakeven event when the stop-loss is moved to entry price.
|
|
23669
|
+
*
|
|
23670
|
+
* Records the current price when breakeven protection is activated.
|
|
23671
|
+
*
|
|
23672
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23673
|
+
* @param currentPrice - Current market price at time of breakeven activation
|
|
23674
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
23675
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
23676
|
+
*/
|
|
23677
|
+
this.breakeven = async (symbol, currentPrice, isBacktest, context) => {
|
|
23678
|
+
this.loggerService.log("strategyReportService breakeven", {
|
|
23679
|
+
symbol,
|
|
23680
|
+
currentPrice,
|
|
23681
|
+
isBacktest,
|
|
23682
|
+
});
|
|
23683
|
+
if (!this.subscribe.hasValue()) {
|
|
23684
|
+
return;
|
|
23685
|
+
}
|
|
23686
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN$1(this);
|
|
23687
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
23688
|
+
exchangeName: context.exchangeName,
|
|
23689
|
+
strategyName: context.strategyName,
|
|
23690
|
+
frameName: context.frameName,
|
|
23691
|
+
});
|
|
23692
|
+
if (!pendingRow) {
|
|
23693
|
+
return;
|
|
23694
|
+
}
|
|
23695
|
+
await Report.writeData("strategy", {
|
|
23696
|
+
action: "breakeven",
|
|
23697
|
+
currentPrice,
|
|
23698
|
+
symbol,
|
|
23699
|
+
createdAt,
|
|
23700
|
+
}, {
|
|
23701
|
+
signalId: pendingRow.id,
|
|
23702
|
+
exchangeName: context.exchangeName,
|
|
23703
|
+
frameName: context.frameName,
|
|
23704
|
+
strategyName: context.strategyName,
|
|
23705
|
+
symbol,
|
|
23706
|
+
walkerName: "",
|
|
23707
|
+
});
|
|
23708
|
+
};
|
|
23709
|
+
/**
|
|
23710
|
+
* Initializes the service for event logging.
|
|
23711
|
+
*
|
|
23712
|
+
* Must be called before any events can be logged. Uses singleshot pattern
|
|
23713
|
+
* to ensure only one subscription exists at a time.
|
|
23714
|
+
*
|
|
23715
|
+
* @returns Cleanup function that clears the subscription when called
|
|
23716
|
+
*/
|
|
23717
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
23718
|
+
this.loggerService.log("strategyReportService subscribe");
|
|
23719
|
+
const unCancelSchedule = strategyCommitSubject
|
|
23720
|
+
.filter(({ action }) => action === "cancel-scheduled")
|
|
23721
|
+
.connect(async (event) => await this.cancelScheduled(event.symbol, event.backtest, {
|
|
23722
|
+
exchangeName: event.exchangeName,
|
|
23723
|
+
frameName: event.frameName,
|
|
23724
|
+
strategyName: event.strategyName,
|
|
23725
|
+
}, event.cancelId));
|
|
23726
|
+
const unClosePending = strategyCommitSubject
|
|
23727
|
+
.filter(({ action }) => action === "close-pending")
|
|
23728
|
+
.connect(async (event) => await this.closePending(event.symbol, event.backtest, {
|
|
23729
|
+
exchangeName: event.exchangeName,
|
|
23730
|
+
frameName: event.frameName,
|
|
23731
|
+
strategyName: event.strategyName,
|
|
23732
|
+
}, event.closeId));
|
|
23733
|
+
const unPartialProfit = strategyCommitSubject
|
|
23734
|
+
.filter(({ action }) => action === "partial-profit")
|
|
23735
|
+
.connect(async (event) => await this.partialProfit(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
|
|
23736
|
+
exchangeName: event.exchangeName,
|
|
23737
|
+
frameName: event.frameName,
|
|
23738
|
+
strategyName: event.strategyName,
|
|
23739
|
+
}));
|
|
23740
|
+
const unPartialLoss = strategyCommitSubject
|
|
23741
|
+
.filter(({ action }) => action === "partial-loss")
|
|
23742
|
+
.connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
|
|
23743
|
+
exchangeName: event.exchangeName,
|
|
23744
|
+
frameName: event.frameName,
|
|
23745
|
+
strategyName: event.strategyName,
|
|
23746
|
+
}));
|
|
23747
|
+
const unTrailingStop = strategyCommitSubject
|
|
23748
|
+
.filter(({ action }) => action === "trailing-stop")
|
|
23749
|
+
.connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
|
|
23750
|
+
exchangeName: event.exchangeName,
|
|
23751
|
+
frameName: event.frameName,
|
|
23752
|
+
strategyName: event.strategyName,
|
|
23753
|
+
}));
|
|
23754
|
+
const unTrailingTake = strategyCommitSubject
|
|
23755
|
+
.filter(({ action }) => action === "trailing-take")
|
|
23756
|
+
.connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
|
|
23757
|
+
exchangeName: event.exchangeName,
|
|
23758
|
+
frameName: event.frameName,
|
|
23759
|
+
strategyName: event.strategyName,
|
|
23760
|
+
}));
|
|
23761
|
+
const unBreakeven = strategyCommitSubject
|
|
23762
|
+
.filter(({ action }) => action === "breakeven")
|
|
23763
|
+
.connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
|
|
23764
|
+
exchangeName: event.exchangeName,
|
|
23765
|
+
frameName: event.frameName,
|
|
23766
|
+
strategyName: event.strategyName,
|
|
23767
|
+
}));
|
|
23768
|
+
const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven());
|
|
23769
|
+
return () => {
|
|
23770
|
+
disposeFn();
|
|
23771
|
+
this.subscribe.clear();
|
|
23772
|
+
};
|
|
23773
|
+
});
|
|
23774
|
+
/**
|
|
23775
|
+
* Stops event logging and cleans up the subscription.
|
|
23776
|
+
*
|
|
23777
|
+
* Safe to call multiple times - only clears if subscription exists.
|
|
23778
|
+
*/
|
|
23779
|
+
this.unsubscribe = async () => {
|
|
23780
|
+
this.loggerService.log("strategyReportService unsubscribe");
|
|
23781
|
+
if (this.subscribe.hasValue()) {
|
|
23782
|
+
const lastSubscription = this.subscribe();
|
|
23783
|
+
lastSubscription();
|
|
23784
|
+
}
|
|
23785
|
+
};
|
|
23786
|
+
}
|
|
23787
|
+
}
|
|
23788
|
+
|
|
23789
|
+
/**
|
|
23790
|
+
* Extracts execution context timestamp for strategy event logging.
|
|
23791
|
+
*
|
|
23792
|
+
* @param self - The StrategyMarkdownService instance to extract context from
|
|
23793
|
+
* @returns Object containing ISO 8601 formatted timestamp, or empty string if no context
|
|
23794
|
+
* @internal
|
|
23795
|
+
*/
|
|
23796
|
+
const GET_EXECUTION_CONTEXT_FN = (self) => {
|
|
23797
|
+
if (ExecutionContextService.hasContext()) {
|
|
23798
|
+
const { when } = self.executionContextService.context;
|
|
23799
|
+
return { when: when.toISOString() };
|
|
23800
|
+
}
|
|
23801
|
+
return {
|
|
23802
|
+
when: "",
|
|
23803
|
+
};
|
|
23804
|
+
};
|
|
23805
|
+
/**
|
|
23806
|
+
* Creates a unique key for memoizing ReportStorage instances.
|
|
23807
|
+
*
|
|
23808
|
+
* Key format: `{symbol}:{strategyName}:{exchangeName}[:{frameName}]:{backtest|live}`
|
|
23809
|
+
*
|
|
23810
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23811
|
+
* @param strategyName - Name of the trading strategy
|
|
23812
|
+
* @param exchangeName - Name of the exchange
|
|
23813
|
+
* @param frameName - Timeframe name (optional, included if present)
|
|
23814
|
+
* @param backtest - Whether this is backtest or live mode
|
|
23815
|
+
* @returns Colon-separated key string for memoization
|
|
23816
|
+
* @internal
|
|
23817
|
+
*/
|
|
23818
|
+
const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
23819
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
23820
|
+
if (frameName)
|
|
23821
|
+
parts.push(frameName);
|
|
23822
|
+
parts.push(backtest ? "backtest" : "live");
|
|
23823
|
+
return parts.join(":");
|
|
23824
|
+
};
|
|
23825
|
+
/**
|
|
23826
|
+
* Creates a filename for markdown report output.
|
|
23827
|
+
*
|
|
23828
|
+
* Filename format: `{symbol}_{strategyName}_{exchangeName}[_{frameName}_backtest|_live]-{timestamp}.md`
|
|
23829
|
+
*
|
|
23830
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23831
|
+
* @param strategyName - Name of the trading strategy
|
|
23832
|
+
* @param exchangeName - Name of the exchange
|
|
23833
|
+
* @param frameName - Timeframe name (indicates backtest mode if present)
|
|
23834
|
+
* @param timestamp - Unix timestamp in milliseconds for uniqueness
|
|
23835
|
+
* @returns Underscore-separated filename with .md extension
|
|
23836
|
+
* @internal
|
|
23837
|
+
*/
|
|
23838
|
+
const CREATE_FILE_NAME_FN = (symbol, strategyName, exchangeName, frameName, timestamp) => {
|
|
23839
|
+
const parts = [symbol, strategyName, exchangeName];
|
|
23840
|
+
if (frameName) {
|
|
23841
|
+
parts.push(frameName);
|
|
23842
|
+
parts.push("backtest");
|
|
23843
|
+
}
|
|
23844
|
+
else
|
|
23845
|
+
parts.push("live");
|
|
23846
|
+
return `${parts.join("_")}-${timestamp}.md`;
|
|
23847
|
+
};
|
|
23848
|
+
/**
|
|
23849
|
+
* Maximum number of events to store per symbol-strategy pair.
|
|
23850
|
+
* Older events are discarded when this limit is exceeded.
|
|
23851
|
+
* @internal
|
|
23852
|
+
*/
|
|
23853
|
+
const MAX_EVENTS = 250;
|
|
23854
|
+
/**
|
|
23855
|
+
* In-memory storage for accumulating strategy events per symbol-strategy pair.
|
|
23856
|
+
*
|
|
23857
|
+
* Maintains a rolling window of the most recent events (up to MAX_EVENTS),
|
|
23858
|
+
* with newer events added to the front of the list. Provides methods to:
|
|
23859
|
+
* - Add new events (FIFO queue with max size)
|
|
23860
|
+
* - Retrieve aggregated statistics
|
|
23861
|
+
* - Generate markdown reports
|
|
23862
|
+
* - Dump reports to disk
|
|
23863
|
+
*
|
|
23864
|
+
* @internal
|
|
23865
|
+
*/
|
|
23866
|
+
class ReportStorage {
|
|
23867
|
+
/**
|
|
23868
|
+
* Creates a new ReportStorage instance.
|
|
23869
|
+
*
|
|
23870
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
23871
|
+
* @param strategyName - Name of the trading strategy
|
|
23872
|
+
* @param exchangeName - Name of the exchange
|
|
23873
|
+
* @param frameName - Timeframe name for backtest identification
|
|
23874
|
+
*/
|
|
23875
|
+
constructor(symbol, strategyName, exchangeName, frameName) {
|
|
23876
|
+
this.symbol = symbol;
|
|
23877
|
+
this.strategyName = strategyName;
|
|
23878
|
+
this.exchangeName = exchangeName;
|
|
23879
|
+
this.frameName = frameName;
|
|
23880
|
+
this._eventList = [];
|
|
23881
|
+
}
|
|
23882
|
+
/**
|
|
23883
|
+
* Adds a new event to the storage.
|
|
23884
|
+
*
|
|
23885
|
+
* Events are added to the front of the list (most recent first).
|
|
23886
|
+
* If the list exceeds MAX_EVENTS, the oldest event is removed.
|
|
23887
|
+
*
|
|
23888
|
+
* @param event - The strategy event to store
|
|
23889
|
+
*/
|
|
23890
|
+
addEvent(event) {
|
|
23891
|
+
this._eventList.unshift(event);
|
|
23892
|
+
if (this._eventList.length > MAX_EVENTS) {
|
|
23893
|
+
this._eventList.pop();
|
|
23894
|
+
}
|
|
23895
|
+
}
|
|
23896
|
+
/**
|
|
23897
|
+
* Retrieves aggregated statistics from stored events.
|
|
23898
|
+
*
|
|
23899
|
+
* Calculates counts for each action type from the event list.
|
|
23900
|
+
*
|
|
23901
|
+
* @returns Promise resolving to StrategyStatisticsModel with event list and counts
|
|
23902
|
+
*/
|
|
23903
|
+
async getData() {
|
|
23904
|
+
if (this._eventList.length === 0) {
|
|
23905
|
+
return {
|
|
23906
|
+
eventList: [],
|
|
23907
|
+
totalEvents: 0,
|
|
23908
|
+
cancelScheduledCount: 0,
|
|
23909
|
+
closePendingCount: 0,
|
|
23910
|
+
partialProfitCount: 0,
|
|
23911
|
+
partialLossCount: 0,
|
|
23912
|
+
trailingStopCount: 0,
|
|
23913
|
+
trailingTakeCount: 0,
|
|
23914
|
+
breakevenCount: 0,
|
|
23915
|
+
};
|
|
23916
|
+
}
|
|
23917
|
+
return {
|
|
23918
|
+
eventList: this._eventList,
|
|
23919
|
+
totalEvents: this._eventList.length,
|
|
23920
|
+
cancelScheduledCount: this._eventList.filter(e => e.action === "cancel-scheduled").length,
|
|
23921
|
+
closePendingCount: this._eventList.filter(e => e.action === "close-pending").length,
|
|
23922
|
+
partialProfitCount: this._eventList.filter(e => e.action === "partial-profit").length,
|
|
23923
|
+
partialLossCount: this._eventList.filter(e => e.action === "partial-loss").length,
|
|
23924
|
+
trailingStopCount: this._eventList.filter(e => e.action === "trailing-stop").length,
|
|
23925
|
+
trailingTakeCount: this._eventList.filter(e => e.action === "trailing-take").length,
|
|
23926
|
+
breakevenCount: this._eventList.filter(e => e.action === "breakeven").length,
|
|
23927
|
+
};
|
|
23928
|
+
}
|
|
23929
|
+
/**
|
|
23930
|
+
* Generates a markdown report from stored events.
|
|
23931
|
+
*
|
|
23932
|
+
* Creates a formatted markdown document containing:
|
|
23933
|
+
* - Header with symbol and strategy name
|
|
23934
|
+
* - Table of all events with configurable columns
|
|
23935
|
+
* - Summary statistics with counts by action type
|
|
23936
|
+
*
|
|
23937
|
+
* @param symbol - Trading pair symbol for report header
|
|
23938
|
+
* @param strategyName - Strategy name for report header
|
|
23939
|
+
* @param columns - Column configuration for the event table
|
|
23940
|
+
* @returns Promise resolving to formatted markdown string
|
|
23941
|
+
*/
|
|
23942
|
+
async getReport(symbol, strategyName, columns = COLUMN_CONFIG.strategy_columns) {
|
|
23943
|
+
const stats = await this.getData();
|
|
23944
|
+
if (stats.totalEvents === 0) {
|
|
23945
|
+
return [
|
|
23946
|
+
`# Strategy Report: ${symbol}:${strategyName}`,
|
|
23947
|
+
"",
|
|
23948
|
+
"No strategy events recorded yet."
|
|
23949
|
+
].join("\n");
|
|
23950
|
+
}
|
|
23951
|
+
const visibleColumns = [];
|
|
23952
|
+
for (const col of columns) {
|
|
23953
|
+
if (await col.isVisible()) {
|
|
23954
|
+
visibleColumns.push(col);
|
|
23955
|
+
}
|
|
23956
|
+
}
|
|
23957
|
+
const header = visibleColumns.map((col) => col.label);
|
|
23958
|
+
const separator = visibleColumns.map(() => "---");
|
|
23959
|
+
const rows = await Promise.all(this._eventList.map(async (event, index) => Promise.all(visibleColumns.map((col) => col.format(event, index)))));
|
|
23960
|
+
const tableData = [header, separator, ...rows];
|
|
23961
|
+
const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
23962
|
+
return [
|
|
23963
|
+
`# Strategy Report: ${symbol}:${strategyName}`,
|
|
23964
|
+
"",
|
|
23965
|
+
table,
|
|
23966
|
+
"",
|
|
23967
|
+
`**Total events:** ${stats.totalEvents}`,
|
|
23968
|
+
`- Cancel scheduled: ${stats.cancelScheduledCount}`,
|
|
23969
|
+
`- Close pending: ${stats.closePendingCount}`,
|
|
23970
|
+
`- Partial profit: ${stats.partialProfitCount}`,
|
|
23971
|
+
`- Partial loss: ${stats.partialLossCount}`,
|
|
23972
|
+
`- Trailing stop: ${stats.trailingStopCount}`,
|
|
23973
|
+
`- Trailing take: ${stats.trailingTakeCount}`,
|
|
23974
|
+
`- Breakeven: ${stats.breakevenCount}`,
|
|
23975
|
+
].join("\n");
|
|
23976
|
+
}
|
|
23977
|
+
/**
|
|
23978
|
+
* Generates and saves a markdown report to disk.
|
|
23979
|
+
*
|
|
23980
|
+
* Creates the output directory if it doesn't exist and writes
|
|
23981
|
+
* the report with a timestamped filename.
|
|
23982
|
+
*
|
|
23983
|
+
* @param symbol - Trading pair symbol for report
|
|
23984
|
+
* @param strategyName - Strategy name for report
|
|
23985
|
+
* @param path - Output directory path (default: "./dump/strategy")
|
|
23986
|
+
* @param columns - Column configuration for the event table
|
|
23987
|
+
*/
|
|
23988
|
+
async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
|
|
23989
|
+
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
23990
|
+
const timestamp = Date.now();
|
|
23991
|
+
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
23992
|
+
await Markdown.writeData("strategy", markdown, {
|
|
23993
|
+
path,
|
|
23994
|
+
file: filename,
|
|
23995
|
+
symbol: this.symbol,
|
|
23996
|
+
strategyName: this.strategyName,
|
|
23997
|
+
exchangeName: this.exchangeName,
|
|
23998
|
+
signalId: "",
|
|
23999
|
+
frameName: this.frameName
|
|
24000
|
+
});
|
|
24001
|
+
}
|
|
24002
|
+
}
|
|
24003
|
+
/**
|
|
24004
|
+
* Service for accumulating strategy management events and generating markdown reports.
|
|
24005
|
+
*
|
|
24006
|
+
* Collects strategy actions (cancel-scheduled, close-pending, partial-profit,
|
|
24007
|
+
* partial-loss, trailing-stop, trailing-take, breakeven) in memory and provides
|
|
24008
|
+
* methods to retrieve statistics, generate reports, and export to files.
|
|
24009
|
+
*
|
|
24010
|
+
* Unlike StrategyReportService which writes each event to disk immediately,
|
|
24011
|
+
* this service accumulates events in ReportStorage instances (max 250 per
|
|
24012
|
+
* symbol-strategy pair) for batch reporting.
|
|
24013
|
+
*
|
|
24014
|
+
* Features:
|
|
24015
|
+
* - In-memory event accumulation with memoized storage per symbol-strategy pair
|
|
24016
|
+
* - Statistical data extraction (event counts by action type)
|
|
24017
|
+
* - Markdown report generation with configurable columns
|
|
24018
|
+
* - File export with timestamped filenames
|
|
24019
|
+
* - Selective or full cache clearing
|
|
24020
|
+
*
|
|
24021
|
+
* Lifecycle:
|
|
24022
|
+
* - Call subscribe() to enable event collection
|
|
24023
|
+
* - Events are collected automatically via cancelScheduled, closePending, etc.
|
|
24024
|
+
* - Use getData(), getReport(), or dump() to retrieve accumulated data
|
|
24025
|
+
* - Call unsubscribe() to disable collection and clear all data
|
|
24026
|
+
*
|
|
24027
|
+
* @example
|
|
24028
|
+
* ```typescript
|
|
24029
|
+
* strategyMarkdownService.subscribe();
|
|
24030
|
+
*
|
|
24031
|
+
* // Events are collected automatically during strategy execution
|
|
24032
|
+
* // ...
|
|
24033
|
+
*
|
|
24034
|
+
* // Get statistics
|
|
24035
|
+
* const stats = await strategyMarkdownService.getData("BTCUSDT", "my-strategy", "binance", "1h", true);
|
|
24036
|
+
*
|
|
24037
|
+
* // Generate markdown report
|
|
24038
|
+
* const report = await strategyMarkdownService.getReport("BTCUSDT", "my-strategy", "binance", "1h", true);
|
|
24039
|
+
*
|
|
24040
|
+
* // Export to file
|
|
24041
|
+
* await strategyMarkdownService.dump("BTCUSDT", "my-strategy", "binance", "1h", true);
|
|
24042
|
+
*
|
|
24043
|
+
* strategyMarkdownService.unsubscribe();
|
|
24044
|
+
* ```
|
|
24045
|
+
*
|
|
24046
|
+
* @see StrategyReportService for immediate event persistence to JSON files
|
|
24047
|
+
* @see Strategy for the high-level utility class that wraps this service
|
|
24048
|
+
*/
|
|
24049
|
+
class StrategyMarkdownService {
|
|
24050
|
+
constructor() {
|
|
24051
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
24052
|
+
this.executionContextService = inject(TYPES.executionContextService);
|
|
24053
|
+
this.strategyCoreService = inject(TYPES.strategyCoreService);
|
|
24054
|
+
/**
|
|
24055
|
+
* Memoized factory for ReportStorage instances.
|
|
24056
|
+
*
|
|
24057
|
+
* Creates and caches ReportStorage per unique symbol-strategy-exchange-frame-backtest combination.
|
|
24058
|
+
* Uses CREATE_KEY_FN for cache key generation.
|
|
24059
|
+
*
|
|
24060
|
+
* @internal
|
|
24061
|
+
*/
|
|
24062
|
+
this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
|
|
24063
|
+
/**
|
|
24064
|
+
* Records a cancel-scheduled event when a scheduled signal is cancelled.
|
|
24065
|
+
*
|
|
24066
|
+
* Retrieves the scheduled signal from StrategyCoreService and stores
|
|
24067
|
+
* the cancellation event in the appropriate ReportStorage.
|
|
24068
|
+
*
|
|
24069
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24070
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24071
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24072
|
+
* @param cancelId - Optional identifier for the cancellation reason
|
|
24073
|
+
*/
|
|
24074
|
+
this.cancelScheduled = async (symbol, isBacktest, context, cancelId) => {
|
|
24075
|
+
this.loggerService.log("strategyMarkdownService cancelScheduled", {
|
|
24076
|
+
symbol,
|
|
24077
|
+
isBacktest,
|
|
24078
|
+
cancelId,
|
|
24079
|
+
});
|
|
24080
|
+
if (!this.subscribe.hasValue()) {
|
|
24081
|
+
return;
|
|
24082
|
+
}
|
|
24083
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24084
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
24085
|
+
exchangeName: context.exchangeName,
|
|
24086
|
+
strategyName: context.strategyName,
|
|
24087
|
+
frameName: context.frameName,
|
|
24088
|
+
});
|
|
24089
|
+
if (!scheduledRow) {
|
|
24090
|
+
return;
|
|
24091
|
+
}
|
|
24092
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24093
|
+
storage.addEvent({
|
|
24094
|
+
timestamp: Date.now(),
|
|
24095
|
+
symbol,
|
|
24096
|
+
strategyName: context.strategyName,
|
|
24097
|
+
exchangeName: context.exchangeName,
|
|
24098
|
+
frameName: context.frameName,
|
|
24099
|
+
signalId: scheduledRow.id,
|
|
24100
|
+
action: "cancel-scheduled",
|
|
24101
|
+
cancelId,
|
|
24102
|
+
createdAt,
|
|
24103
|
+
backtest: isBacktest,
|
|
24104
|
+
});
|
|
24105
|
+
};
|
|
24106
|
+
/**
|
|
24107
|
+
* Records a close-pending event when a pending signal is closed.
|
|
24108
|
+
*
|
|
24109
|
+
* Retrieves the pending signal from StrategyCoreService and stores
|
|
24110
|
+
* the close event in the appropriate ReportStorage.
|
|
24111
|
+
*
|
|
24112
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24113
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24114
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24115
|
+
* @param closeId - Optional identifier for the close reason
|
|
24116
|
+
*/
|
|
24117
|
+
this.closePending = async (symbol, isBacktest, context, closeId) => {
|
|
24118
|
+
this.loggerService.log("strategyMarkdownService closePending", {
|
|
24119
|
+
symbol,
|
|
24120
|
+
isBacktest,
|
|
24121
|
+
closeId,
|
|
24122
|
+
});
|
|
24123
|
+
if (!this.subscribe.hasValue()) {
|
|
24124
|
+
return;
|
|
24125
|
+
}
|
|
24126
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24127
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24128
|
+
exchangeName: context.exchangeName,
|
|
24129
|
+
strategyName: context.strategyName,
|
|
24130
|
+
frameName: context.frameName,
|
|
24131
|
+
});
|
|
24132
|
+
if (!pendingRow) {
|
|
24133
|
+
return;
|
|
24134
|
+
}
|
|
24135
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24136
|
+
storage.addEvent({
|
|
24137
|
+
timestamp: Date.now(),
|
|
24138
|
+
symbol,
|
|
24139
|
+
strategyName: context.strategyName,
|
|
24140
|
+
exchangeName: context.exchangeName,
|
|
24141
|
+
frameName: context.frameName,
|
|
24142
|
+
signalId: pendingRow.id,
|
|
24143
|
+
action: "close-pending",
|
|
24144
|
+
closeId,
|
|
24145
|
+
createdAt,
|
|
24146
|
+
backtest: isBacktest,
|
|
24147
|
+
});
|
|
24148
|
+
};
|
|
24149
|
+
/**
|
|
24150
|
+
* Records a partial-profit event when a portion of the position is closed at profit.
|
|
24151
|
+
*
|
|
24152
|
+
* Stores the percentage closed and current price when partial profit-taking occurs.
|
|
24153
|
+
*
|
|
24154
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24155
|
+
* @param percentToClose - Percentage of position to close (0-100)
|
|
24156
|
+
* @param currentPrice - Current market price at time of partial close
|
|
24157
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24158
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24159
|
+
*/
|
|
24160
|
+
this.partialProfit = async (symbol, percentToClose, currentPrice, isBacktest, context) => {
|
|
24161
|
+
this.loggerService.log("strategyMarkdownService partialProfit", {
|
|
24162
|
+
symbol,
|
|
24163
|
+
percentToClose,
|
|
24164
|
+
currentPrice,
|
|
24165
|
+
isBacktest,
|
|
24166
|
+
});
|
|
24167
|
+
if (!this.subscribe.hasValue()) {
|
|
24168
|
+
return;
|
|
24169
|
+
}
|
|
24170
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24171
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24172
|
+
exchangeName: context.exchangeName,
|
|
24173
|
+
strategyName: context.strategyName,
|
|
24174
|
+
frameName: context.frameName,
|
|
24175
|
+
});
|
|
24176
|
+
if (!pendingRow) {
|
|
24177
|
+
return;
|
|
24178
|
+
}
|
|
24179
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24180
|
+
storage.addEvent({
|
|
24181
|
+
timestamp: Date.now(),
|
|
24182
|
+
symbol,
|
|
24183
|
+
strategyName: context.strategyName,
|
|
24184
|
+
exchangeName: context.exchangeName,
|
|
24185
|
+
frameName: context.frameName,
|
|
24186
|
+
signalId: pendingRow.id,
|
|
24187
|
+
action: "partial-profit",
|
|
24188
|
+
percentToClose,
|
|
24189
|
+
currentPrice,
|
|
24190
|
+
createdAt,
|
|
24191
|
+
backtest: isBacktest,
|
|
24192
|
+
});
|
|
24193
|
+
};
|
|
24194
|
+
/**
|
|
24195
|
+
* Records a partial-loss event when a portion of the position is closed at loss.
|
|
24196
|
+
*
|
|
24197
|
+
* Stores the percentage closed and current price when partial loss-cutting occurs.
|
|
24198
|
+
*
|
|
24199
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24200
|
+
* @param percentToClose - Percentage of position to close (0-100)
|
|
24201
|
+
* @param currentPrice - Current market price at time of partial close
|
|
24202
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24203
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24204
|
+
*/
|
|
24205
|
+
this.partialLoss = async (symbol, percentToClose, currentPrice, isBacktest, context) => {
|
|
24206
|
+
this.loggerService.log("strategyMarkdownService partialLoss", {
|
|
24207
|
+
symbol,
|
|
24208
|
+
percentToClose,
|
|
24209
|
+
currentPrice,
|
|
24210
|
+
isBacktest,
|
|
24211
|
+
});
|
|
24212
|
+
if (!this.subscribe.hasValue()) {
|
|
24213
|
+
return;
|
|
24214
|
+
}
|
|
24215
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24216
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24217
|
+
exchangeName: context.exchangeName,
|
|
24218
|
+
strategyName: context.strategyName,
|
|
24219
|
+
frameName: context.frameName,
|
|
24220
|
+
});
|
|
24221
|
+
if (!pendingRow) {
|
|
24222
|
+
return;
|
|
24223
|
+
}
|
|
24224
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24225
|
+
storage.addEvent({
|
|
24226
|
+
timestamp: Date.now(),
|
|
24227
|
+
symbol,
|
|
24228
|
+
strategyName: context.strategyName,
|
|
24229
|
+
exchangeName: context.exchangeName,
|
|
24230
|
+
frameName: context.frameName,
|
|
24231
|
+
signalId: pendingRow.id,
|
|
24232
|
+
action: "partial-loss",
|
|
24233
|
+
percentToClose,
|
|
24234
|
+
currentPrice,
|
|
24235
|
+
createdAt,
|
|
24236
|
+
backtest: isBacktest,
|
|
24237
|
+
});
|
|
24238
|
+
};
|
|
24239
|
+
/**
|
|
24240
|
+
* Records a trailing-stop event when the stop-loss is adjusted.
|
|
24241
|
+
*
|
|
24242
|
+
* Stores the percentage shift and current price when trailing stop moves.
|
|
24243
|
+
*
|
|
24244
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24245
|
+
* @param percentShift - Percentage the stop-loss was shifted
|
|
24246
|
+
* @param currentPrice - Current market price at time of adjustment
|
|
24247
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24248
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24249
|
+
*/
|
|
24250
|
+
this.trailingStop = async (symbol, percentShift, currentPrice, isBacktest, context) => {
|
|
24251
|
+
this.loggerService.log("strategyMarkdownService trailingStop", {
|
|
24252
|
+
symbol,
|
|
24253
|
+
percentShift,
|
|
24254
|
+
currentPrice,
|
|
24255
|
+
isBacktest,
|
|
24256
|
+
});
|
|
24257
|
+
if (!this.subscribe.hasValue()) {
|
|
24258
|
+
return;
|
|
24259
|
+
}
|
|
24260
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24261
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24262
|
+
exchangeName: context.exchangeName,
|
|
24263
|
+
strategyName: context.strategyName,
|
|
24264
|
+
frameName: context.frameName,
|
|
24265
|
+
});
|
|
24266
|
+
if (!pendingRow) {
|
|
24267
|
+
return;
|
|
24268
|
+
}
|
|
24269
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24270
|
+
storage.addEvent({
|
|
24271
|
+
timestamp: Date.now(),
|
|
24272
|
+
symbol,
|
|
24273
|
+
strategyName: context.strategyName,
|
|
24274
|
+
exchangeName: context.exchangeName,
|
|
24275
|
+
frameName: context.frameName,
|
|
24276
|
+
signalId: pendingRow.id,
|
|
24277
|
+
action: "trailing-stop",
|
|
24278
|
+
percentShift,
|
|
24279
|
+
currentPrice,
|
|
24280
|
+
createdAt,
|
|
24281
|
+
backtest: isBacktest,
|
|
24282
|
+
});
|
|
24283
|
+
};
|
|
24284
|
+
/**
|
|
24285
|
+
* Records a trailing-take event when the take-profit is adjusted.
|
|
24286
|
+
*
|
|
24287
|
+
* Stores the percentage shift and current price when trailing take-profit moves.
|
|
24288
|
+
*
|
|
24289
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24290
|
+
* @param percentShift - Percentage the take-profit was shifted
|
|
24291
|
+
* @param currentPrice - Current market price at time of adjustment
|
|
24292
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24293
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24294
|
+
*/
|
|
24295
|
+
this.trailingTake = async (symbol, percentShift, currentPrice, isBacktest, context) => {
|
|
24296
|
+
this.loggerService.log("strategyMarkdownService trailingTake", {
|
|
24297
|
+
symbol,
|
|
24298
|
+
percentShift,
|
|
24299
|
+
currentPrice,
|
|
24300
|
+
isBacktest,
|
|
24301
|
+
});
|
|
24302
|
+
if (!this.subscribe.hasValue()) {
|
|
24303
|
+
return;
|
|
24304
|
+
}
|
|
24305
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24306
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24307
|
+
exchangeName: context.exchangeName,
|
|
24308
|
+
strategyName: context.strategyName,
|
|
24309
|
+
frameName: context.frameName,
|
|
24310
|
+
});
|
|
24311
|
+
if (!pendingRow) {
|
|
24312
|
+
return;
|
|
24313
|
+
}
|
|
24314
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24315
|
+
storage.addEvent({
|
|
24316
|
+
timestamp: Date.now(),
|
|
24317
|
+
symbol,
|
|
24318
|
+
strategyName: context.strategyName,
|
|
24319
|
+
exchangeName: context.exchangeName,
|
|
24320
|
+
frameName: context.frameName,
|
|
24321
|
+
signalId: pendingRow.id,
|
|
24322
|
+
action: "trailing-take",
|
|
24323
|
+
percentShift,
|
|
24324
|
+
currentPrice,
|
|
24325
|
+
createdAt,
|
|
24326
|
+
backtest: isBacktest,
|
|
24327
|
+
});
|
|
24328
|
+
};
|
|
24329
|
+
/**
|
|
24330
|
+
* Records a breakeven event when the stop-loss is moved to entry price.
|
|
24331
|
+
*
|
|
24332
|
+
* Stores the current price when breakeven protection is activated.
|
|
24333
|
+
*
|
|
24334
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24335
|
+
* @param currentPrice - Current market price at time of breakeven activation
|
|
24336
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24337
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24338
|
+
*/
|
|
24339
|
+
this.breakeven = async (symbol, currentPrice, isBacktest, context) => {
|
|
24340
|
+
this.loggerService.log("strategyMarkdownService breakeven", {
|
|
24341
|
+
symbol,
|
|
24342
|
+
currentPrice,
|
|
24343
|
+
isBacktest,
|
|
24344
|
+
});
|
|
24345
|
+
if (!this.subscribe.hasValue()) {
|
|
24346
|
+
return;
|
|
24347
|
+
}
|
|
24348
|
+
const { when: createdAt } = GET_EXECUTION_CONTEXT_FN(this);
|
|
24349
|
+
const pendingRow = await this.strategyCoreService.getPendingSignal(isBacktest, symbol, {
|
|
24350
|
+
exchangeName: context.exchangeName,
|
|
24351
|
+
strategyName: context.strategyName,
|
|
24352
|
+
frameName: context.frameName,
|
|
24353
|
+
});
|
|
24354
|
+
if (!pendingRow) {
|
|
24355
|
+
return;
|
|
24356
|
+
}
|
|
24357
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
24358
|
+
storage.addEvent({
|
|
24359
|
+
timestamp: Date.now(),
|
|
24360
|
+
symbol,
|
|
24361
|
+
strategyName: context.strategyName,
|
|
24362
|
+
exchangeName: context.exchangeName,
|
|
24363
|
+
frameName: context.frameName,
|
|
24364
|
+
signalId: pendingRow.id,
|
|
24365
|
+
action: "breakeven",
|
|
24366
|
+
currentPrice,
|
|
24367
|
+
createdAt,
|
|
24368
|
+
backtest: isBacktest,
|
|
24369
|
+
});
|
|
24370
|
+
};
|
|
24371
|
+
/**
|
|
24372
|
+
* Retrieves aggregated statistics from accumulated strategy events.
|
|
24373
|
+
*
|
|
24374
|
+
* Returns counts for each action type and the full event list from the
|
|
24375
|
+
* ReportStorage for the specified symbol-strategy pair.
|
|
24376
|
+
*
|
|
24377
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24378
|
+
* @param strategyName - Name of the trading strategy
|
|
24379
|
+
* @param exchangeName - Name of the exchange
|
|
24380
|
+
* @param frameName - Timeframe name for backtest identification
|
|
24381
|
+
* @param backtest - Whether to get backtest or live data
|
|
24382
|
+
* @returns Promise resolving to StrategyStatisticsModel with event list and counts
|
|
24383
|
+
* @throws Error if service not initialized (subscribe() not called)
|
|
24384
|
+
*/
|
|
24385
|
+
this.getData = async (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
24386
|
+
this.loggerService.log("strategyMarkdownService getData", {
|
|
24387
|
+
symbol,
|
|
24388
|
+
strategyName,
|
|
24389
|
+
exchangeName,
|
|
24390
|
+
frameName,
|
|
24391
|
+
backtest,
|
|
24392
|
+
});
|
|
24393
|
+
if (!this.subscribe.hasValue()) {
|
|
24394
|
+
throw new Error("StrategyMarkdownService not initialized. Call subscribe() before getting data.");
|
|
24395
|
+
}
|
|
24396
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
24397
|
+
return storage.getData();
|
|
24398
|
+
};
|
|
24399
|
+
/**
|
|
24400
|
+
* Generates a markdown report from accumulated strategy events.
|
|
24401
|
+
*
|
|
24402
|
+
* Creates a formatted markdown document containing:
|
|
24403
|
+
* - Header with symbol and strategy name
|
|
24404
|
+
* - Table of all events with configurable columns
|
|
24405
|
+
* - Summary statistics with counts by action type
|
|
24406
|
+
*
|
|
24407
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24408
|
+
* @param strategyName - Name of the trading strategy
|
|
24409
|
+
* @param exchangeName - Name of the exchange
|
|
24410
|
+
* @param frameName - Timeframe name for backtest identification
|
|
24411
|
+
* @param backtest - Whether to get backtest or live data
|
|
24412
|
+
* @param columns - Column configuration for the event table
|
|
24413
|
+
* @returns Promise resolving to formatted markdown string
|
|
24414
|
+
* @throws Error if service not initialized (subscribe() not called)
|
|
24415
|
+
*/
|
|
24416
|
+
this.getReport = async (symbol, strategyName, exchangeName, frameName, backtest, columns = COLUMN_CONFIG.strategy_columns) => {
|
|
24417
|
+
this.loggerService.log("strategyMarkdownService getReport", {
|
|
24418
|
+
symbol,
|
|
24419
|
+
strategyName,
|
|
24420
|
+
exchangeName,
|
|
24421
|
+
frameName,
|
|
24422
|
+
backtest,
|
|
24423
|
+
});
|
|
24424
|
+
if (!this.subscribe.hasValue()) {
|
|
24425
|
+
throw new Error("StrategyMarkdownService not initialized. Call subscribe() before generating reports.");
|
|
24426
|
+
}
|
|
24427
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
24428
|
+
return storage.getReport(symbol, strategyName, columns);
|
|
24429
|
+
};
|
|
24430
|
+
/**
|
|
24431
|
+
* Generates and saves a markdown report to disk.
|
|
24432
|
+
*
|
|
24433
|
+
* Creates the output directory if it doesn't exist and writes
|
|
24434
|
+
* the report with a timestamped filename via Markdown.writeData().
|
|
24435
|
+
*
|
|
24436
|
+
* Filename format: `{symbol}_{strategyName}_{exchangeName}[_{frameName}_backtest|_live]-{timestamp}.md`
|
|
24437
|
+
*
|
|
24438
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24439
|
+
* @param strategyName - Name of the trading strategy
|
|
24440
|
+
* @param exchangeName - Name of the exchange
|
|
24441
|
+
* @param frameName - Timeframe name for backtest identification
|
|
24442
|
+
* @param backtest - Whether to dump backtest or live data
|
|
24443
|
+
* @param path - Output directory path (default: "./dump/strategy")
|
|
24444
|
+
* @param columns - Column configuration for the event table
|
|
24445
|
+
* @returns Promise that resolves when file is written
|
|
24446
|
+
* @throws Error if service not initialized (subscribe() not called)
|
|
24447
|
+
*/
|
|
24448
|
+
this.dump = async (symbol, strategyName, exchangeName, frameName, backtest, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) => {
|
|
24449
|
+
this.loggerService.log("strategyMarkdownService dump", {
|
|
24450
|
+
symbol,
|
|
24451
|
+
strategyName,
|
|
24452
|
+
exchangeName,
|
|
24453
|
+
frameName,
|
|
24454
|
+
backtest,
|
|
24455
|
+
path,
|
|
24456
|
+
});
|
|
24457
|
+
if (!this.subscribe.hasValue()) {
|
|
24458
|
+
throw new Error("StrategyMarkdownService not initialized. Call subscribe() before dumping reports.");
|
|
24459
|
+
}
|
|
24460
|
+
const storage = this.getStorage(symbol, strategyName, exchangeName, frameName, backtest);
|
|
24461
|
+
await storage.dump(symbol, strategyName, path, columns);
|
|
24462
|
+
};
|
|
24463
|
+
/**
|
|
24464
|
+
* Clears accumulated events from storage.
|
|
24465
|
+
*
|
|
24466
|
+
* Can clear either a specific symbol-strategy pair or all stored data.
|
|
24467
|
+
*
|
|
24468
|
+
* @param payload - Optional filter to clear specific storage. If omitted, clears all.
|
|
24469
|
+
* @param payload.symbol - Trading pair symbol
|
|
24470
|
+
* @param payload.strategyName - Strategy name
|
|
24471
|
+
* @param payload.exchangeName - Exchange name
|
|
24472
|
+
* @param payload.frameName - Frame name
|
|
24473
|
+
* @param payload.backtest - Backtest mode flag
|
|
24474
|
+
*/
|
|
24475
|
+
this.clear = async (payload) => {
|
|
24476
|
+
this.loggerService.log("strategyMarkdownService clear", { payload });
|
|
24477
|
+
if (payload) {
|
|
24478
|
+
const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
|
|
24479
|
+
this.getStorage.clear(key);
|
|
24480
|
+
}
|
|
24481
|
+
else {
|
|
24482
|
+
this.getStorage.clear();
|
|
24483
|
+
}
|
|
24484
|
+
};
|
|
24485
|
+
/**
|
|
24486
|
+
* Initializes the service for event collection.
|
|
24487
|
+
*
|
|
24488
|
+
* Must be called before any events can be collected or reports generated.
|
|
24489
|
+
* Uses singleshot pattern to ensure only one subscription exists at a time.
|
|
24490
|
+
*
|
|
24491
|
+
* @returns Cleanup function that clears the subscription and all accumulated data
|
|
24492
|
+
*/
|
|
24493
|
+
this.subscribe = functoolsKit.singleshot(() => {
|
|
24494
|
+
this.loggerService.log("strategyMarkdownService subscribe");
|
|
24495
|
+
const unCancelSchedule = strategyCommitSubject
|
|
24496
|
+
.filter(({ action }) => action === "cancel-scheduled")
|
|
24497
|
+
.connect(async (event) => await this.cancelScheduled(event.symbol, event.backtest, {
|
|
24498
|
+
exchangeName: event.exchangeName,
|
|
24499
|
+
frameName: event.frameName,
|
|
24500
|
+
strategyName: event.strategyName,
|
|
24501
|
+
}, event.cancelId));
|
|
24502
|
+
const unClosePending = strategyCommitSubject
|
|
24503
|
+
.filter(({ action }) => action === "close-pending")
|
|
24504
|
+
.connect(async (event) => await this.closePending(event.symbol, event.backtest, {
|
|
24505
|
+
exchangeName: event.exchangeName,
|
|
24506
|
+
frameName: event.frameName,
|
|
24507
|
+
strategyName: event.strategyName,
|
|
24508
|
+
}, event.closeId));
|
|
24509
|
+
const unPartialProfit = strategyCommitSubject
|
|
24510
|
+
.filter(({ action }) => action === "partial-profit")
|
|
24511
|
+
.connect(async (event) => await this.partialProfit(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
|
|
24512
|
+
exchangeName: event.exchangeName,
|
|
24513
|
+
frameName: event.frameName,
|
|
24514
|
+
strategyName: event.strategyName,
|
|
24515
|
+
}));
|
|
24516
|
+
const unPartialLoss = strategyCommitSubject
|
|
24517
|
+
.filter(({ action }) => action === "partial-loss")
|
|
24518
|
+
.connect(async (event) => await this.partialLoss(event.symbol, event.percentToClose, event.currentPrice, event.backtest, {
|
|
24519
|
+
exchangeName: event.exchangeName,
|
|
24520
|
+
frameName: event.frameName,
|
|
24521
|
+
strategyName: event.strategyName,
|
|
24522
|
+
}));
|
|
24523
|
+
const unTrailingStop = strategyCommitSubject
|
|
24524
|
+
.filter(({ action }) => action === "trailing-stop")
|
|
24525
|
+
.connect(async (event) => await this.trailingStop(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
|
|
24526
|
+
exchangeName: event.exchangeName,
|
|
24527
|
+
frameName: event.frameName,
|
|
24528
|
+
strategyName: event.strategyName,
|
|
24529
|
+
}));
|
|
24530
|
+
const unTrailingTake = strategyCommitSubject
|
|
24531
|
+
.filter(({ action }) => action === "trailing-take")
|
|
24532
|
+
.connect(async (event) => await this.trailingTake(event.symbol, event.percentShift, event.currentPrice, event.backtest, {
|
|
24533
|
+
exchangeName: event.exchangeName,
|
|
24534
|
+
frameName: event.frameName,
|
|
24535
|
+
strategyName: event.strategyName,
|
|
24536
|
+
}));
|
|
24537
|
+
const unBreakeven = strategyCommitSubject
|
|
24538
|
+
.filter(({ action }) => action === "breakeven")
|
|
24539
|
+
.connect(async (event) => await this.breakeven(event.symbol, event.currentPrice, event.backtest, {
|
|
24540
|
+
exchangeName: event.exchangeName,
|
|
24541
|
+
frameName: event.frameName,
|
|
24542
|
+
strategyName: event.strategyName,
|
|
24543
|
+
}));
|
|
24544
|
+
const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven());
|
|
24545
|
+
return () => {
|
|
24546
|
+
disposeFn();
|
|
24547
|
+
this.subscribe.clear();
|
|
24548
|
+
this.clear();
|
|
24549
|
+
};
|
|
24550
|
+
});
|
|
24551
|
+
/**
|
|
24552
|
+
* Stops event collection and clears all accumulated data.
|
|
24553
|
+
*
|
|
24554
|
+
* Invokes the cleanup function returned by subscribe(), which clears
|
|
24555
|
+
* both the subscription and all ReportStorage instances.
|
|
24556
|
+
* Safe to call multiple times - only acts if subscription exists.
|
|
24557
|
+
*/
|
|
24558
|
+
this.unsubscribe = async () => {
|
|
24559
|
+
this.loggerService.log("strategyMarkdownService unsubscribe");
|
|
24560
|
+
if (this.subscribe.hasValue()) {
|
|
24561
|
+
const lastSubscription = this.subscribe();
|
|
24562
|
+
lastSubscription();
|
|
24563
|
+
}
|
|
24564
|
+
};
|
|
24565
|
+
}
|
|
24566
|
+
}
|
|
24567
|
+
|
|
24568
|
+
{
|
|
24569
|
+
provide(TYPES.loggerService, () => new LoggerService());
|
|
24570
|
+
}
|
|
24571
|
+
{
|
|
24572
|
+
provide(TYPES.executionContextService, () => new ExecutionContextService());
|
|
24573
|
+
provide(TYPES.methodContextService, () => new MethodContextService());
|
|
24574
|
+
}
|
|
24575
|
+
{
|
|
24576
|
+
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
|
|
24577
|
+
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
|
|
24578
|
+
provide(TYPES.frameConnectionService, () => new FrameConnectionService());
|
|
24579
|
+
provide(TYPES.sizingConnectionService, () => new SizingConnectionService());
|
|
24580
|
+
provide(TYPES.riskConnectionService, () => new RiskConnectionService());
|
|
24581
|
+
provide(TYPES.actionConnectionService, () => new ActionConnectionService());
|
|
24582
|
+
provide(TYPES.partialConnectionService, () => new PartialConnectionService());
|
|
24583
|
+
provide(TYPES.breakevenConnectionService, () => new BreakevenConnectionService());
|
|
23101
24584
|
}
|
|
23102
24585
|
{
|
|
23103
24586
|
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
|
|
@@ -23145,6 +24628,7 @@ class RiskReportService {
|
|
|
23145
24628
|
provide(TYPES.partialMarkdownService, () => new PartialMarkdownService());
|
|
23146
24629
|
provide(TYPES.breakevenMarkdownService, () => new BreakevenMarkdownService());
|
|
23147
24630
|
provide(TYPES.riskMarkdownService, () => new RiskMarkdownService());
|
|
24631
|
+
provide(TYPES.strategyMarkdownService, () => new StrategyMarkdownService());
|
|
23148
24632
|
}
|
|
23149
24633
|
{
|
|
23150
24634
|
provide(TYPES.backtestReportService, () => new BacktestReportService());
|
|
@@ -23156,6 +24640,7 @@ class RiskReportService {
|
|
|
23156
24640
|
provide(TYPES.partialReportService, () => new PartialReportService());
|
|
23157
24641
|
provide(TYPES.breakevenReportService, () => new BreakevenReportService());
|
|
23158
24642
|
provide(TYPES.riskReportService, () => new RiskReportService());
|
|
24643
|
+
provide(TYPES.strategyReportService, () => new StrategyReportService());
|
|
23159
24644
|
}
|
|
23160
24645
|
{
|
|
23161
24646
|
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
|
|
@@ -23232,6 +24717,7 @@ const markdownServices = {
|
|
|
23232
24717
|
partialMarkdownService: inject(TYPES.partialMarkdownService),
|
|
23233
24718
|
breakevenMarkdownService: inject(TYPES.breakevenMarkdownService),
|
|
23234
24719
|
riskMarkdownService: inject(TYPES.riskMarkdownService),
|
|
24720
|
+
strategyMarkdownService: inject(TYPES.strategyMarkdownService),
|
|
23235
24721
|
};
|
|
23236
24722
|
const reportServices = {
|
|
23237
24723
|
backtestReportService: inject(TYPES.backtestReportService),
|
|
@@ -23243,6 +24729,7 @@ const reportServices = {
|
|
|
23243
24729
|
partialReportService: inject(TYPES.partialReportService),
|
|
23244
24730
|
breakevenReportService: inject(TYPES.breakevenReportService),
|
|
23245
24731
|
riskReportService: inject(TYPES.riskReportService),
|
|
24732
|
+
strategyReportService: inject(TYPES.strategyReportService),
|
|
23246
24733
|
};
|
|
23247
24734
|
const validationServices = {
|
|
23248
24735
|
exchangeValidationService: inject(TYPES.exchangeValidationService),
|
|
@@ -25321,6 +26808,8 @@ const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
|
|
|
25321
26808
|
const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
|
|
25322
26809
|
const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
|
|
25323
26810
|
const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
|
|
26811
|
+
const LISTEN_STRATEGY_COMMIT_METHOD_NAME = "event.listenStrategyCommit";
|
|
26812
|
+
const LISTEN_STRATEGY_COMMIT_ONCE_METHOD_NAME = "event.listenStrategyCommitOnce";
|
|
25324
26813
|
/**
|
|
25325
26814
|
* Subscribes to all signal events with queued async processing.
|
|
25326
26815
|
*
|
|
@@ -26350,6 +27839,78 @@ function listenActivePingOnce(filterFn, fn) {
|
|
|
26350
27839
|
bt.loggerService.log(LISTEN_ACTIVE_PING_ONCE_METHOD_NAME);
|
|
26351
27840
|
return activePingSubject.filter(filterFn).once(fn);
|
|
26352
27841
|
}
|
|
27842
|
+
/**
|
|
27843
|
+
* Subscribes to strategy management events with queued async processing.
|
|
27844
|
+
*
|
|
27845
|
+
* Emits when strategy management actions are executed:
|
|
27846
|
+
* - cancel-scheduled: Scheduled signal cancelled
|
|
27847
|
+
* - close-pending: Pending signal closed
|
|
27848
|
+
* - partial-profit: Partial close at profit level
|
|
27849
|
+
* - partial-loss: Partial close at loss level
|
|
27850
|
+
* - trailing-stop: Stop-loss adjusted
|
|
27851
|
+
* - trailing-take: Take-profit adjusted
|
|
27852
|
+
* - breakeven: Stop-loss moved to entry price
|
|
27853
|
+
*
|
|
27854
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
27855
|
+
* Uses queued wrapper to prevent concurrent execution of the callback.
|
|
27856
|
+
*
|
|
27857
|
+
* @param fn - Callback function to handle strategy commit events
|
|
27858
|
+
* @returns Unsubscribe function to stop listening
|
|
27859
|
+
*
|
|
27860
|
+
* @example
|
|
27861
|
+
* ```typescript
|
|
27862
|
+
* import { listenStrategyCommit } from "./function/event";
|
|
27863
|
+
*
|
|
27864
|
+
* const unsubscribe = listenStrategyCommit((event) => {
|
|
27865
|
+
* console.log(`[${event.action}] ${event.symbol}`);
|
|
27866
|
+
* console.log(`Strategy: ${event.strategyName}, Exchange: ${event.exchangeName}`);
|
|
27867
|
+
* if (event.action === "partial-profit") {
|
|
27868
|
+
* console.log(`Closed ${event.percentToClose}% at ${event.currentPrice}`);
|
|
27869
|
+
* }
|
|
27870
|
+
* });
|
|
27871
|
+
*
|
|
27872
|
+
* // Later: stop listening
|
|
27873
|
+
* unsubscribe();
|
|
27874
|
+
* ```
|
|
27875
|
+
*/
|
|
27876
|
+
function listenStrategyCommit(fn) {
|
|
27877
|
+
bt.loggerService.log(LISTEN_STRATEGY_COMMIT_METHOD_NAME);
|
|
27878
|
+
return strategyCommitSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
27879
|
+
}
|
|
27880
|
+
/**
|
|
27881
|
+
* Subscribes to filtered strategy management events with one-time execution.
|
|
27882
|
+
*
|
|
27883
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
27884
|
+
* and automatically unsubscribes. Useful for waiting for specific strategy actions.
|
|
27885
|
+
*
|
|
27886
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
27887
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
27888
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
27889
|
+
*
|
|
27890
|
+
* @example
|
|
27891
|
+
* ```typescript
|
|
27892
|
+
* import { listenStrategyCommitOnce } from "./function/event";
|
|
27893
|
+
*
|
|
27894
|
+
* // Wait for first trailing stop adjustment
|
|
27895
|
+
* listenStrategyCommitOnce(
|
|
27896
|
+
* (event) => event.action === "trailing-stop",
|
|
27897
|
+
* (event) => console.log("Trailing stop adjusted:", event.symbol)
|
|
27898
|
+
* );
|
|
27899
|
+
*
|
|
27900
|
+
* // Wait for breakeven on BTCUSDT
|
|
27901
|
+
* const cancel = listenStrategyCommitOnce(
|
|
27902
|
+
* (event) => event.action === "breakeven" && event.symbol === "BTCUSDT",
|
|
27903
|
+
* (event) => console.log("BTCUSDT moved to breakeven at", event.currentPrice)
|
|
27904
|
+
* );
|
|
27905
|
+
*
|
|
27906
|
+
* // Cancel if needed before event fires
|
|
27907
|
+
* cancel();
|
|
27908
|
+
* ```
|
|
27909
|
+
*/
|
|
27910
|
+
function listenStrategyCommitOnce(filterFn, fn) {
|
|
27911
|
+
bt.loggerService.log(LISTEN_STRATEGY_COMMIT_ONCE_METHOD_NAME);
|
|
27912
|
+
return strategyCommitSubject.filter(filterFn).once(fn);
|
|
27913
|
+
}
|
|
26353
27914
|
|
|
26354
27915
|
const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
|
|
26355
27916
|
const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
|
|
@@ -31409,6 +32970,201 @@ class BreakevenUtils {
|
|
|
31409
32970
|
*/
|
|
31410
32971
|
const Breakeven = new BreakevenUtils();
|
|
31411
32972
|
|
|
32973
|
+
const STRATEGY_METHOD_NAME_GET_DATA = "StrategyUtils.getData";
|
|
32974
|
+
const STRATEGY_METHOD_NAME_GET_REPORT = "StrategyUtils.getReport";
|
|
32975
|
+
const STRATEGY_METHOD_NAME_DUMP = "StrategyUtils.dump";
|
|
32976
|
+
/**
|
|
32977
|
+
* Utility class for accessing strategy management reports and statistics.
|
|
32978
|
+
*
|
|
32979
|
+
* Provides static-like methods (via singleton instance) to retrieve data
|
|
32980
|
+
* accumulated by StrategyMarkdownService from strategy management events.
|
|
32981
|
+
*
|
|
32982
|
+
* Features:
|
|
32983
|
+
* - Statistical data extraction (event counts by action type)
|
|
32984
|
+
* - Markdown report generation with event tables
|
|
32985
|
+
* - File export to disk
|
|
32986
|
+
*
|
|
32987
|
+
* Data source:
|
|
32988
|
+
* - StrategyMarkdownService receives events via direct method calls
|
|
32989
|
+
* - Accumulates events in ReportStorage (max 250 events per symbol-strategy pair)
|
|
32990
|
+
* - Events include: cancel-scheduled, close-pending, partial-profit, partial-loss,
|
|
32991
|
+
* trailing-stop, trailing-take, breakeven
|
|
32992
|
+
*
|
|
32993
|
+
* @example
|
|
32994
|
+
* ```typescript
|
|
32995
|
+
* import { Strategy } from "./classes/Strategy";
|
|
32996
|
+
*
|
|
32997
|
+
* // Get statistical data for BTCUSDT:my-strategy
|
|
32998
|
+
* const stats = await Strategy.getData("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
32999
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
33000
|
+
* console.log(`Partial profit events: ${stats.partialProfitCount}`);
|
|
33001
|
+
*
|
|
33002
|
+
* // Generate markdown report
|
|
33003
|
+
* const markdown = await Strategy.getReport("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33004
|
+
* console.log(markdown); // Formatted table with all events
|
|
33005
|
+
*
|
|
33006
|
+
* // Export report to file
|
|
33007
|
+
* await Strategy.dump("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" }); // Saves to ./dump/strategy/
|
|
33008
|
+
* ```
|
|
33009
|
+
*/
|
|
33010
|
+
class StrategyUtils {
|
|
33011
|
+
constructor() {
|
|
33012
|
+
/**
|
|
33013
|
+
* Retrieves statistical data from accumulated strategy events.
|
|
33014
|
+
*
|
|
33015
|
+
* Delegates to StrategyMarkdownService.getData() which reads from ReportStorage.
|
|
33016
|
+
* Returns aggregated metrics calculated from all strategy events.
|
|
33017
|
+
*
|
|
33018
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
33019
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
33020
|
+
* @param backtest - Whether to get backtest data (default: false)
|
|
33021
|
+
* @returns Promise resolving to StrategyStatisticsModel object with counts and event list
|
|
33022
|
+
*
|
|
33023
|
+
* @example
|
|
33024
|
+
* ```typescript
|
|
33025
|
+
* const stats = await Strategy.getData("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33026
|
+
*
|
|
33027
|
+
* console.log(`Total events: ${stats.totalEvents}`);
|
|
33028
|
+
* console.log(`Partial profit: ${stats.partialProfitCount}`);
|
|
33029
|
+
* console.log(`Trailing stop: ${stats.trailingStopCount}`);
|
|
33030
|
+
*
|
|
33031
|
+
* // Iterate through all events
|
|
33032
|
+
* for (const event of stats.eventList) {
|
|
33033
|
+
* console.log(`Signal ${event.signalId}: ${event.action} at ${event.currentPrice}`);
|
|
33034
|
+
* }
|
|
33035
|
+
* ```
|
|
33036
|
+
*/
|
|
33037
|
+
this.getData = async (symbol, context, backtest = false) => {
|
|
33038
|
+
bt.loggerService.info(STRATEGY_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
|
|
33039
|
+
bt.strategyValidationService.validate(context.strategyName, STRATEGY_METHOD_NAME_GET_DATA);
|
|
33040
|
+
bt.exchangeValidationService.validate(context.exchangeName, STRATEGY_METHOD_NAME_GET_DATA);
|
|
33041
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, STRATEGY_METHOD_NAME_GET_DATA);
|
|
33042
|
+
{
|
|
33043
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33044
|
+
riskName && bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_GET_DATA);
|
|
33045
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_GET_DATA));
|
|
33046
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, STRATEGY_METHOD_NAME_GET_DATA));
|
|
33047
|
+
}
|
|
33048
|
+
return await bt.strategyMarkdownService.getData(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
33049
|
+
};
|
|
33050
|
+
/**
|
|
33051
|
+
* Generates markdown report with all strategy events for a symbol-strategy pair.
|
|
33052
|
+
*
|
|
33053
|
+
* Creates formatted table containing:
|
|
33054
|
+
* - Symbol
|
|
33055
|
+
* - Strategy
|
|
33056
|
+
* - Signal ID
|
|
33057
|
+
* - Action (cancel-scheduled, close-pending, partial-profit, etc.)
|
|
33058
|
+
* - Price
|
|
33059
|
+
* - Percent values (% To Close, % Shift)
|
|
33060
|
+
* - Cancel/Close IDs
|
|
33061
|
+
* - Timestamp (ISO 8601)
|
|
33062
|
+
* - Mode (Backtest/Live)
|
|
33063
|
+
*
|
|
33064
|
+
* Also includes summary statistics at the end with counts by action type.
|
|
33065
|
+
*
|
|
33066
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
33067
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
33068
|
+
* @param backtest - Whether to get backtest data (default: false)
|
|
33069
|
+
* @param columns - Optional columns configuration for the report
|
|
33070
|
+
* @returns Promise resolving to markdown formatted report string
|
|
33071
|
+
*
|
|
33072
|
+
* @example
|
|
33073
|
+
* ```typescript
|
|
33074
|
+
* const markdown = await Strategy.getReport("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33075
|
+
* console.log(markdown);
|
|
33076
|
+
*
|
|
33077
|
+
* // Output:
|
|
33078
|
+
* // # Strategy Report: BTCUSDT:my-strategy
|
|
33079
|
+
* //
|
|
33080
|
+
* // | Symbol | Strategy | Signal ID | Action | Price | ... |
|
|
33081
|
+
* // | --- | --- | --- | --- | --- | ... |
|
|
33082
|
+
* // | BTCUSDT | my-strategy | abc123 | partial-profit | 50100.00000000 USD | ... |
|
|
33083
|
+
* //
|
|
33084
|
+
* // **Total events:** 5
|
|
33085
|
+
* // - Cancel scheduled: 0
|
|
33086
|
+
* // - Close pending: 1
|
|
33087
|
+
* // - Partial profit: 2
|
|
33088
|
+
* // ...
|
|
33089
|
+
* ```
|
|
33090
|
+
*/
|
|
33091
|
+
this.getReport = async (symbol, context, backtest = false, columns) => {
|
|
33092
|
+
bt.loggerService.info(STRATEGY_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
|
|
33093
|
+
bt.strategyValidationService.validate(context.strategyName, STRATEGY_METHOD_NAME_GET_REPORT);
|
|
33094
|
+
bt.exchangeValidationService.validate(context.exchangeName, STRATEGY_METHOD_NAME_GET_REPORT);
|
|
33095
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, STRATEGY_METHOD_NAME_GET_REPORT);
|
|
33096
|
+
{
|
|
33097
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33098
|
+
riskName && bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_GET_REPORT);
|
|
33099
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_GET_REPORT));
|
|
33100
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, STRATEGY_METHOD_NAME_GET_REPORT));
|
|
33101
|
+
}
|
|
33102
|
+
return await bt.strategyMarkdownService.getReport(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, columns);
|
|
33103
|
+
};
|
|
33104
|
+
/**
|
|
33105
|
+
* Generates and saves markdown report to file.
|
|
33106
|
+
*
|
|
33107
|
+
* Creates directory if it doesn't exist.
|
|
33108
|
+
* Filename format: {symbol}_{strategyName}_{exchangeName}_{frameName|live}-{timestamp}.md
|
|
33109
|
+
*
|
|
33110
|
+
* Delegates to StrategyMarkdownService.dump() which:
|
|
33111
|
+
* 1. Generates markdown report via getReport()
|
|
33112
|
+
* 2. Creates output directory (recursive mkdir)
|
|
33113
|
+
* 3. Writes file with UTF-8 encoding
|
|
33114
|
+
* 4. Logs success/failure to console
|
|
33115
|
+
*
|
|
33116
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
33117
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
33118
|
+
* @param backtest - Whether to dump backtest data (default: false)
|
|
33119
|
+
* @param path - Output directory path (default: "./dump/strategy")
|
|
33120
|
+
* @param columns - Optional columns configuration for the report
|
|
33121
|
+
* @returns Promise that resolves when file is written
|
|
33122
|
+
*
|
|
33123
|
+
* @example
|
|
33124
|
+
* ```typescript
|
|
33125
|
+
* // Save to default path: ./dump/strategy/BTCUSDT_my-strategy_binance_1h_backtest-{timestamp}.md
|
|
33126
|
+
* await Strategy.dump("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" }, true);
|
|
33127
|
+
*
|
|
33128
|
+
* // Save to custom path
|
|
33129
|
+
* await Strategy.dump("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" }, true, "./reports/strategy");
|
|
33130
|
+
*
|
|
33131
|
+
* // After multiple symbols backtested, export all reports
|
|
33132
|
+
* for (const symbol of ["BTCUSDT", "ETHUSDT", "BNBUSDT"]) {
|
|
33133
|
+
* await Strategy.dump(symbol, { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" }, true, "./backtest-results");
|
|
33134
|
+
* }
|
|
33135
|
+
* ```
|
|
33136
|
+
*/
|
|
33137
|
+
this.dump = async (symbol, context, backtest = false, path, columns) => {
|
|
33138
|
+
bt.loggerService.info(STRATEGY_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
|
|
33139
|
+
bt.strategyValidationService.validate(context.strategyName, STRATEGY_METHOD_NAME_DUMP);
|
|
33140
|
+
bt.exchangeValidationService.validate(context.exchangeName, STRATEGY_METHOD_NAME_DUMP);
|
|
33141
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, STRATEGY_METHOD_NAME_DUMP);
|
|
33142
|
+
{
|
|
33143
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33144
|
+
riskName && bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_DUMP);
|
|
33145
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, STRATEGY_METHOD_NAME_DUMP));
|
|
33146
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, STRATEGY_METHOD_NAME_DUMP));
|
|
33147
|
+
}
|
|
33148
|
+
await bt.strategyMarkdownService.dump(symbol, context.strategyName, context.exchangeName, context.frameName, backtest, path, columns);
|
|
33149
|
+
};
|
|
33150
|
+
}
|
|
33151
|
+
}
|
|
33152
|
+
/**
|
|
33153
|
+
* Global singleton instance of StrategyUtils.
|
|
33154
|
+
* Provides static-like access to strategy management reporting methods.
|
|
33155
|
+
*
|
|
33156
|
+
* @example
|
|
33157
|
+
* ```typescript
|
|
33158
|
+
* import { Strategy } from "backtest-kit";
|
|
33159
|
+
*
|
|
33160
|
+
* // Usage same as StrategyUtils methods
|
|
33161
|
+
* const stats = await Strategy.getData("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33162
|
+
* const report = await Strategy.getReport("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33163
|
+
* await Strategy.dump("BTCUSDT", { strategyName: "my-strategy", exchangeName: "binance", frameName: "1h" });
|
|
33164
|
+
* ```
|
|
33165
|
+
*/
|
|
33166
|
+
const Strategy = new StrategyUtils();
|
|
33167
|
+
|
|
31412
33168
|
/**
|
|
31413
33169
|
* Rounds a price to the appropriate precision based on the tick size.
|
|
31414
33170
|
*
|
|
@@ -31569,6 +33325,7 @@ exports.Report = Report;
|
|
|
31569
33325
|
exports.ReportBase = ReportBase;
|
|
31570
33326
|
exports.Risk = Risk;
|
|
31571
33327
|
exports.Schedule = Schedule;
|
|
33328
|
+
exports.Strategy = Strategy;
|
|
31572
33329
|
exports.Walker = Walker;
|
|
31573
33330
|
exports.addActionSchema = addActionSchema;
|
|
31574
33331
|
exports.addExchangeSchema = addExchangeSchema;
|
|
@@ -31644,6 +33401,8 @@ exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|
|
|
31644
33401
|
exports.listenSignalLive = listenSignalLive;
|
|
31645
33402
|
exports.listenSignalLiveOnce = listenSignalLiveOnce;
|
|
31646
33403
|
exports.listenSignalOnce = listenSignalOnce;
|
|
33404
|
+
exports.listenStrategyCommit = listenStrategyCommit;
|
|
33405
|
+
exports.listenStrategyCommitOnce = listenStrategyCommitOnce;
|
|
31647
33406
|
exports.listenValidation = listenValidation;
|
|
31648
33407
|
exports.listenWalker = listenWalker;
|
|
31649
33408
|
exports.listenWalkerComplete = listenWalkerComplete;
|