backtest-kit 13.6.0 → 14.1.0
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 +524 -1723
- package/build/index.cjs +1640 -54
- package/build/index.mjs +1635 -55
- package/package.json +3 -3
- package/types.d.ts +1516 -40
package/build/index.cjs
CHANGED
|
@@ -768,6 +768,22 @@ const riskSubject = new functoolsKit.Subject();
|
|
|
768
768
|
* Allows users to track scheduled signal lifecycle and implement custom cancellation logic.
|
|
769
769
|
*/
|
|
770
770
|
const schedulePingSubject = new functoolsKit.Subject();
|
|
771
|
+
/**
|
|
772
|
+
* Scheduled signal lifecycle emitter (creation and cancellation).
|
|
773
|
+
* Emits when a scheduled signal is created (action "scheduled") or cancelled before
|
|
774
|
+
* activation (action "cancelled": timeout / price_reject / user) during tick()/backtest().
|
|
775
|
+
*
|
|
776
|
+
* The scheduled -> active transition (activation) is intentionally NOT emitted here — that
|
|
777
|
+
* produces an "opened" signal on the regular signal emitters instead.
|
|
778
|
+
*/
|
|
779
|
+
const scheduleEventSubject = new functoolsKit.Subject();
|
|
780
|
+
/**
|
|
781
|
+
* Pending signal lifecycle emitter (open and close).
|
|
782
|
+
* Emits when a pending position is opened (action "opened": new signal / immediate / scheduled
|
|
783
|
+
* or user activation) or closed (action "closed" with closeReason take_profit / stop_loss /
|
|
784
|
+
* time_expired / closed) during tick()/backtest().
|
|
785
|
+
*/
|
|
786
|
+
const signalEventSubject = new functoolsKit.Subject();
|
|
771
787
|
/**
|
|
772
788
|
* Active ping emitter for active pending signal monitoring events.
|
|
773
789
|
* Emits every minute when an active pending signal is being monitored.
|
|
@@ -858,10 +874,12 @@ var emitters = /*#__PURE__*/Object.freeze({
|
|
|
858
874
|
progressBacktestEmitter: progressBacktestEmitter,
|
|
859
875
|
progressWalkerEmitter: progressWalkerEmitter,
|
|
860
876
|
riskSubject: riskSubject,
|
|
877
|
+
scheduleEventSubject: scheduleEventSubject,
|
|
861
878
|
schedulePingSubject: schedulePingSubject,
|
|
862
879
|
shutdownEmitter: shutdownEmitter,
|
|
863
880
|
signalBacktestEmitter: signalBacktestEmitter,
|
|
864
881
|
signalEmitter: signalEmitter,
|
|
882
|
+
signalEventSubject: signalEventSubject,
|
|
865
883
|
signalLiveEmitter: signalLiveEmitter,
|
|
866
884
|
signalNotifySubject: signalNotifySubject,
|
|
867
885
|
strategyCommitSubject: strategyCommitSubject,
|
|
@@ -1929,7 +1947,7 @@ class PersistStrategyInstance {
|
|
|
1929
1947
|
const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
|
|
1930
1948
|
// JSON serializes Infinity as null, so an eternal-hold signal
|
|
1931
1949
|
// (minuteEstimatedTime: Infinity) reads back as null — restore it.
|
|
1932
|
-
for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
|
|
1950
|
+
for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal, strategyData.takeProfitSignal, strategyData.stopLossSignal]) {
|
|
1933
1951
|
if (signal && signal.minuteEstimatedTime == null) {
|
|
1934
1952
|
signal.minuteEstimatedTime = Infinity;
|
|
1935
1953
|
}
|
|
@@ -7673,6 +7691,10 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7673
7691
|
self._closedSignal = strategyData.closedSignal;
|
|
7674
7692
|
self._cancelledSignal = strategyData.cancelledSignal;
|
|
7675
7693
|
self._activatedSignal = strategyData.activatedSignal;
|
|
7694
|
+
// Deferred broker-confirmed TP/SL fills snapshot the pending signal and clear it (like
|
|
7695
|
+
// _closedSignal), so they stand on their own and are restored unconditionally too.
|
|
7696
|
+
self._takeProfitSignal = strategyData.takeProfitSignal;
|
|
7697
|
+
self._stopLossSignal = strategyData.stopLossSignal;
|
|
7676
7698
|
}
|
|
7677
7699
|
// Restore pending signal
|
|
7678
7700
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
@@ -7753,6 +7775,8 @@ const PERSIST_STRATEGY_FN = async (self) => {
|
|
|
7753
7775
|
closedSignal: self._closedSignal,
|
|
7754
7776
|
cancelledSignal: self._cancelledSignal,
|
|
7755
7777
|
activatedSignal: self._activatedSignal,
|
|
7778
|
+
takeProfitSignal: self._takeProfitSignal,
|
|
7779
|
+
stopLossSignal: self._stopLossSignal,
|
|
7756
7780
|
}, self.params.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7757
7781
|
};
|
|
7758
7782
|
const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
|
|
@@ -8286,6 +8310,7 @@ const CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN = async (self, scheduled, currentPrice)
|
|
|
8286
8310
|
maxMinutes: GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES,
|
|
8287
8311
|
});
|
|
8288
8312
|
await self.setScheduledSignal(null);
|
|
8313
|
+
await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "timeout");
|
|
8289
8314
|
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
8290
8315
|
const result = {
|
|
8291
8316
|
action: "cancelled",
|
|
@@ -8342,6 +8367,7 @@ const CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN = async (self, scheduled, currentPr
|
|
|
8342
8367
|
});
|
|
8343
8368
|
await self.setScheduledSignal(null);
|
|
8344
8369
|
const currentTime = self.params.execution.context.when.getTime();
|
|
8370
|
+
await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, currentPrice, currentTime, "price_reject");
|
|
8345
8371
|
await CALL_CANCEL_CALLBACKS_FN(self, self.params.execution.context.symbol, scheduled, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
8346
8372
|
const result = {
|
|
8347
8373
|
action: "cancelled",
|
|
@@ -8435,6 +8461,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
8435
8461
|
await self.setScheduledSignal(null);
|
|
8436
8462
|
await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
|
|
8437
8463
|
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
|
|
8464
|
+
await CALL_SIGNAL_EVENT_FN(self, "opened", self._pendingSignal, self._pendingSignal.priceOpen, activationTime);
|
|
8438
8465
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
8439
8466
|
const result = {
|
|
8440
8467
|
action: "opened",
|
|
@@ -8569,6 +8596,53 @@ const CALL_SCHEDULE_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self,
|
|
|
8569
8596
|
errorEmitter.next(error);
|
|
8570
8597
|
},
|
|
8571
8598
|
});
|
|
8599
|
+
/**
|
|
8600
|
+
* Emits a scheduled signal lifecycle event (creation / cancellation) to onScheduleEvent.
|
|
8601
|
+
*
|
|
8602
|
+
* Called when a scheduled signal is created (action "scheduled") or cancelled before activation
|
|
8603
|
+
* (action "cancelled" with reason timeout / price_reject / user). The scheduled -> active
|
|
8604
|
+
* transition is intentionally NOT emitted through this path.
|
|
8605
|
+
*
|
|
8606
|
+
* Unlike the CALL_*_CALLBACKS_FN helpers this does not run inside beginTime/runInContext: the
|
|
8607
|
+
* timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
|
|
8608
|
+
* the connection-level emitter, mirroring how onSchedulePing carries its own timestamp.
|
|
8609
|
+
*/
|
|
8610
|
+
const CALL_SCHEDULE_EVENT_FN$1 = functoolsKit.trycatch(async (self, action, signal, currentPrice, timestamp, reason) => {
|
|
8611
|
+
await self.params.onScheduleEvent(action, self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, TO_PUBLIC_SIGNAL("scheduled", signal, currentPrice), currentPrice, self.params.execution.context.backtest, timestamp, reason);
|
|
8612
|
+
}, {
|
|
8613
|
+
fallback: (error, self) => {
|
|
8614
|
+
const message = "ClientStrategy CALL_SCHEDULE_EVENT_FN thrown";
|
|
8615
|
+
const payload = {
|
|
8616
|
+
error: functoolsKit.errorData(error),
|
|
8617
|
+
message: functoolsKit.getErrorMessage(error),
|
|
8618
|
+
};
|
|
8619
|
+
self.params.logger.warn(message, payload);
|
|
8620
|
+
console.warn(message, payload);
|
|
8621
|
+
errorEmitter.next(error);
|
|
8622
|
+
},
|
|
8623
|
+
});
|
|
8624
|
+
/**
|
|
8625
|
+
* Emits a pending signal lifecycle event (open / close) to onSignalEvent.
|
|
8626
|
+
*
|
|
8627
|
+
* Called when a pending position is opened (action "opened") or closed (action "closed" with a
|
|
8628
|
+
* closeReason). Like CALL_SCHEDULE_EVENT_FN it does not run inside beginTime/runInContext: the
|
|
8629
|
+
* timestamp is passed explicitly by the caller (tick when / candle timestamp) and forwarded to
|
|
8630
|
+
* the connection-level emitter.
|
|
8631
|
+
*/
|
|
8632
|
+
const CALL_SIGNAL_EVENT_FN = functoolsKit.trycatch(async (self, action, signal, currentPrice, timestamp, closeReason) => {
|
|
8633
|
+
await self.params.onSignalEvent(action, self.params.execution.context.symbol, self.params.method.context.strategyName, self.params.method.context.exchangeName, TO_PUBLIC_SIGNAL("pending", signal, currentPrice), currentPrice, self.params.execution.context.backtest, timestamp, closeReason);
|
|
8634
|
+
}, {
|
|
8635
|
+
fallback: (error, self) => {
|
|
8636
|
+
const message = "ClientStrategy CALL_SIGNAL_EVENT_FN thrown";
|
|
8637
|
+
const payload = {
|
|
8638
|
+
error: functoolsKit.errorData(error),
|
|
8639
|
+
message: functoolsKit.getErrorMessage(error),
|
|
8640
|
+
};
|
|
8641
|
+
self.params.logger.warn(message, payload);
|
|
8642
|
+
console.warn(message, payload);
|
|
8643
|
+
errorEmitter.next(error);
|
|
8644
|
+
},
|
|
8645
|
+
});
|
|
8572
8646
|
const CALL_CANCEL_CALLBACKS_FN = functoolsKit.trycatch(beginTime(async (self, symbol, signal, currentPrice, timestamp, backtest) => {
|
|
8573
8647
|
await ExecutionContextService.runInContext(async () => {
|
|
8574
8648
|
if (self.params.callbacks?.onCancel) {
|
|
@@ -8944,6 +9018,7 @@ const OPEN_NEW_SCHEDULED_SIGNAL_FN = async (self, signal) => {
|
|
|
8944
9018
|
priceOpen: signal.priceOpen,
|
|
8945
9019
|
currentPrice: currentPrice,
|
|
8946
9020
|
});
|
|
9021
|
+
await CALL_SCHEDULE_EVENT_FN$1(self, "scheduled", signal, currentPrice, currentTime);
|
|
8947
9022
|
await CALL_SCHEDULE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
8948
9023
|
const result = {
|
|
8949
9024
|
action: "scheduled",
|
|
@@ -8974,6 +9049,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
|
8974
9049
|
return null;
|
|
8975
9050
|
}
|
|
8976
9051
|
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
|
|
9052
|
+
await CALL_SIGNAL_EVENT_FN(self, "opened", signal, signal.priceOpen, currentTime);
|
|
8977
9053
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
|
|
8978
9054
|
const result = {
|
|
8979
9055
|
action: "opened",
|
|
@@ -9040,6 +9116,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
|
|
|
9040
9116
|
priceClose: currentPrice,
|
|
9041
9117
|
pnlPercentage: publicSignal.pnl.pnlPercentage,
|
|
9042
9118
|
});
|
|
9119
|
+
await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, closeReason);
|
|
9043
9120
|
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
9044
9121
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
9045
9122
|
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
@@ -9081,6 +9158,7 @@ const CLOSE_PENDING_SIGNAL_AS_CLOSED_FN = async (self, signal, currentPrice) =>
|
|
|
9081
9158
|
priceClose: currentPrice,
|
|
9082
9159
|
pnlPercentage: publicSignal.pnl.pnlPercentage,
|
|
9083
9160
|
});
|
|
9161
|
+
await CALL_SIGNAL_EVENT_FN(self, "closed", signal, currentPrice, currentTime, "closed");
|
|
9084
9162
|
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
9085
9163
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
9086
9164
|
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
@@ -9251,6 +9329,7 @@ const CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, averagePr
|
|
|
9251
9329
|
});
|
|
9252
9330
|
await self.setScheduledSignal(null);
|
|
9253
9331
|
const publicSignal = TO_PUBLIC_SIGNAL("scheduled", scheduled, averagePrice);
|
|
9332
|
+
await CALL_SCHEDULE_EVENT_FN$1(self, "cancelled", scheduled, averagePrice, closeTimestamp, reason);
|
|
9254
9333
|
if (reason === "user") {
|
|
9255
9334
|
await CALL_COMMIT_FN(self, {
|
|
9256
9335
|
action: "cancel-scheduled",
|
|
@@ -9363,6 +9442,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
9363
9442
|
await self.setScheduledSignal(null);
|
|
9364
9443
|
await self.setPendingSignal(activatedSignal, activatedSignal.priceOpen);
|
|
9365
9444
|
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
|
|
9445
|
+
await CALL_SIGNAL_EVENT_FN(self, "opened", activatedSignal, activatedSignal.priceOpen, activationTime);
|
|
9366
9446
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
9367
9447
|
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
|
|
9368
9448
|
return true;
|
|
@@ -9393,6 +9473,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
9393
9473
|
if (closeReason === "time_expired" && publicSignal.pnl.pnlPercentage < 0) {
|
|
9394
9474
|
self.params.logger.warn(`ClientStrategy backtest: Signal closed with loss (time_expired), PNL: ${publicSignal.pnl.pnlPercentage.toFixed(2)}%`);
|
|
9395
9475
|
}
|
|
9476
|
+
await CALL_SIGNAL_EVENT_FN(self, "closed", signal, averagePrice, closeTimestamp, closeReason);
|
|
9396
9477
|
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9397
9478
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
9398
9479
|
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, signal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
@@ -9451,6 +9532,7 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
|
|
|
9451
9532
|
signal: publicSignal,
|
|
9452
9533
|
note: closedSignal.closeNote ?? closedSignal.note,
|
|
9453
9534
|
});
|
|
9535
|
+
await CALL_SIGNAL_EVENT_FN(self, "closed", closedSignal, averagePrice, closeTimestamp, "closed");
|
|
9454
9536
|
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9455
9537
|
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9456
9538
|
await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, closedSignal, averagePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
@@ -9473,6 +9555,79 @@ const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, aver
|
|
|
9473
9555
|
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
9474
9556
|
return result;
|
|
9475
9557
|
};
|
|
9558
|
+
/**
|
|
9559
|
+
* Closes a deferred broker-confirmed TP/SL fill (createTakeProfit / createStopLoss).
|
|
9560
|
+
*
|
|
9561
|
+
* The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against VWAP,
|
|
9562
|
+
* but the real order may fill on candle high/low. When the broker confirms such a fill out of
|
|
9563
|
+
* context, the pending signal is snapshotted into _takeProfitSignal / _stopLossSignal and the next
|
|
9564
|
+
* tick()/backtest() drains it here, closing with the matching closeReason at the effective TP/SL
|
|
9565
|
+
* level (trailing override if set) — bypassing the VWAP completion check.
|
|
9566
|
+
*
|
|
9567
|
+
* Shared by live tick and backtest: the caller passes the close timestamp explicitly (execution
|
|
9568
|
+
* context when in live, candle timestamp in backtest). Mirrors CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN
|
|
9569
|
+
* (commit action "close-pending", carrying closeId/note) but with closeReason take_profit / stop_loss.
|
|
9570
|
+
*
|
|
9571
|
+
* Like the deferred close, the fill snapshot is already established by the broker, so it does NOT
|
|
9572
|
+
* re-confirm via onSignalSync. _takeProfitSignal / _stopLossSignal is cleared and re-persisted by
|
|
9573
|
+
* the caller after draining.
|
|
9574
|
+
*/
|
|
9575
|
+
const CLOSE_PENDING_SIGNAL_AS_FILL_FN = async (self, filledSignal, closeReason, closeTimestamp) => {
|
|
9576
|
+
const closePrice = closeReason === "take_profit"
|
|
9577
|
+
? (filledSignal._trailingPriceTakeProfit ?? filledSignal.priceTakeProfit)
|
|
9578
|
+
: (filledSignal._trailingPriceStopLoss ?? filledSignal.priceStopLoss);
|
|
9579
|
+
const publicSignal = TO_PUBLIC_SIGNAL("pending", filledSignal, closePrice);
|
|
9580
|
+
self.params.logger.info(`ClientStrategy signal ${closeReason} by broker-confirmed fill (createTakeProfit/createStopLoss)`, {
|
|
9581
|
+
symbol: self.params.execution.context.symbol,
|
|
9582
|
+
signalId: filledSignal.id,
|
|
9583
|
+
closeReason,
|
|
9584
|
+
priceClose: closePrice,
|
|
9585
|
+
pnlPercentage: publicSignal.pnl.pnlPercentage,
|
|
9586
|
+
});
|
|
9587
|
+
await CALL_COMMIT_FN(self, {
|
|
9588
|
+
action: "close-pending",
|
|
9589
|
+
symbol: self.params.execution.context.symbol,
|
|
9590
|
+
strategyName: self.params.strategyName,
|
|
9591
|
+
exchangeName: self.params.exchangeName,
|
|
9592
|
+
frameName: self.params.frameName,
|
|
9593
|
+
signalId: filledSignal.id,
|
|
9594
|
+
backtest: self.params.execution.context.backtest,
|
|
9595
|
+
closeId: filledSignal.closeId,
|
|
9596
|
+
timestamp: closeTimestamp,
|
|
9597
|
+
totalEntries: filledSignal._entry?.length ?? 1,
|
|
9598
|
+
totalPartials: filledSignal._partial?.length ?? 0,
|
|
9599
|
+
originalPriceOpen: filledSignal.priceOpen,
|
|
9600
|
+
pnl: publicSignal.pnl,
|
|
9601
|
+
maxDrawdown: publicSignal.maxDrawdown,
|
|
9602
|
+
peakProfit: publicSignal.peakProfit,
|
|
9603
|
+
signal: publicSignal,
|
|
9604
|
+
note: filledSignal.closeNote ?? filledSignal.note,
|
|
9605
|
+
});
|
|
9606
|
+
await CALL_SIGNAL_EVENT_FN(self, "closed", filledSignal, closePrice, closeTimestamp, closeReason);
|
|
9607
|
+
await CALL_CLOSE_CALLBACKS_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9608
|
+
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
9609
|
+
await CALL_PARTIAL_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9610
|
+
// КРИТИЧНО: Очищаем состояние ClientBreakeven при закрытии позиции
|
|
9611
|
+
await CALL_BREAKEVEN_CLEAR_FN(self, self.params.execution.context.symbol, filledSignal, closePrice, closeTimestamp, self.params.execution.context.backtest);
|
|
9612
|
+
await CALL_RISK_REMOVE_SIGNAL_FN(self, self.params.execution.context.symbol, closeTimestamp, self.params.execution.context.backtest);
|
|
9613
|
+
const result = {
|
|
9614
|
+
action: "closed",
|
|
9615
|
+
signal: publicSignal,
|
|
9616
|
+
currentPrice: closePrice,
|
|
9617
|
+
closeReason,
|
|
9618
|
+
closeTimestamp,
|
|
9619
|
+
pnl: publicSignal.pnl,
|
|
9620
|
+
strategyName: self.params.method.context.strategyName,
|
|
9621
|
+
exchangeName: self.params.method.context.exchangeName,
|
|
9622
|
+
frameName: self.params.method.context.frameName,
|
|
9623
|
+
symbol: self.params.execution.context.symbol,
|
|
9624
|
+
backtest: self.params.execution.context.backtest,
|
|
9625
|
+
closeId: filledSignal.closeId,
|
|
9626
|
+
createdAt: closeTimestamp,
|
|
9627
|
+
};
|
|
9628
|
+
await CALL_TICK_CALLBACKS_FN(self, self.params.execution.context.symbol, result, closeTimestamp, self.params.execution.context.backtest);
|
|
9629
|
+
return result;
|
|
9630
|
+
};
|
|
9476
9631
|
const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, frameEndTime) => {
|
|
9477
9632
|
const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
9478
9633
|
const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
|
|
@@ -9595,6 +9750,7 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles, fra
|
|
|
9595
9750
|
totalPartials: publicSignalForCommit.totalPartials,
|
|
9596
9751
|
note: activatedSignal.activateNote ?? publicSignalForCommit.note,
|
|
9597
9752
|
});
|
|
9753
|
+
await CALL_SIGNAL_EVENT_FN(self, "opened", pendingSignal, pendingSignal.priceOpen, candle.timestamp);
|
|
9598
9754
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
9599
9755
|
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
9600
9756
|
return { outcome: "activated", activationIndex: i };
|
|
@@ -9690,6 +9846,20 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
|
|
|
9690
9846
|
}
|
|
9691
9847
|
continue;
|
|
9692
9848
|
}
|
|
9849
|
+
// КРИТИЧНО: Проверяем broker-confirmed TP fill через createTakeProfit() (напр. из onActivePing).
|
|
9850
|
+
// Закрываем по эффективному уровню TP, минуя VWAP-проверку. Sync не пере-подтверждается.
|
|
9851
|
+
if (self._takeProfitSignal) {
|
|
9852
|
+
const filledSignal = self._takeProfitSignal;
|
|
9853
|
+
self._takeProfitSignal = null;
|
|
9854
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "take_profit", currentCandleTimestamp);
|
|
9855
|
+
}
|
|
9856
|
+
// КРИТИЧНО: Проверяем broker-confirmed SL fill через createStopLoss() (напр. из onActivePing).
|
|
9857
|
+
// Закрываем по эффективному уровню SL, минуя VWAP-проверку. Sync не пере-подтверждается.
|
|
9858
|
+
if (self._stopLossSignal) {
|
|
9859
|
+
const filledSignal = self._stopLossSignal;
|
|
9860
|
+
self._stopLossSignal = null;
|
|
9861
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(self, filledSignal, "stop_loss", currentCandleTimestamp);
|
|
9862
|
+
}
|
|
9693
9863
|
let shouldClose = false;
|
|
9694
9864
|
let closeReason;
|
|
9695
9865
|
// Check time expiration FIRST (КРИТИЧНО!)
|
|
@@ -9925,6 +10095,20 @@ class ClientStrategy {
|
|
|
9925
10095
|
this._cancelledSignal = null;
|
|
9926
10096
|
this._closedSignal = null;
|
|
9927
10097
|
this._activatedSignal = null;
|
|
10098
|
+
/**
|
|
10099
|
+
* Deferred broker-confirmed take-profit fill (set via createTakeProfit). When non-null, the
|
|
10100
|
+
* exchange reported the TP order was actually filled (e.g. by candle high/low) — the next
|
|
10101
|
+
* tick()/backtest() drains it and closes the pending position with closeReason "take_profit"
|
|
10102
|
+
* at the effective take-profit level, bypassing the VWAP-based TP check.
|
|
10103
|
+
*/
|
|
10104
|
+
this._takeProfitSignal = null;
|
|
10105
|
+
/**
|
|
10106
|
+
* Deferred broker-confirmed stop-loss fill (set via createStopLoss). When non-null, the
|
|
10107
|
+
* exchange reported the SL order was actually filled (e.g. by candle high/low) — the next
|
|
10108
|
+
* tick()/backtest() drains it and closes the pending position with closeReason "stop_loss"
|
|
10109
|
+
* at the effective stop-loss level, bypassing the VWAP-based SL check.
|
|
10110
|
+
*/
|
|
10111
|
+
this._stopLossSignal = null;
|
|
9928
10112
|
/**
|
|
9929
10113
|
* User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
|
|
9930
10114
|
* params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
|
|
@@ -9998,6 +10182,10 @@ class ClientStrategy {
|
|
|
9998
10182
|
// - при null: сигнал закрыт по TP/SL/timeout, флаг больше не нужен
|
|
9999
10183
|
// - при новом сигнале: флаг от предыдущего сигнала не должен влиять на новый
|
|
10000
10184
|
this._closedSignal = null;
|
|
10185
|
+
// КРИТИЧНО: Так же сбрасываем отложенные broker-confirmed TP/SL fills — закрытие позиции
|
|
10186
|
+
// любым путём делает их неактуальными, а новая позиция не должна унаследовать чужой fill.
|
|
10187
|
+
this._takeProfitSignal = null;
|
|
10188
|
+
this._stopLossSignal = null;
|
|
10001
10189
|
// ЗАЩИТА ИНВАРИАНТА: При установке нового pending сигнала очищаем scheduled
|
|
10002
10190
|
// Не может быть одновременно pending И scheduled (взаимоисключающие состояния)
|
|
10003
10191
|
// При null: scheduled может существовать (новый сигнал после закрытия позиции)
|
|
@@ -10891,6 +11079,7 @@ class ClientStrategy {
|
|
|
10891
11079
|
signal: publicSignal,
|
|
10892
11080
|
note: cancelledSignal.cancelNote ?? cancelledSignal.note,
|
|
10893
11081
|
});
|
|
11082
|
+
await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, currentTime, "user");
|
|
10894
11083
|
// Call onCancel callback
|
|
10895
11084
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
10896
11085
|
const result = {
|
|
@@ -10951,6 +11140,7 @@ class ClientStrategy {
|
|
|
10951
11140
|
signal: publicSignal,
|
|
10952
11141
|
note: closedSignal.closeNote ?? closedSignal.note,
|
|
10953
11142
|
});
|
|
11143
|
+
await CALL_SIGNAL_EVENT_FN(this, "closed", closedSignal, currentPrice, currentTime, "closed");
|
|
10954
11144
|
// Call onClose callback
|
|
10955
11145
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
10956
11146
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
@@ -10976,6 +11166,32 @@ class ClientStrategy {
|
|
|
10976
11166
|
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
|
|
10977
11167
|
return result;
|
|
10978
11168
|
}
|
|
11169
|
+
// Check if a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
|
|
11170
|
+
// The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
|
|
11171
|
+
if (this._takeProfitSignal) {
|
|
11172
|
+
const filledSignal = this._takeProfitSignal;
|
|
11173
|
+
this._takeProfitSignal = null; // Clear after draining
|
|
11174
|
+
// Persist the cleared deferred state so the drained flag is not replayed on restart
|
|
11175
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11176
|
+
this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed take-profit fill", {
|
|
11177
|
+
symbol: this.params.execution.context.symbol,
|
|
11178
|
+
signalId: filledSignal.id,
|
|
11179
|
+
});
|
|
11180
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", currentTime);
|
|
11181
|
+
}
|
|
11182
|
+
// Check if a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
|
|
11183
|
+
// The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
|
|
11184
|
+
if (this._stopLossSignal) {
|
|
11185
|
+
const filledSignal = this._stopLossSignal;
|
|
11186
|
+
this._stopLossSignal = null; // Clear after draining
|
|
11187
|
+
// Persist the cleared deferred state so the drained flag is not replayed on restart
|
|
11188
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11189
|
+
this.params.logger.info("ClientStrategy tick: pending signal closed by broker-confirmed stop-loss fill", {
|
|
11190
|
+
symbol: this.params.execution.context.symbol,
|
|
11191
|
+
signalId: filledSignal.id,
|
|
11192
|
+
});
|
|
11193
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", currentTime);
|
|
11194
|
+
}
|
|
10979
11195
|
// Check if scheduled signal was activated - emit opened event once
|
|
10980
11196
|
if (this._activatedSignal) {
|
|
10981
11197
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
@@ -11076,6 +11292,7 @@ class ClientStrategy {
|
|
|
11076
11292
|
totalPartials: publicSignalForCommit.totalPartials,
|
|
11077
11293
|
note: activatedSignal.activateNote ?? publicSignalForCommit.note,
|
|
11078
11294
|
});
|
|
11295
|
+
await CALL_SIGNAL_EVENT_FN(this, "opened", pendingSignal, currentPrice, currentTime);
|
|
11079
11296
|
// Call onOpen callback
|
|
11080
11297
|
await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
11081
11298
|
const result = {
|
|
@@ -11226,6 +11443,7 @@ class ClientStrategy {
|
|
|
11226
11443
|
signal: publicSignal,
|
|
11227
11444
|
note: cancelledSignal.cancelNote ?? cancelledSignal.note,
|
|
11228
11445
|
});
|
|
11446
|
+
await CALL_SCHEDULE_EVENT_FN$1(this, "cancelled", cancelledSignal, currentPrice, closeTimestamp, "user");
|
|
11229
11447
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
11230
11448
|
const cancelledResult = {
|
|
11231
11449
|
action: "cancelled",
|
|
@@ -11311,6 +11529,24 @@ class ClientStrategy {
|
|
|
11311
11529
|
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, closedResult, closeTimestamp, this.params.execution.context.backtest);
|
|
11312
11530
|
return closedResult;
|
|
11313
11531
|
}
|
|
11532
|
+
// If a broker-confirmed take-profit fill is awaiting (createTakeProfit) - close once.
|
|
11533
|
+
// The exchange filled the TP order (e.g. by high/low); close bypassing the VWAP TP check.
|
|
11534
|
+
if (this._takeProfitSignal) {
|
|
11535
|
+
this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed take-profit fill");
|
|
11536
|
+
const filledSignal = this._takeProfitSignal;
|
|
11537
|
+
this._takeProfitSignal = null; // Clear after draining
|
|
11538
|
+
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
11539
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "take_profit", closeTimestamp);
|
|
11540
|
+
}
|
|
11541
|
+
// If a broker-confirmed stop-loss fill is awaiting (createStopLoss) - close once.
|
|
11542
|
+
// The exchange filled the SL order (e.g. by high/low); close bypassing the VWAP SL check.
|
|
11543
|
+
if (this._stopLossSignal) {
|
|
11544
|
+
this.params.logger.debug("ClientStrategy backtest: pending signal closed by broker-confirmed stop-loss fill");
|
|
11545
|
+
const filledSignal = this._stopLossSignal;
|
|
11546
|
+
this._stopLossSignal = null; // Clear after draining
|
|
11547
|
+
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
11548
|
+
return await CLOSE_PENDING_SIGNAL_AS_FILL_FN(this, filledSignal, "stop_loss", closeTimestamp);
|
|
11549
|
+
}
|
|
11314
11550
|
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
11315
11551
|
throw new Error("ClientStrategy backtest: no pending or scheduled signal");
|
|
11316
11552
|
}
|
|
@@ -11428,12 +11664,17 @@ class ClientStrategy {
|
|
|
11428
11664
|
hasActivatedSignal: this._activatedSignal !== null,
|
|
11429
11665
|
hasCancelledSignal: this._cancelledSignal !== null,
|
|
11430
11666
|
hasClosedSignal: this._closedSignal !== null,
|
|
11667
|
+
hasTakeProfitSignal: this._takeProfitSignal !== null,
|
|
11668
|
+
hasStopLossSignal: this._stopLossSignal !== null,
|
|
11431
11669
|
});
|
|
11432
11670
|
this._isStopped = true;
|
|
11433
11671
|
// Clear pending flags to start from clean state
|
|
11434
11672
|
// NOTE: _isStopped blocks NEW position opening, but allows:
|
|
11435
11673
|
// - cancelScheduled() / closePending() for graceful shutdown
|
|
11436
11674
|
// - Monitoring existing _pendingSignal until TP/SL/timeout
|
|
11675
|
+
// NOTE: _takeProfitSignal / _stopLossSignal are NOT cleared — a broker-confirmed fill
|
|
11676
|
+
// reflects a real exchange close that must still drain to emit the closed event, the
|
|
11677
|
+
// same way an existing _pendingSignal keeps being monitored until its natural close.
|
|
11437
11678
|
this._activatedSignal = null;
|
|
11438
11679
|
this._cancelledSignal = null;
|
|
11439
11680
|
this._closedSignal = null;
|
|
@@ -11651,9 +11892,97 @@ class ClientStrategy {
|
|
|
11651
11892
|
if (this._cancelledSignal) {
|
|
11652
11893
|
throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
|
|
11653
11894
|
}
|
|
11895
|
+
if (this._takeProfitSignal) {
|
|
11896
|
+
throw new Error(`ClientStrategy createSignal: a take-profit fill is awaiting for symbol=${symbol}`);
|
|
11897
|
+
}
|
|
11898
|
+
if (this._stopLossSignal) {
|
|
11899
|
+
throw new Error(`ClientStrategy createSignal: a stop-loss fill is awaiting for symbol=${symbol}`);
|
|
11900
|
+
}
|
|
11654
11901
|
this._userSignal = dto;
|
|
11655
11902
|
await PERSIST_STRATEGY_FN(this);
|
|
11656
11903
|
}
|
|
11904
|
+
/**
|
|
11905
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
11906
|
+
* (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based TP check.
|
|
11907
|
+
*
|
|
11908
|
+
* The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
|
|
11909
|
+
* VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
|
|
11910
|
+
* the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
|
|
11911
|
+
* into _takeProfitSignal and cleared, and the next tick()/backtest() drains it, closing the
|
|
11912
|
+
* position with closeReason "take_profit" at the effective take-profit level.
|
|
11913
|
+
*
|
|
11914
|
+
* No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
|
|
11915
|
+
* tick does not lose the deferred close.
|
|
11916
|
+
*
|
|
11917
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11918
|
+
* @param backtest - Whether running in backtest mode
|
|
11919
|
+
* @param payload - Optional commit id/note attached to the close
|
|
11920
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
11921
|
+
*/
|
|
11922
|
+
async createTakeProfit(symbol, backtest, payload) {
|
|
11923
|
+
const closeId = payload.id;
|
|
11924
|
+
this.params.logger.debug("ClientStrategy createTakeProfit", {
|
|
11925
|
+
symbol,
|
|
11926
|
+
hasPendingSignal: this._pendingSignal !== null,
|
|
11927
|
+
closeId,
|
|
11928
|
+
});
|
|
11929
|
+
// Snapshot the pending signal for the next tick/backtest to close with reason "take_profit".
|
|
11930
|
+
if (this._pendingSignal) {
|
|
11931
|
+
this._takeProfitSignal = Object.assign({}, this._pendingSignal, {
|
|
11932
|
+
closeId,
|
|
11933
|
+
closeNote: payload.note,
|
|
11934
|
+
});
|
|
11935
|
+
this._pendingSignal = null;
|
|
11936
|
+
}
|
|
11937
|
+
if (backtest) {
|
|
11938
|
+
// Drained in backtest() with correct candle timestamp; no live persistence.
|
|
11939
|
+
return;
|
|
11940
|
+
}
|
|
11941
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
11942
|
+
// Persist deferred _takeProfitSignal so a crash before the next tick does not lose it
|
|
11943
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11944
|
+
}
|
|
11945
|
+
/**
|
|
11946
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
11947
|
+
* (e.g. by candle high/low), forcing a close that does not wait for the VWAP-based SL check.
|
|
11948
|
+
*
|
|
11949
|
+
* The exchange and the strategy are parallel states: ClientStrategy evaluates TP/SL against
|
|
11950
|
+
* VWAP, but the real order may close on high/low. This bridges that gap — the broker confirms
|
|
11951
|
+
* the fill OUT of the async-hooks execution context, the current pending signal is snapshotted
|
|
11952
|
+
* into _stopLossSignal and cleared, and the next tick()/backtest() drains it, closing the
|
|
11953
|
+
* position with closeReason "stop_loss" at the effective stop-loss level.
|
|
11954
|
+
*
|
|
11955
|
+
* No-op if no pending signal exists. Persisted (live mode only) so a crash before the next
|
|
11956
|
+
* tick does not lose the deferred close.
|
|
11957
|
+
*
|
|
11958
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11959
|
+
* @param backtest - Whether running in backtest mode
|
|
11960
|
+
* @param payload - Optional commit id/note attached to the close
|
|
11961
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
11962
|
+
*/
|
|
11963
|
+
async createStopLoss(symbol, backtest, payload) {
|
|
11964
|
+
const closeId = payload.id;
|
|
11965
|
+
this.params.logger.debug("ClientStrategy createStopLoss", {
|
|
11966
|
+
symbol,
|
|
11967
|
+
hasPendingSignal: this._pendingSignal !== null,
|
|
11968
|
+
closeId,
|
|
11969
|
+
});
|
|
11970
|
+
// Snapshot the pending signal for the next tick/backtest to close with reason "stop_loss".
|
|
11971
|
+
if (this._pendingSignal) {
|
|
11972
|
+
this._stopLossSignal = Object.assign({}, this._pendingSignal, {
|
|
11973
|
+
closeId,
|
|
11974
|
+
closeNote: payload.note,
|
|
11975
|
+
});
|
|
11976
|
+
this._pendingSignal = null;
|
|
11977
|
+
}
|
|
11978
|
+
if (backtest) {
|
|
11979
|
+
// Drained in backtest() with correct candle timestamp; no live persistence.
|
|
11980
|
+
return;
|
|
11981
|
+
}
|
|
11982
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
11983
|
+
// Persist deferred _stopLossSignal so a crash before the next tick does not lose it
|
|
11984
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11985
|
+
}
|
|
11657
11986
|
/**
|
|
11658
11987
|
* Returns the deferred strategy-state snapshot exactly as it would be written to persist on
|
|
11659
11988
|
* this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
|
|
@@ -11673,6 +12002,8 @@ class ClientStrategy {
|
|
|
11673
12002
|
closedSignal: this._closedSignal,
|
|
11674
12003
|
cancelledSignal: this._cancelledSignal,
|
|
11675
12004
|
activatedSignal: this._activatedSignal,
|
|
12005
|
+
takeProfitSignal: this._takeProfitSignal,
|
|
12006
|
+
stopLossSignal: this._stopLossSignal,
|
|
11676
12007
|
};
|
|
11677
12008
|
}
|
|
11678
12009
|
/**
|
|
@@ -13011,7 +13342,7 @@ const CREATE_SYNC_PENDING_FN = (self, strategyName, exchangeName, frameName, bac
|
|
|
13011
13342
|
return true;
|
|
13012
13343
|
}
|
|
13013
13344
|
await syncPendingSubject.next(event);
|
|
13014
|
-
await self.actionCoreService.
|
|
13345
|
+
await self.actionCoreService.orderCheck(backtest, event, { strategyName, exchangeName, frameName });
|
|
13015
13346
|
return true;
|
|
13016
13347
|
}, {
|
|
13017
13348
|
fallback: (error) => {
|
|
@@ -13165,6 +13496,81 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
|
|
|
13165
13496
|
},
|
|
13166
13497
|
defaultValue: null,
|
|
13167
13498
|
});
|
|
13499
|
+
/**
|
|
13500
|
+
* Creates a callback function for emitting scheduled signal lifecycle events to scheduleEventSubject.
|
|
13501
|
+
*
|
|
13502
|
+
* Called by ClientStrategy when a scheduled signal is created (action "scheduled") or cancelled
|
|
13503
|
+
* before activation (action "cancelled": timeout / price_reject / user). The scheduled -> active
|
|
13504
|
+
* transition is intentionally NOT reported here.
|
|
13505
|
+
*
|
|
13506
|
+
* @param self - Reference to StrategyConnectionService instance
|
|
13507
|
+
* @returns Callback function for scheduled signal lifecycle events
|
|
13508
|
+
*/
|
|
13509
|
+
const CREATE_COMMIT_SCHEDULE_EVENT_FN = (self) => functoolsKit.trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, reason) => {
|
|
13510
|
+
const event = {
|
|
13511
|
+
action,
|
|
13512
|
+
symbol,
|
|
13513
|
+
strategyName,
|
|
13514
|
+
exchangeName,
|
|
13515
|
+
frameName: data.frameName,
|
|
13516
|
+
data,
|
|
13517
|
+
reason,
|
|
13518
|
+
currentPrice,
|
|
13519
|
+
backtest,
|
|
13520
|
+
timestamp,
|
|
13521
|
+
};
|
|
13522
|
+
await scheduleEventSubject.next(event);
|
|
13523
|
+
await self.actionCoreService.scheduleEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
|
|
13524
|
+
}, {
|
|
13525
|
+
fallback: (error) => {
|
|
13526
|
+
const message = "StrategyConnectionService CREATE_COMMIT_SCHEDULE_EVENT_FN thrown";
|
|
13527
|
+
const payload = {
|
|
13528
|
+
error: functoolsKit.errorData(error),
|
|
13529
|
+
message: functoolsKit.getErrorMessage(error),
|
|
13530
|
+
};
|
|
13531
|
+
self.loggerService.warn(message, payload);
|
|
13532
|
+
console.warn(message, payload);
|
|
13533
|
+
errorEmitter.next(error);
|
|
13534
|
+
},
|
|
13535
|
+
defaultValue: null,
|
|
13536
|
+
});
|
|
13537
|
+
/**
|
|
13538
|
+
* Creates a callback function for emitting pending signal lifecycle events to signalEventSubject.
|
|
13539
|
+
*
|
|
13540
|
+
* Called by ClientStrategy when a pending position is opened (action "opened") or closed
|
|
13541
|
+
* (action "closed" with a closeReason).
|
|
13542
|
+
*
|
|
13543
|
+
* @param self - Reference to StrategyConnectionService instance
|
|
13544
|
+
* @returns Callback function for pending signal lifecycle events
|
|
13545
|
+
*/
|
|
13546
|
+
const CREATE_COMMIT_SIGNAL_EVENT_FN = (self) => functoolsKit.trycatch(async (action, symbol, strategyName, exchangeName, data, currentPrice, backtest, timestamp, closeReason) => {
|
|
13547
|
+
const event = {
|
|
13548
|
+
action,
|
|
13549
|
+
symbol,
|
|
13550
|
+
strategyName,
|
|
13551
|
+
exchangeName,
|
|
13552
|
+
frameName: data.frameName,
|
|
13553
|
+
data,
|
|
13554
|
+
closeReason,
|
|
13555
|
+
currentPrice,
|
|
13556
|
+
backtest,
|
|
13557
|
+
timestamp,
|
|
13558
|
+
};
|
|
13559
|
+
await signalEventSubject.next(event);
|
|
13560
|
+
await self.actionCoreService.pendingEvent(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
|
|
13561
|
+
}, {
|
|
13562
|
+
fallback: (error) => {
|
|
13563
|
+
const message = "StrategyConnectionService CREATE_COMMIT_SIGNAL_EVENT_FN thrown";
|
|
13564
|
+
const payload = {
|
|
13565
|
+
error: functoolsKit.errorData(error),
|
|
13566
|
+
message: functoolsKit.getErrorMessage(error),
|
|
13567
|
+
};
|
|
13568
|
+
self.loggerService.warn(message, payload);
|
|
13569
|
+
console.warn(message, payload);
|
|
13570
|
+
errorEmitter.next(error);
|
|
13571
|
+
},
|
|
13572
|
+
defaultValue: null,
|
|
13573
|
+
});
|
|
13168
13574
|
/**
|
|
13169
13575
|
* Creates a callback function for emitting idle ping events.
|
|
13170
13576
|
*
|
|
@@ -13457,6 +13863,8 @@ class StrategyConnectionService {
|
|
|
13457
13863
|
callbacks,
|
|
13458
13864
|
onInit: CREATE_COMMIT_INIT_FN(this),
|
|
13459
13865
|
onSchedulePing: CREATE_COMMIT_SCHEDULE_PING_FN(this),
|
|
13866
|
+
onScheduleEvent: CREATE_COMMIT_SCHEDULE_EVENT_FN(this),
|
|
13867
|
+
onSignalEvent: CREATE_COMMIT_SIGNAL_EVENT_FN(this),
|
|
13460
13868
|
onActivePing: CREATE_COMMIT_ACTIVE_PING_FN(this),
|
|
13461
13869
|
onIdlePing: CREATE_COMMIT_IDLE_PING_FN(this),
|
|
13462
13870
|
onDispose: CREATE_COMMIT_DISPOSE_FN(this),
|
|
@@ -14476,6 +14884,50 @@ class StrategyConnectionService {
|
|
|
14476
14884
|
const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
|
|
14477
14885
|
await strategy.createSignal(symbol, currentPrice, dto);
|
|
14478
14886
|
};
|
|
14887
|
+
/**
|
|
14888
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
14889
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
|
|
14890
|
+
*
|
|
14891
|
+
* Delegates to ClientStrategy.createTakeProfit(). The close is deferred and emitted with
|
|
14892
|
+
* closeReason "take_profit" on the next tick()/backtest(). Works out of the execution context.
|
|
14893
|
+
*
|
|
14894
|
+
* @param backtest - Whether running in backtest mode
|
|
14895
|
+
* @param symbol - Trading pair symbol
|
|
14896
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14897
|
+
* @param payload - Optional commit payload with id and note
|
|
14898
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
14899
|
+
*/
|
|
14900
|
+
this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
|
|
14901
|
+
this.loggerService.log("strategyConnectionService createTakeProfit", {
|
|
14902
|
+
symbol,
|
|
14903
|
+
context,
|
|
14904
|
+
payload,
|
|
14905
|
+
});
|
|
14906
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14907
|
+
await strategy.createTakeProfit(symbol, backtest, payload);
|
|
14908
|
+
};
|
|
14909
|
+
/**
|
|
14910
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
14911
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
|
|
14912
|
+
*
|
|
14913
|
+
* Delegates to ClientStrategy.createStopLoss(). The close is deferred and emitted with
|
|
14914
|
+
* closeReason "stop_loss" on the next tick()/backtest(). Works out of the execution context.
|
|
14915
|
+
*
|
|
14916
|
+
* @param backtest - Whether running in backtest mode
|
|
14917
|
+
* @param symbol - Trading pair symbol
|
|
14918
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14919
|
+
* @param payload - Optional commit payload with id and note
|
|
14920
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
14921
|
+
*/
|
|
14922
|
+
this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
|
|
14923
|
+
this.loggerService.log("strategyConnectionService createStopLoss", {
|
|
14924
|
+
symbol,
|
|
14925
|
+
context,
|
|
14926
|
+
payload,
|
|
14927
|
+
});
|
|
14928
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14929
|
+
await strategy.createStopLoss(symbol, backtest, payload);
|
|
14930
|
+
};
|
|
14479
14931
|
/**
|
|
14480
14932
|
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
14481
14933
|
*
|
|
@@ -16119,6 +16571,54 @@ const CALL_PING_IDLE_FN = functoolsKit.trycatch(async (event, self) => {
|
|
|
16119
16571
|
},
|
|
16120
16572
|
defaultValue: null,
|
|
16121
16573
|
});
|
|
16574
|
+
/**
|
|
16575
|
+
* Wrapper to call scheduleEvent method with error capture.
|
|
16576
|
+
*
|
|
16577
|
+
* Unlike CALL_PING_SCHEDULED_FN there is no hasScheduledSignal guard: the "cancelled" action fires
|
|
16578
|
+
* exactly while the scheduled signal is being removed, so guarding on its presence would suppress it.
|
|
16579
|
+
*/
|
|
16580
|
+
const CALL_SCHEDULE_EVENT_FN = functoolsKit.trycatch(async (event, self) => {
|
|
16581
|
+
if (!self._target.scheduleEvent) {
|
|
16582
|
+
return;
|
|
16583
|
+
}
|
|
16584
|
+
return await self._target.scheduleEvent(event);
|
|
16585
|
+
}, {
|
|
16586
|
+
fallback: (error) => {
|
|
16587
|
+
const message = "ActionProxy.scheduleEvent thrown";
|
|
16588
|
+
const payload = {
|
|
16589
|
+
error: functoolsKit.errorData(error),
|
|
16590
|
+
message: functoolsKit.getErrorMessage(error),
|
|
16591
|
+
};
|
|
16592
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
16593
|
+
console.warn(message, payload);
|
|
16594
|
+
errorEmitter.next(error);
|
|
16595
|
+
},
|
|
16596
|
+
defaultValue: null,
|
|
16597
|
+
});
|
|
16598
|
+
/**
|
|
16599
|
+
* Wrapper to call pendingEvent method with error capture.
|
|
16600
|
+
*
|
|
16601
|
+
* Like CALL_SCHEDULE_EVENT_FN there is no hasPendingSignal guard: the "closed" action fires exactly
|
|
16602
|
+
* while the pending signal is being removed, so guarding on its presence would suppress it.
|
|
16603
|
+
*/
|
|
16604
|
+
const CALL_PENDING_EVENT_FN = functoolsKit.trycatch(async (event, self) => {
|
|
16605
|
+
if (!self._target.pendingEvent) {
|
|
16606
|
+
return;
|
|
16607
|
+
}
|
|
16608
|
+
return await self._target.pendingEvent(event);
|
|
16609
|
+
}, {
|
|
16610
|
+
fallback: (error) => {
|
|
16611
|
+
const message = "ActionProxy.pendingEvent thrown";
|
|
16612
|
+
const payload = {
|
|
16613
|
+
error: functoolsKit.errorData(error),
|
|
16614
|
+
message: functoolsKit.getErrorMessage(error),
|
|
16615
|
+
};
|
|
16616
|
+
LOGGER_SERVICE$5.warn(message, payload);
|
|
16617
|
+
console.warn(message, payload);
|
|
16618
|
+
errorEmitter.next(error);
|
|
16619
|
+
},
|
|
16620
|
+
defaultValue: null,
|
|
16621
|
+
});
|
|
16122
16622
|
/**
|
|
16123
16623
|
* Wrapper to call pingActive method with error capture.
|
|
16124
16624
|
*/
|
|
@@ -16345,6 +16845,31 @@ class ActionProxy {
|
|
|
16345
16845
|
async pingScheduled(event) {
|
|
16346
16846
|
return await CALL_PING_SCHEDULED_FN(event, this);
|
|
16347
16847
|
}
|
|
16848
|
+
/**
|
|
16849
|
+
* Handles scheduled signal lifecycle events with error capture.
|
|
16850
|
+
*
|
|
16851
|
+
* Wraps the user's scheduleEvent() method to catch and log any errors. Called once when a
|
|
16852
|
+
* scheduled signal is created (action "scheduled") and once when it is cancelled before
|
|
16853
|
+
* activation (action "cancelled").
|
|
16854
|
+
*
|
|
16855
|
+
* @param event - Scheduled lifecycle data (action discriminates created vs cancelled)
|
|
16856
|
+
* @returns Promise resolving to user's scheduleEvent() result or null on error
|
|
16857
|
+
*/
|
|
16858
|
+
async scheduleEvent(event) {
|
|
16859
|
+
return await CALL_SCHEDULE_EVENT_FN(event, this);
|
|
16860
|
+
}
|
|
16861
|
+
/**
|
|
16862
|
+
* Handles pending signal lifecycle events with error capture.
|
|
16863
|
+
*
|
|
16864
|
+
* Wraps the user's pendingEvent() method to catch and log any errors. Called once when a
|
|
16865
|
+
* pending position is opened (action "opened") and once when it is closed (action "closed").
|
|
16866
|
+
*
|
|
16867
|
+
* @param event - Pending lifecycle data (action discriminates opened vs closed)
|
|
16868
|
+
* @returns Promise resolving to user's pendingEvent() result or null on error
|
|
16869
|
+
*/
|
|
16870
|
+
async pendingEvent(event) {
|
|
16871
|
+
return await CALL_PENDING_EVENT_FN(event, this);
|
|
16872
|
+
}
|
|
16348
16873
|
/**
|
|
16349
16874
|
* Handles active ping events with error capture.
|
|
16350
16875
|
*
|
|
@@ -16403,14 +16928,14 @@ class ActionProxy {
|
|
|
16403
16928
|
*
|
|
16404
16929
|
* @param event - Pending-ping event with action "signal-ping"
|
|
16405
16930
|
*/
|
|
16406
|
-
async
|
|
16407
|
-
if (this._target.
|
|
16408
|
-
console.error("Action::
|
|
16409
|
-
console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with
|
|
16410
|
-
console.error("If Action::
|
|
16931
|
+
async orderCheck(event) {
|
|
16932
|
+
if (this._target.orderCheck) {
|
|
16933
|
+
console.error("Action::orderCheck is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
|
|
16934
|
+
console.error("If you need to check whether the order is still open on the exchange, please use Broker.useBrokerAdapter with onOrderCheck");
|
|
16935
|
+
console.error("If Action::orderCheck throws the framework will close the position with closeReason \"closed\"!");
|
|
16411
16936
|
console.error("");
|
|
16412
16937
|
console.error("You have been warned!");
|
|
16413
|
-
await this._target.
|
|
16938
|
+
await this._target.orderCheck(event);
|
|
16414
16939
|
}
|
|
16415
16940
|
}
|
|
16416
16941
|
/**
|
|
@@ -16572,6 +17097,40 @@ const CALL_PING_SCHEDULED_CALLBACK_FN = functoolsKit.trycatch(async (self, event
|
|
|
16572
17097
|
errorEmitter.next(error);
|
|
16573
17098
|
},
|
|
16574
17099
|
});
|
|
17100
|
+
/** Wrapper to call scheduled lifecycle (creation / cancellation) callback with error handling */
|
|
17101
|
+
const CALL_SCHEDULE_EVENT_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
|
|
17102
|
+
if (self.params.callbacks?.onScheduleEvent) {
|
|
17103
|
+
await self.params.callbacks.onScheduleEvent(event, self.params.actionName, strategyName, frameName, backtest);
|
|
17104
|
+
}
|
|
17105
|
+
}, {
|
|
17106
|
+
fallback: (error, self) => {
|
|
17107
|
+
const message = "ClientAction CALL_SCHEDULE_EVENT_CALLBACK_FN thrown";
|
|
17108
|
+
const payload = {
|
|
17109
|
+
error: functoolsKit.errorData(error),
|
|
17110
|
+
message: functoolsKit.getErrorMessage(error),
|
|
17111
|
+
};
|
|
17112
|
+
self.params.logger.warn(message, payload);
|
|
17113
|
+
console.warn(message, payload);
|
|
17114
|
+
errorEmitter.next(error);
|
|
17115
|
+
},
|
|
17116
|
+
});
|
|
17117
|
+
/** Wrapper to call pending lifecycle (open / close) callback with error handling */
|
|
17118
|
+
const CALL_PENDING_EVENT_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
|
|
17119
|
+
if (self.params.callbacks?.onPendingEvent) {
|
|
17120
|
+
await self.params.callbacks.onPendingEvent(event, self.params.actionName, strategyName, frameName, backtest);
|
|
17121
|
+
}
|
|
17122
|
+
}, {
|
|
17123
|
+
fallback: (error, self) => {
|
|
17124
|
+
const message = "ClientAction CALL_PENDING_EVENT_CALLBACK_FN thrown";
|
|
17125
|
+
const payload = {
|
|
17126
|
+
error: functoolsKit.errorData(error),
|
|
17127
|
+
message: functoolsKit.getErrorMessage(error),
|
|
17128
|
+
};
|
|
17129
|
+
self.params.logger.warn(message, payload);
|
|
17130
|
+
console.warn(message, payload);
|
|
17131
|
+
errorEmitter.next(error);
|
|
17132
|
+
},
|
|
17133
|
+
});
|
|
16575
17134
|
/** Wrapper to call idle ping callback with error handling */
|
|
16576
17135
|
const CALL_PING_IDLE_CALLBACK_FN = functoolsKit.trycatch(async (self, event, strategyName, frameName, backtest) => {
|
|
16577
17136
|
if (self.params.callbacks?.onPingIdle) {
|
|
@@ -16633,12 +17192,12 @@ const CALL_SIGNAL_SYNC_CALLBACK_FN = async (self, event, strategyName, frameName
|
|
|
16633
17192
|
}
|
|
16634
17193
|
};
|
|
16635
17194
|
/**
|
|
16636
|
-
* Calls
|
|
17195
|
+
* Calls onOrderCheck callback WITHOUT trycatch — exceptions must propagate
|
|
16637
17196
|
* up to CREATE_SYNC_PENDING_FN in StrategyConnectionService (which returns false on error).
|
|
16638
17197
|
*/
|
|
16639
17198
|
const CALL_ORDER_PING_CALLBACK_FN = async (self, event, strategyName, frameName, isBacktest) => {
|
|
16640
|
-
if (self.params.callbacks?.
|
|
16641
|
-
await self.params.callbacks.
|
|
17199
|
+
if (self.params.callbacks?.onOrderCheck) {
|
|
17200
|
+
await self.params.callbacks.onOrderCheck(event, self.params.actionName, strategyName, frameName, isBacktest);
|
|
16642
17201
|
}
|
|
16643
17202
|
};
|
|
16644
17203
|
/** Wrapper to call onInit callback with error handling */
|
|
@@ -16946,6 +17505,52 @@ class ClientAction {
|
|
|
16946
17505
|
await CALL_PING_SCHEDULED_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
|
|
16947
17506
|
}
|
|
16948
17507
|
;
|
|
17508
|
+
/**
|
|
17509
|
+
* Handles scheduled signal lifecycle events (creation / cancellation).
|
|
17510
|
+
*
|
|
17511
|
+
* Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onScheduleEvent} (via `addActionSchema`)
|
|
17512
|
+
* to drive the exchange (`commitActivateScheduled` / `commitCancelScheduled`); see that contract
|
|
17513
|
+
* for the full guidance and example. This internal dispatch forwards to the handler/callback.
|
|
17514
|
+
*/
|
|
17515
|
+
async scheduleEvent(event) {
|
|
17516
|
+
this.params.logger.debug("ClientAction scheduleEvent", {
|
|
17517
|
+
actionName: this.params.actionName,
|
|
17518
|
+
strategyName: this.params.strategyName,
|
|
17519
|
+
frameName: this.params.frameName,
|
|
17520
|
+
action: event.action,
|
|
17521
|
+
});
|
|
17522
|
+
if (!this._handlerInstance) {
|
|
17523
|
+
await this.waitForInit();
|
|
17524
|
+
}
|
|
17525
|
+
// Call handler method if defined
|
|
17526
|
+
await this._handlerInstance?.scheduleEvent(event);
|
|
17527
|
+
// Call callback if defined
|
|
17528
|
+
await CALL_SCHEDULE_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
|
|
17529
|
+
}
|
|
17530
|
+
;
|
|
17531
|
+
/**
|
|
17532
|
+
* Handles pending signal lifecycle events (open / close).
|
|
17533
|
+
*
|
|
17534
|
+
* Manual wiring — EVENT-BASED: users implement {@link IActionCallbacks.onPendingEvent} (via `addActionSchema`) to
|
|
17535
|
+
* drive the exchange; for per-tick fills use `onPingActive`. See that contract for the full
|
|
17536
|
+
* guidance and example. This internal dispatch forwards to the handler/callback.
|
|
17537
|
+
*/
|
|
17538
|
+
async pendingEvent(event) {
|
|
17539
|
+
this.params.logger.debug("ClientAction pendingEvent", {
|
|
17540
|
+
actionName: this.params.actionName,
|
|
17541
|
+
strategyName: this.params.strategyName,
|
|
17542
|
+
frameName: this.params.frameName,
|
|
17543
|
+
action: event.action,
|
|
17544
|
+
});
|
|
17545
|
+
if (!this._handlerInstance) {
|
|
17546
|
+
await this.waitForInit();
|
|
17547
|
+
}
|
|
17548
|
+
// Call handler method if defined
|
|
17549
|
+
await this._handlerInstance?.pendingEvent(event);
|
|
17550
|
+
// Call callback if defined
|
|
17551
|
+
await CALL_PENDING_EVENT_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
|
|
17552
|
+
}
|
|
17553
|
+
;
|
|
16949
17554
|
/**
|
|
16950
17555
|
* Handles active ping events during active pending signal monitoring.
|
|
16951
17556
|
*/
|
|
@@ -17023,8 +17628,8 @@ class ClientAction {
|
|
|
17023
17628
|
* Gate for the pending-order ping (order still open on exchange?).
|
|
17024
17629
|
* NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
|
|
17025
17630
|
*/
|
|
17026
|
-
async
|
|
17027
|
-
this.params.logger.debug("ClientAction
|
|
17631
|
+
async orderCheck(event) {
|
|
17632
|
+
this.params.logger.debug("ClientAction orderCheck", {
|
|
17028
17633
|
actionName: this.params.actionName,
|
|
17029
17634
|
strategyName: this.params.strategyName,
|
|
17030
17635
|
frameName: this.params.frameName,
|
|
@@ -17033,7 +17638,7 @@ class ClientAction {
|
|
|
17033
17638
|
await this.waitForInit();
|
|
17034
17639
|
}
|
|
17035
17640
|
// Call handler method if defined — exceptions propagate
|
|
17036
|
-
await this._handlerInstance?.
|
|
17641
|
+
await this._handlerInstance?.orderCheck(event);
|
|
17037
17642
|
// Call callback if defined — exceptions propagate
|
|
17038
17643
|
await CALL_ORDER_PING_CALLBACK_FN(this, event, this.params.strategyName, this.params.frameName, event.backtest);
|
|
17039
17644
|
}
|
|
@@ -17238,6 +17843,36 @@ class ActionConnectionService {
|
|
|
17238
17843
|
const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
17239
17844
|
await action.pingScheduled(event);
|
|
17240
17845
|
};
|
|
17846
|
+
/**
|
|
17847
|
+
* Routes a scheduled signal lifecycle event (creation / cancellation) to the ClientAction instance.
|
|
17848
|
+
*
|
|
17849
|
+
* @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
|
|
17850
|
+
* @param backtest - Whether running in backtest mode
|
|
17851
|
+
* @param context - Execution context with action name, strategy name, exchange name, frame name
|
|
17852
|
+
*/
|
|
17853
|
+
this.scheduleEvent = async (event, backtest, context) => {
|
|
17854
|
+
this.loggerService.log("actionConnectionService scheduleEvent", {
|
|
17855
|
+
backtest,
|
|
17856
|
+
context,
|
|
17857
|
+
});
|
|
17858
|
+
const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
17859
|
+
await action.scheduleEvent(event);
|
|
17860
|
+
};
|
|
17861
|
+
/**
|
|
17862
|
+
* Routes a pending signal lifecycle event (open / close) to the ClientAction instance.
|
|
17863
|
+
*
|
|
17864
|
+
* @param event - Pending lifecycle event data (action discriminates opened vs closed)
|
|
17865
|
+
* @param backtest - Whether running in backtest mode
|
|
17866
|
+
* @param context - Execution context with action name, strategy name, exchange name, frame name
|
|
17867
|
+
*/
|
|
17868
|
+
this.pendingEvent = async (event, backtest, context) => {
|
|
17869
|
+
this.loggerService.log("actionConnectionService pendingEvent", {
|
|
17870
|
+
backtest,
|
|
17871
|
+
context,
|
|
17872
|
+
});
|
|
17873
|
+
const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
17874
|
+
await action.pendingEvent(event);
|
|
17875
|
+
};
|
|
17241
17876
|
/**
|
|
17242
17877
|
* Routes active ping event to appropriate ClientAction instance.
|
|
17243
17878
|
*
|
|
@@ -17301,20 +17936,20 @@ class ActionConnectionService {
|
|
|
17301
17936
|
await action.signalSync(event);
|
|
17302
17937
|
};
|
|
17303
17938
|
/**
|
|
17304
|
-
* Routes
|
|
17939
|
+
* Routes orderCheck event to appropriate ClientAction instance.
|
|
17305
17940
|
* NOT wrapped in trycatch — exceptions propagate to CREATE_SYNC_PENDING_FN.
|
|
17306
17941
|
*
|
|
17307
17942
|
* @param event - Pending-ping event with action "signal-ping"
|
|
17308
17943
|
* @param backtest - Whether running in backtest mode
|
|
17309
17944
|
* @param context - Execution context
|
|
17310
17945
|
*/
|
|
17311
|
-
this.
|
|
17312
|
-
this.loggerService.log("actionConnectionService
|
|
17946
|
+
this.orderCheck = async (event, backtest, context) => {
|
|
17947
|
+
this.loggerService.log("actionConnectionService orderCheck", {
|
|
17313
17948
|
backtest,
|
|
17314
17949
|
context,
|
|
17315
17950
|
});
|
|
17316
17951
|
const action = this.getAction(context.actionName, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
17317
|
-
await action.
|
|
17952
|
+
await action.orderCheck(event);
|
|
17318
17953
|
};
|
|
17319
17954
|
/**
|
|
17320
17955
|
* Disposes the ClientAction instance for the given action name.
|
|
@@ -18182,6 +18817,54 @@ class StrategyCoreService {
|
|
|
18182
18817
|
await this.validate(context);
|
|
18183
18818
|
return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
|
|
18184
18819
|
};
|
|
18820
|
+
/**
|
|
18821
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
18822
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
|
|
18823
|
+
*
|
|
18824
|
+
* Validates the context, then delegates to StrategyConnectionService.createTakeProfit().
|
|
18825
|
+
* The close is deferred and emitted with closeReason "take_profit" on the next tick/backtest.
|
|
18826
|
+
* Does not require execution context.
|
|
18827
|
+
*
|
|
18828
|
+
* @param backtest - Whether running in backtest mode
|
|
18829
|
+
* @param symbol - Trading pair symbol
|
|
18830
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
18831
|
+
* @param payload - Optional commit payload with id and note
|
|
18832
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
18833
|
+
*/
|
|
18834
|
+
this.createTakeProfit = async (backtest, symbol, context, payload = {}) => {
|
|
18835
|
+
this.loggerService.log("strategyCoreService createTakeProfit", {
|
|
18836
|
+
symbol,
|
|
18837
|
+
context,
|
|
18838
|
+
backtest,
|
|
18839
|
+
payload,
|
|
18840
|
+
});
|
|
18841
|
+
await this.validate(context);
|
|
18842
|
+
return await this.strategyConnectionService.createTakeProfit(backtest, symbol, context, payload);
|
|
18843
|
+
};
|
|
18844
|
+
/**
|
|
18845
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
18846
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
|
|
18847
|
+
*
|
|
18848
|
+
* Validates the context, then delegates to StrategyConnectionService.createStopLoss().
|
|
18849
|
+
* The close is deferred and emitted with closeReason "stop_loss" on the next tick/backtest.
|
|
18850
|
+
* Does not require execution context.
|
|
18851
|
+
*
|
|
18852
|
+
* @param backtest - Whether running in backtest mode
|
|
18853
|
+
* @param symbol - Trading pair symbol
|
|
18854
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
18855
|
+
* @param payload - Optional commit payload with id and note
|
|
18856
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
18857
|
+
*/
|
|
18858
|
+
this.createStopLoss = async (backtest, symbol, context, payload = {}) => {
|
|
18859
|
+
this.loggerService.log("strategyCoreService createStopLoss", {
|
|
18860
|
+
symbol,
|
|
18861
|
+
context,
|
|
18862
|
+
backtest,
|
|
18863
|
+
payload,
|
|
18864
|
+
});
|
|
18865
|
+
await this.validate(context);
|
|
18866
|
+
return await this.strategyConnectionService.createStopLoss(backtest, symbol, context, payload);
|
|
18867
|
+
};
|
|
18185
18868
|
/**
|
|
18186
18869
|
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
18187
18870
|
*
|
|
@@ -19458,6 +20141,48 @@ class ActionCoreService {
|
|
|
19458
20141
|
await this.actionConnectionService.pingScheduled(event, backtest, { actionName, ...context });
|
|
19459
20142
|
}
|
|
19460
20143
|
};
|
|
20144
|
+
/**
|
|
20145
|
+
* Routes a scheduled signal lifecycle event (creation / cancellation) to all registered actions.
|
|
20146
|
+
*
|
|
20147
|
+
* Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
|
|
20148
|
+
* scheduleEvent handler on each ClientAction instance sequentially. Called once on creation
|
|
20149
|
+
* (action "scheduled") and once on cancellation before activation (action "cancelled").
|
|
20150
|
+
*
|
|
20151
|
+
* @param backtest - Whether running in backtest mode (true) or live mode (false)
|
|
20152
|
+
* @param event - Scheduled lifecycle event data (action discriminates created vs cancelled)
|
|
20153
|
+
* @param context - Strategy execution context with strategyName, exchangeName, frameName
|
|
20154
|
+
*/
|
|
20155
|
+
this.scheduleEvent = async (backtest, event, context) => {
|
|
20156
|
+
this.loggerService.log("actionCoreService scheduleEvent", {
|
|
20157
|
+
context,
|
|
20158
|
+
});
|
|
20159
|
+
await this.validate(context);
|
|
20160
|
+
const { actions = [] } = this.strategySchemaService.get(context.strategyName);
|
|
20161
|
+
for (const actionName of actions) {
|
|
20162
|
+
await this.actionConnectionService.scheduleEvent(event, backtest, { actionName, ...context });
|
|
20163
|
+
}
|
|
20164
|
+
};
|
|
20165
|
+
/**
|
|
20166
|
+
* Routes a pending signal lifecycle event (open / close) to all registered actions.
|
|
20167
|
+
*
|
|
20168
|
+
* Retrieves action list from strategy schema (IStrategySchema.actions) and invokes the
|
|
20169
|
+
* pendingEvent handler on each ClientAction instance sequentially. Called once on open
|
|
20170
|
+
* (action "opened") and once on close (action "closed").
|
|
20171
|
+
*
|
|
20172
|
+
* @param backtest - Whether running in backtest mode (true) or live mode (false)
|
|
20173
|
+
* @param event - Pending lifecycle event data (action discriminates opened vs closed)
|
|
20174
|
+
* @param context - Strategy execution context with strategyName, exchangeName, frameName
|
|
20175
|
+
*/
|
|
20176
|
+
this.pendingEvent = async (backtest, event, context) => {
|
|
20177
|
+
this.loggerService.log("actionCoreService pendingEvent", {
|
|
20178
|
+
context,
|
|
20179
|
+
});
|
|
20180
|
+
await this.validate(context);
|
|
20181
|
+
const { actions = [] } = this.strategySchemaService.get(context.strategyName);
|
|
20182
|
+
for (const actionName of actions) {
|
|
20183
|
+
await this.actionConnectionService.pendingEvent(event, backtest, { actionName, ...context });
|
|
20184
|
+
}
|
|
20185
|
+
};
|
|
19461
20186
|
/**
|
|
19462
20187
|
* Routes active ping event to all registered actions for the strategy.
|
|
19463
20188
|
*
|
|
@@ -19548,14 +20273,14 @@ class ActionCoreService {
|
|
|
19548
20273
|
* @param event - Pending-ping event with action "signal-ping"
|
|
19549
20274
|
* @param context - Strategy execution context
|
|
19550
20275
|
*/
|
|
19551
|
-
this.
|
|
19552
|
-
this.loggerService.log("actionCoreService
|
|
20276
|
+
this.orderCheck = async (backtest, event, context) => {
|
|
20277
|
+
this.loggerService.log("actionCoreService orderCheck", {
|
|
19553
20278
|
context,
|
|
19554
20279
|
});
|
|
19555
20280
|
await this.validate(context);
|
|
19556
20281
|
const { actions = [] } = this.strategySchemaService.get(context.strategyName);
|
|
19557
20282
|
for (const actionName of actions) {
|
|
19558
|
-
await this.actionConnectionService.
|
|
20283
|
+
await this.actionConnectionService.orderCheck(event, backtest, { actionName, ...context });
|
|
19559
20284
|
}
|
|
19560
20285
|
};
|
|
19561
20286
|
/**
|
|
@@ -20023,6 +20748,8 @@ const VALID_METHOD_NAMES = [
|
|
|
20023
20748
|
"partialLossAvailable",
|
|
20024
20749
|
"pingScheduled",
|
|
20025
20750
|
"pingActive",
|
|
20751
|
+
"scheduleEvent",
|
|
20752
|
+
"pendingEvent",
|
|
20026
20753
|
"riskRejection",
|
|
20027
20754
|
"dispose",
|
|
20028
20755
|
];
|
|
@@ -20033,7 +20760,7 @@ const VALID_METHOD_NAMES = [
|
|
|
20033
20760
|
* method one of these produces a dedicated error redirecting to the Broker adapter instead of the
|
|
20034
20761
|
* generic "invalid method" suggestion.
|
|
20035
20762
|
*/
|
|
20036
|
-
const DISCOURAGED_METHOD_NAMES = ["signalSync", "
|
|
20763
|
+
const DISCOURAGED_METHOD_NAMES = ["signalSync", "orderCheck"];
|
|
20037
20764
|
/**
|
|
20038
20765
|
* Builds the dedicated error message for a discouraged exchange-integration handler method.
|
|
20039
20766
|
*
|
|
@@ -20045,7 +20772,7 @@ const DISCOURAGED_METHOD_MESSAGE = (actionName, methodName) => functoolsKit.str.
|
|
|
20045
20772
|
`ActionSchema ${actionName} contains discouraged method "${methodName}". `,
|
|
20046
20773
|
`Exchange integration must be implemented in Broker.useBrokerAdapter as the infrastructure domain layer, not in an action handler.`,
|
|
20047
20774
|
functoolsKit.typo.nbsp,
|
|
20048
|
-
`Use Broker.useBrokerAdapter with
|
|
20775
|
+
`Use Broker.useBrokerAdapter with onOrderCheck (order still open?) and onSignalOpenCommit / onSignalCloseCommit (order fill) instead.`,
|
|
20049
20776
|
functoolsKit.typo.nbsp,
|
|
20050
20777
|
`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`,
|
|
20051
20778
|
]);
|
|
@@ -42424,6 +43151,13 @@ const breakevenNewTakeProfitPrice = (priceTakeProfit, trailingPriceTakeProfit) =
|
|
|
42424
43151
|
const BROKER_METHOD_NAME_COMMIT_SIGNAL_OPEN = "BrokerAdapter.commitSignalOpen";
|
|
42425
43152
|
const BROKER_METHOD_NAME_COMMIT_SIGNAL_CLOSE = "BrokerAdapter.commitSignalClose";
|
|
42426
43153
|
const BROKER_METHOD_NAME_COMMIT_SIGNAL_PENDING = "BrokerAdapter.commitSignalPending";
|
|
43154
|
+
const BROKER_METHOD_NAME_COMMIT_ACTIVE_PING = "BrokerAdapter.commitActivePing";
|
|
43155
|
+
const BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING = "BrokerAdapter.commitSchedulePing";
|
|
43156
|
+
const BROKER_METHOD_NAME_COMMIT_IDLE_PING = "BrokerAdapter.commitIdlePing";
|
|
43157
|
+
const BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN = "BrokerAdapter.commitScheduleOpen";
|
|
43158
|
+
const BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED = "BrokerAdapter.commitScheduleCancelled";
|
|
43159
|
+
const BROKER_METHOD_NAME_COMMIT_PENDING_OPEN = "BrokerAdapter.commitPendingOpen";
|
|
43160
|
+
const BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE = "BrokerAdapter.commitPendingClose";
|
|
42427
43161
|
const BROKER_METHOD_NAME_COMMIT_PARTIAL_PROFIT = "BrokerAdapter.commitPartialProfit";
|
|
42428
43162
|
const BROKER_METHOD_NAME_COMMIT_PARTIAL_LOSS = "BrokerAdapter.commitPartialLoss";
|
|
42429
43163
|
const BROKER_METHOD_NAME_COMMIT_TRAILING_STOP = "BrokerAdapter.commitTrailingStop";
|
|
@@ -42437,7 +43171,14 @@ const BROKER_METHOD_NAME_CLEAR = "BrokerAdapter.clear";
|
|
|
42437
43171
|
const BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT = "BrokerBase.waitForInit";
|
|
42438
43172
|
const BROKER_BASE_METHOD_NAME_ON_SIGNAL_OPEN = "BrokerBase.onSignalOpenCommit";
|
|
42439
43173
|
const BROKER_BASE_METHOD_NAME_ON_SIGNAL_CLOSE = "BrokerBase.onSignalCloseCommit";
|
|
42440
|
-
const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.
|
|
43174
|
+
const BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING = "BrokerBase.onOrderCheck";
|
|
43175
|
+
const BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING = "BrokerBase.onSignalActivePing";
|
|
43176
|
+
const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING = "BrokerBase.onSignalSchedulePing";
|
|
43177
|
+
const BROKER_BASE_METHOD_NAME_ON_IDLE_PING = "BrokerBase.onSignalIdlePing";
|
|
43178
|
+
const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN = "BrokerBase.onSignalScheduleOpen";
|
|
43179
|
+
const BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED = "BrokerBase.onSignalScheduleCancelled";
|
|
43180
|
+
const BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN = "BrokerBase.onSignalPendingOpen";
|
|
43181
|
+
const BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE = "BrokerBase.onSignalPendingClose";
|
|
42441
43182
|
const BROKER_BASE_METHOD_NAME_ON_PARTIAL_PROFIT = "BrokerBase.onPartialProfitCommit";
|
|
42442
43183
|
const BROKER_BASE_METHOD_NAME_ON_PARTIAL_LOSS = "BrokerBase.onPartialLossCommit";
|
|
42443
43184
|
const BROKER_BASE_METHOD_NAME_ON_TRAILING_STOP = "BrokerBase.onTrailingStopCommit";
|
|
@@ -42489,7 +43230,7 @@ class BrokerProxy {
|
|
|
42489
43230
|
/**
|
|
42490
43231
|
* Forwards a pending-order ping to the underlying adapter.
|
|
42491
43232
|
*
|
|
42492
|
-
* If the adapter does not implement `
|
|
43233
|
+
* If the adapter does not implement `onOrderCheck`, the call is silently skipped
|
|
42493
43234
|
* (the order is assumed still open). When implemented, exceptions propagate — a throw means
|
|
42494
43235
|
* the order was NOT FOUND by `payload.signalId` and the framework closes the position with
|
|
42495
43236
|
* closeReason "closed". The adapter must throw ONLY on a confirmed "order not found by id"
|
|
@@ -42498,10 +43239,101 @@ class BrokerProxy {
|
|
|
42498
43239
|
*
|
|
42499
43240
|
* @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest flag.
|
|
42500
43241
|
*/
|
|
42501
|
-
async
|
|
42502
|
-
if (this._instance.
|
|
43242
|
+
async onOrderCheck(payload) {
|
|
43243
|
+
if (this._instance.onOrderCheck) {
|
|
43244
|
+
await this.waitForInit();
|
|
43245
|
+
await this._instance.onOrderCheck(payload);
|
|
43246
|
+
return;
|
|
43247
|
+
}
|
|
43248
|
+
}
|
|
43249
|
+
/**
|
|
43250
|
+
* Forwards an active-ping event to the underlying adapter.
|
|
43251
|
+
* Silently skipped when the adapter does not implement `onSignalActivePing`.
|
|
43252
|
+
*
|
|
43253
|
+
* @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest.
|
|
43254
|
+
*/
|
|
43255
|
+
async onSignalActivePing(payload) {
|
|
43256
|
+
if (this._instance.onSignalActivePing) {
|
|
43257
|
+
await this.waitForInit();
|
|
43258
|
+
await this._instance.onSignalActivePing(payload);
|
|
43259
|
+
return;
|
|
43260
|
+
}
|
|
43261
|
+
}
|
|
43262
|
+
/**
|
|
43263
|
+
* Forwards a schedule-ping event to the underlying adapter.
|
|
43264
|
+
* Silently skipped when the adapter does not implement `onSignalSchedulePing`.
|
|
43265
|
+
*
|
|
43266
|
+
* @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest.
|
|
43267
|
+
*/
|
|
43268
|
+
async onSignalSchedulePing(payload) {
|
|
43269
|
+
if (this._instance.onSignalSchedulePing) {
|
|
43270
|
+
await this.waitForInit();
|
|
43271
|
+
await this._instance.onSignalSchedulePing(payload);
|
|
43272
|
+
return;
|
|
43273
|
+
}
|
|
43274
|
+
}
|
|
43275
|
+
/**
|
|
43276
|
+
* Forwards an idle-ping event to the underlying adapter.
|
|
43277
|
+
* Silently skipped when the adapter does not implement `onSignalIdlePing`.
|
|
43278
|
+
*
|
|
43279
|
+
* @param payload - Idle ping details: symbol, currentPrice, context, backtest.
|
|
43280
|
+
*/
|
|
43281
|
+
async onSignalIdlePing(payload) {
|
|
43282
|
+
if (this._instance.onSignalIdlePing) {
|
|
43283
|
+
await this.waitForInit();
|
|
43284
|
+
await this._instance.onSignalIdlePing(payload);
|
|
43285
|
+
return;
|
|
43286
|
+
}
|
|
43287
|
+
}
|
|
43288
|
+
/**
|
|
43289
|
+
* Forwards a scheduled-signal-open event to the underlying adapter.
|
|
43290
|
+
* Silently skipped when the adapter does not implement `onSignalScheduleOpen`.
|
|
43291
|
+
*
|
|
43292
|
+
* @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest.
|
|
43293
|
+
*/
|
|
43294
|
+
async onSignalScheduleOpen(payload) {
|
|
43295
|
+
if (this._instance.onSignalScheduleOpen) {
|
|
43296
|
+
await this.waitForInit();
|
|
43297
|
+
await this._instance.onSignalScheduleOpen(payload);
|
|
43298
|
+
return;
|
|
43299
|
+
}
|
|
43300
|
+
}
|
|
43301
|
+
/**
|
|
43302
|
+
* Forwards a scheduled-signal-cancelled event to the underlying adapter.
|
|
43303
|
+
* Silently skipped when the adapter does not implement `onSignalScheduleCancelled`.
|
|
43304
|
+
*
|
|
43305
|
+
* @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest.
|
|
43306
|
+
*/
|
|
43307
|
+
async onSignalScheduleCancelled(payload) {
|
|
43308
|
+
if (this._instance.onSignalScheduleCancelled) {
|
|
43309
|
+
await this.waitForInit();
|
|
43310
|
+
await this._instance.onSignalScheduleCancelled(payload);
|
|
43311
|
+
return;
|
|
43312
|
+
}
|
|
43313
|
+
}
|
|
43314
|
+
/**
|
|
43315
|
+
* Forwards a pending-signal-open event to the underlying adapter.
|
|
43316
|
+
* Silently skipped when the adapter does not implement `onSignalPendingOpen`.
|
|
43317
|
+
*
|
|
43318
|
+
* @param payload - Pending open details: symbol, signalId, position, prices, context, backtest.
|
|
43319
|
+
*/
|
|
43320
|
+
async onSignalPendingOpen(payload) {
|
|
43321
|
+
if (this._instance.onSignalPendingOpen) {
|
|
42503
43322
|
await this.waitForInit();
|
|
42504
|
-
await this._instance.
|
|
43323
|
+
await this._instance.onSignalPendingOpen(payload);
|
|
43324
|
+
return;
|
|
43325
|
+
}
|
|
43326
|
+
}
|
|
43327
|
+
/**
|
|
43328
|
+
* Forwards a pending-signal-close event to the underlying adapter.
|
|
43329
|
+
* Silently skipped when the adapter does not implement `onSignalPendingClose`.
|
|
43330
|
+
*
|
|
43331
|
+
* @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest.
|
|
43332
|
+
*/
|
|
43333
|
+
async onSignalPendingClose(payload) {
|
|
43334
|
+
if (this._instance.onSignalPendingClose) {
|
|
43335
|
+
await this.waitForInit();
|
|
43336
|
+
await this._instance.onSignalPendingClose(payload);
|
|
42505
43337
|
return;
|
|
42506
43338
|
}
|
|
42507
43339
|
}
|
|
@@ -42757,7 +43589,178 @@ class BrokerAdapter {
|
|
|
42757
43589
|
}
|
|
42758
43590
|
const instance = this.getInstance();
|
|
42759
43591
|
if (instance) {
|
|
42760
|
-
await instance.
|
|
43592
|
+
await instance.onOrderCheck(payload);
|
|
43593
|
+
}
|
|
43594
|
+
};
|
|
43595
|
+
/**
|
|
43596
|
+
* Forwards an active-ping to the registered broker adapter.
|
|
43597
|
+
*
|
|
43598
|
+
* Called automatically via activePingSubject when `enable()` is active, on every live tick while a
|
|
43599
|
+
* pending signal is monitored. Skipped silently in backtest mode or when no adapter is registered.
|
|
43600
|
+
* Purely informational — a throw does NOT close the position.
|
|
43601
|
+
*
|
|
43602
|
+
* @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
|
|
43603
|
+
*/
|
|
43604
|
+
this.commitActivePing = async (payload) => {
|
|
43605
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_ACTIVE_PING, {
|
|
43606
|
+
symbol: payload.symbol,
|
|
43607
|
+
context: payload.context,
|
|
43608
|
+
});
|
|
43609
|
+
if (!this.enable.hasValue()) {
|
|
43610
|
+
return;
|
|
43611
|
+
}
|
|
43612
|
+
if (payload.backtest) {
|
|
43613
|
+
return;
|
|
43614
|
+
}
|
|
43615
|
+
const instance = this.getInstance();
|
|
43616
|
+
if (instance) {
|
|
43617
|
+
await instance.onSignalActivePing(payload);
|
|
43618
|
+
}
|
|
43619
|
+
};
|
|
43620
|
+
/**
|
|
43621
|
+
* Forwards a schedule-ping to the registered broker adapter.
|
|
43622
|
+
*
|
|
43623
|
+
* Called automatically via schedulePingSubject when `enable()` is active, on every live tick while
|
|
43624
|
+
* a scheduled signal is monitored. Skipped silently in backtest mode or when no adapter is
|
|
43625
|
+
* registered. Purely informational.
|
|
43626
|
+
*
|
|
43627
|
+
* @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
|
|
43628
|
+
*/
|
|
43629
|
+
this.commitSchedulePing = async (payload) => {
|
|
43630
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_PING, {
|
|
43631
|
+
symbol: payload.symbol,
|
|
43632
|
+
context: payload.context,
|
|
43633
|
+
});
|
|
43634
|
+
if (!this.enable.hasValue()) {
|
|
43635
|
+
return;
|
|
43636
|
+
}
|
|
43637
|
+
if (payload.backtest) {
|
|
43638
|
+
return;
|
|
43639
|
+
}
|
|
43640
|
+
const instance = this.getInstance();
|
|
43641
|
+
if (instance) {
|
|
43642
|
+
await instance.onSignalSchedulePing(payload);
|
|
43643
|
+
}
|
|
43644
|
+
};
|
|
43645
|
+
/**
|
|
43646
|
+
* Forwards an idle-ping to the registered broker adapter.
|
|
43647
|
+
*
|
|
43648
|
+
* Called automatically via idlePingSubject when `enable()` is active, on every live tick while the
|
|
43649
|
+
* strategy has no pending or scheduled signal. Skipped silently in backtest mode or when no adapter
|
|
43650
|
+
* is registered. Purely informational.
|
|
43651
|
+
*
|
|
43652
|
+
* @param payload - Idle ping details: symbol, currentPrice, context, backtest
|
|
43653
|
+
*/
|
|
43654
|
+
this.commitIdlePing = async (payload) => {
|
|
43655
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_IDLE_PING, {
|
|
43656
|
+
symbol: payload.symbol,
|
|
43657
|
+
context: payload.context,
|
|
43658
|
+
});
|
|
43659
|
+
if (!this.enable.hasValue()) {
|
|
43660
|
+
return;
|
|
43661
|
+
}
|
|
43662
|
+
if (payload.backtest) {
|
|
43663
|
+
return;
|
|
43664
|
+
}
|
|
43665
|
+
const instance = this.getInstance();
|
|
43666
|
+
if (instance) {
|
|
43667
|
+
await instance.onSignalIdlePing(payload);
|
|
43668
|
+
}
|
|
43669
|
+
};
|
|
43670
|
+
/**
|
|
43671
|
+
* Forwards a scheduled-signal-open to the registered broker adapter.
|
|
43672
|
+
*
|
|
43673
|
+
* Called automatically via scheduleEventSubject (action "scheduled") when a scheduled signal is
|
|
43674
|
+
* created. Skipped silently in backtest mode or when no adapter is registered.
|
|
43675
|
+
*
|
|
43676
|
+
* @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
|
|
43677
|
+
*/
|
|
43678
|
+
this.commitScheduleOpen = async (payload) => {
|
|
43679
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_OPEN, {
|
|
43680
|
+
symbol: payload.symbol,
|
|
43681
|
+
context: payload.context,
|
|
43682
|
+
});
|
|
43683
|
+
if (!this.enable.hasValue()) {
|
|
43684
|
+
return;
|
|
43685
|
+
}
|
|
43686
|
+
if (payload.backtest) {
|
|
43687
|
+
return;
|
|
43688
|
+
}
|
|
43689
|
+
const instance = this.getInstance();
|
|
43690
|
+
if (instance) {
|
|
43691
|
+
await instance.onSignalScheduleOpen(payload);
|
|
43692
|
+
}
|
|
43693
|
+
};
|
|
43694
|
+
/**
|
|
43695
|
+
* Forwards a scheduled-signal-cancelled to the registered broker adapter.
|
|
43696
|
+
*
|
|
43697
|
+
* Called automatically via scheduleEventSubject (action "cancelled") when a scheduled signal is
|
|
43698
|
+
* removed before activation. Skipped silently in backtest mode or when no adapter is registered.
|
|
43699
|
+
*
|
|
43700
|
+
* @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
|
|
43701
|
+
*/
|
|
43702
|
+
this.commitScheduleCancelled = async (payload) => {
|
|
43703
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_SCHEDULE_CANCELLED, {
|
|
43704
|
+
symbol: payload.symbol,
|
|
43705
|
+
context: payload.context,
|
|
43706
|
+
});
|
|
43707
|
+
if (!this.enable.hasValue()) {
|
|
43708
|
+
return;
|
|
43709
|
+
}
|
|
43710
|
+
if (payload.backtest) {
|
|
43711
|
+
return;
|
|
43712
|
+
}
|
|
43713
|
+
const instance = this.getInstance();
|
|
43714
|
+
if (instance) {
|
|
43715
|
+
await instance.onSignalScheduleCancelled(payload);
|
|
43716
|
+
}
|
|
43717
|
+
};
|
|
43718
|
+
/**
|
|
43719
|
+
* Forwards a pending-signal-open to the registered broker adapter.
|
|
43720
|
+
*
|
|
43721
|
+
* Called automatically via signalEventSubject (action "opened") when a pending position is opened.
|
|
43722
|
+
* Skipped silently in backtest mode or when no adapter is registered.
|
|
43723
|
+
*
|
|
43724
|
+
* @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
|
|
43725
|
+
*/
|
|
43726
|
+
this.commitPendingOpen = async (payload) => {
|
|
43727
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_OPEN, {
|
|
43728
|
+
symbol: payload.symbol,
|
|
43729
|
+
context: payload.context,
|
|
43730
|
+
});
|
|
43731
|
+
if (!this.enable.hasValue()) {
|
|
43732
|
+
return;
|
|
43733
|
+
}
|
|
43734
|
+
if (payload.backtest) {
|
|
43735
|
+
return;
|
|
43736
|
+
}
|
|
43737
|
+
const instance = this.getInstance();
|
|
43738
|
+
if (instance) {
|
|
43739
|
+
await instance.onSignalPendingOpen(payload);
|
|
43740
|
+
}
|
|
43741
|
+
};
|
|
43742
|
+
/**
|
|
43743
|
+
* Forwards a pending-signal-close to the registered broker adapter.
|
|
43744
|
+
*
|
|
43745
|
+
* Called automatically via signalEventSubject (action "closed") when a pending position is closed.
|
|
43746
|
+
* Skipped silently in backtest mode or when no adapter is registered.
|
|
43747
|
+
*
|
|
43748
|
+
* @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
|
|
43749
|
+
*/
|
|
43750
|
+
this.commitPendingClose = async (payload) => {
|
|
43751
|
+
bt.loggerService.info(BROKER_METHOD_NAME_COMMIT_PENDING_CLOSE, {
|
|
43752
|
+
symbol: payload.symbol,
|
|
43753
|
+
context: payload.context,
|
|
43754
|
+
});
|
|
43755
|
+
if (!this.enable.hasValue()) {
|
|
43756
|
+
return;
|
|
43757
|
+
}
|
|
43758
|
+
if (payload.backtest) {
|
|
43759
|
+
return;
|
|
43760
|
+
}
|
|
43761
|
+
const instance = this.getInstance();
|
|
43762
|
+
if (instance) {
|
|
43763
|
+
await instance.onSignalPendingClose(payload);
|
|
42761
43764
|
}
|
|
42762
43765
|
};
|
|
42763
43766
|
/**
|
|
@@ -43125,7 +44128,98 @@ class BrokerAdapter {
|
|
|
43125
44128
|
backtest: event.backtest,
|
|
43126
44129
|
});
|
|
43127
44130
|
});
|
|
43128
|
-
const
|
|
44131
|
+
const unActivePing = activePingSubject.subscribe(async (event) => {
|
|
44132
|
+
await this.commitActivePing({
|
|
44133
|
+
symbol: event.symbol,
|
|
44134
|
+
signalId: event.data.id,
|
|
44135
|
+
position: event.data.position,
|
|
44136
|
+
currentPrice: event.currentPrice,
|
|
44137
|
+
priceOpen: event.data.priceOpen,
|
|
44138
|
+
priceTakeProfit: event.data.priceTakeProfit,
|
|
44139
|
+
priceStopLoss: event.data.priceStopLoss,
|
|
44140
|
+
pnl: event.data.pnl,
|
|
44141
|
+
context: {
|
|
44142
|
+
strategyName: event.strategyName,
|
|
44143
|
+
exchangeName: event.exchangeName,
|
|
44144
|
+
frameName: event.frameName,
|
|
44145
|
+
},
|
|
44146
|
+
backtest: event.backtest,
|
|
44147
|
+
});
|
|
44148
|
+
});
|
|
44149
|
+
const unSchedulePing = schedulePingSubject.subscribe(async (event) => {
|
|
44150
|
+
await this.commitSchedulePing({
|
|
44151
|
+
symbol: event.symbol,
|
|
44152
|
+
signalId: event.data.id,
|
|
44153
|
+
position: event.data.position,
|
|
44154
|
+
currentPrice: event.currentPrice,
|
|
44155
|
+
priceOpen: event.data.priceOpen,
|
|
44156
|
+
priceTakeProfit: event.data.priceTakeProfit,
|
|
44157
|
+
priceStopLoss: event.data.priceStopLoss,
|
|
44158
|
+
context: {
|
|
44159
|
+
strategyName: event.strategyName,
|
|
44160
|
+
exchangeName: event.exchangeName,
|
|
44161
|
+
frameName: event.frameName,
|
|
44162
|
+
},
|
|
44163
|
+
backtest: event.backtest,
|
|
44164
|
+
});
|
|
44165
|
+
});
|
|
44166
|
+
const unIdlePing = idlePingSubject.subscribe(async (event) => {
|
|
44167
|
+
await this.commitIdlePing({
|
|
44168
|
+
symbol: event.symbol,
|
|
44169
|
+
currentPrice: event.currentPrice,
|
|
44170
|
+
context: {
|
|
44171
|
+
strategyName: event.strategyName,
|
|
44172
|
+
exchangeName: event.exchangeName,
|
|
44173
|
+
frameName: event.frameName,
|
|
44174
|
+
},
|
|
44175
|
+
backtest: event.backtest,
|
|
44176
|
+
});
|
|
44177
|
+
});
|
|
44178
|
+
const unScheduleEvent = scheduleEventSubject.subscribe(async (event) => {
|
|
44179
|
+
const payload = {
|
|
44180
|
+
symbol: event.symbol,
|
|
44181
|
+
signalId: event.data.id,
|
|
44182
|
+
position: event.data.position,
|
|
44183
|
+
currentPrice: event.currentPrice,
|
|
44184
|
+
priceOpen: event.data.priceOpen,
|
|
44185
|
+
priceTakeProfit: event.data.priceTakeProfit,
|
|
44186
|
+
priceStopLoss: event.data.priceStopLoss,
|
|
44187
|
+
context: {
|
|
44188
|
+
strategyName: event.strategyName,
|
|
44189
|
+
exchangeName: event.exchangeName,
|
|
44190
|
+
frameName: event.frameName,
|
|
44191
|
+
},
|
|
44192
|
+
backtest: event.backtest,
|
|
44193
|
+
};
|
|
44194
|
+
if (event.action === "scheduled") {
|
|
44195
|
+
await this.commitScheduleOpen(payload);
|
|
44196
|
+
return;
|
|
44197
|
+
}
|
|
44198
|
+
await this.commitScheduleCancelled({ ...payload, reason: event.reason });
|
|
44199
|
+
});
|
|
44200
|
+
const unSignalEvent = signalEventSubject.subscribe(async (event) => {
|
|
44201
|
+
const payload = {
|
|
44202
|
+
symbol: event.symbol,
|
|
44203
|
+
signalId: event.data.id,
|
|
44204
|
+
position: event.data.position,
|
|
44205
|
+
currentPrice: event.currentPrice,
|
|
44206
|
+
priceOpen: event.data.priceOpen,
|
|
44207
|
+
priceTakeProfit: event.data.priceTakeProfit,
|
|
44208
|
+
priceStopLoss: event.data.priceStopLoss,
|
|
44209
|
+
context: {
|
|
44210
|
+
strategyName: event.strategyName,
|
|
44211
|
+
exchangeName: event.exchangeName,
|
|
44212
|
+
frameName: event.frameName,
|
|
44213
|
+
},
|
|
44214
|
+
backtest: event.backtest,
|
|
44215
|
+
};
|
|
44216
|
+
if (event.action === "opened") {
|
|
44217
|
+
await this.commitPendingOpen(payload);
|
|
44218
|
+
return;
|
|
44219
|
+
}
|
|
44220
|
+
await this.commitPendingClose({ ...payload, closeReason: event.closeReason });
|
|
44221
|
+
});
|
|
44222
|
+
const disposeFn = functoolsKit.compose(() => unSignalOpen(), () => unSignalClose(), () => unSignalPending(), () => unActivePing(), () => unSchedulePing(), () => unIdlePing(), () => unScheduleEvent(), () => unSignalEvent());
|
|
43129
44223
|
return () => {
|
|
43130
44224
|
this.enable.clear();
|
|
43131
44225
|
disposeFn();
|
|
@@ -43271,24 +44365,32 @@ class BrokerBase {
|
|
|
43271
44365
|
bt.loggerService.info(BROKER_BASE_METHOD_NAME_WAIT_FOR_INIT, {});
|
|
43272
44366
|
}
|
|
43273
44367
|
/**
|
|
43274
|
-
* Called when a
|
|
44368
|
+
* Called when a position is being opened (signal activated).
|
|
43275
44369
|
*
|
|
43276
44370
|
* Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit.
|
|
43277
44371
|
* Use to place the actual entry order on the exchange.
|
|
43278
44372
|
*
|
|
43279
44373
|
* Default implementation: Logs signal-open event.
|
|
43280
44374
|
*
|
|
44375
|
+
* Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
|
|
44376
|
+
* (e.g. limit order rejected) rolls back the open — the pending signal returns to idle and retries
|
|
44377
|
+
* next tick; return normally to let it open. Live-only (backtest short-circuits). See
|
|
44378
|
+
* {@link IBroker.onSignalOpenCommit} for the full semantics.
|
|
44379
|
+
*
|
|
43281
44380
|
* @param payload - Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, context, backtest
|
|
43282
44381
|
*
|
|
43283
44382
|
* @example
|
|
43284
44383
|
* ```typescript
|
|
43285
44384
|
* async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
|
|
43286
44385
|
* super.onSignalOpenCommit(payload); // Keep parent logging
|
|
43287
|
-
* await this.exchange.placeMarketOrder({
|
|
44386
|
+
* const order = await this.exchange.placeMarketOrder({
|
|
43288
44387
|
* symbol: payload.symbol,
|
|
43289
44388
|
* side: payload.position === "long" ? "BUY" : "SELL",
|
|
43290
44389
|
* quantity: payload.cost / payload.priceOpen,
|
|
43291
44390
|
* });
|
|
44391
|
+
* if (!order.filled) {
|
|
44392
|
+
* throw new Error(`Entry not filled for ${payload.symbol}`); // -> roll back the open, retry next tick
|
|
44393
|
+
* }
|
|
43292
44394
|
* }
|
|
43293
44395
|
* ```
|
|
43294
44396
|
*/
|
|
@@ -43310,47 +44412,158 @@ class BrokerBase {
|
|
|
43310
44412
|
* normally instead of throwing. A thrown network error would wrongly close an open position; only
|
|
43311
44413
|
* a confirmed "order not found by id" response is a valid reason to throw.
|
|
43312
44414
|
*
|
|
43313
|
-
*
|
|
44415
|
+
* Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
|
|
44416
|
+
* commit-function wiring in `onSignalActivePing`. See {@link IBroker.onOrderCheck} for the full
|
|
44417
|
+
* comparison and example.
|
|
43314
44418
|
*
|
|
43315
|
-
* @
|
|
43316
|
-
* ```typescript
|
|
43317
|
-
* async onOrderPing(payload: BrokerSignalPendingPayload) {
|
|
43318
|
-
* super.onOrderPing(payload); // Keep parent logging
|
|
43319
|
-
* let order: Order | null;
|
|
43320
|
-
* try {
|
|
43321
|
-
* order = await this.exchange.getOrderById(payload.signalId);
|
|
43322
|
-
* } catch (networkError) {
|
|
43323
|
-
* // Transient/network error — swallow and keep the position open, retry next tick
|
|
43324
|
-
* return;
|
|
43325
|
-
* }
|
|
43326
|
-
* if (!order) {
|
|
43327
|
-
* // Confirmed not found by id — close the position
|
|
43328
|
-
* throw new Error(`Order ${payload.signalId} not found on the exchange`);
|
|
43329
|
-
* }
|
|
43330
|
-
* }
|
|
43331
|
-
* ```
|
|
44419
|
+
* @param payload - Pending ping details: symbol, signalId, position, prices, pnl, context, backtest
|
|
43332
44420
|
*/
|
|
43333
|
-
async
|
|
44421
|
+
async onOrderCheck(payload) {
|
|
43334
44422
|
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SIGNAL_PENDING, {
|
|
43335
44423
|
symbol: payload.symbol,
|
|
43336
44424
|
context: payload.context,
|
|
43337
44425
|
});
|
|
43338
44426
|
}
|
|
43339
44427
|
/**
|
|
43340
|
-
* Called
|
|
44428
|
+
* Called on every live tick while a pending (open) signal is monitored.
|
|
44429
|
+
*
|
|
44430
|
+
* Purely informational mirror of the active-ping lifecycle — unlike `onOrderCheck`, a throw here
|
|
44431
|
+
* does NOT close the position. Override to mirror live monitoring state into your own systems.
|
|
44432
|
+
* The default implementation logs.
|
|
44433
|
+
*
|
|
44434
|
+
* Manual wiring — EVENT-BASED: this is the primary per-tick hook to drive an open position from real exchange
|
|
44435
|
+
* state (`commitCreateTakeProfit` / `commitCreateStopLoss` / `commitClosePending`). See the
|
|
44436
|
+
* {@link IBroker.onSignalActivePing} contract docs for the full guidance and example.
|
|
44437
|
+
*
|
|
44438
|
+
* @param payload - Active ping details: symbol, signalId, position, prices, pnl, context, backtest
|
|
44439
|
+
*/
|
|
44440
|
+
async onSignalActivePing(payload) {
|
|
44441
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_ACTIVE_PING, {
|
|
44442
|
+
symbol: payload.symbol,
|
|
44443
|
+
context: payload.context,
|
|
44444
|
+
});
|
|
44445
|
+
}
|
|
44446
|
+
/**
|
|
44447
|
+
* Called on every live tick while a scheduled signal is monitored (waiting for priceOpen).
|
|
44448
|
+
*
|
|
44449
|
+
* Purely informational. Override to mirror scheduled-monitoring state. The default logs.
|
|
44450
|
+
*
|
|
44451
|
+
* Manual wiring — EVENT-BASED: per-tick hook to drive a scheduled (resting) order from real exchange state
|
|
44452
|
+
* (`commitActivateScheduled` / `commitCancelScheduled`). See {@link IBroker.onSignalSchedulePing}
|
|
44453
|
+
* for full guidance and example.
|
|
44454
|
+
*
|
|
44455
|
+
* @param payload - Schedule ping details: symbol, signalId, position, prices, context, backtest
|
|
44456
|
+
*/
|
|
44457
|
+
async onSignalSchedulePing(payload) {
|
|
44458
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_PING, {
|
|
44459
|
+
symbol: payload.symbol,
|
|
44460
|
+
context: payload.context,
|
|
44461
|
+
});
|
|
44462
|
+
}
|
|
44463
|
+
/**
|
|
44464
|
+
* Called on every live tick while the strategy is idle (no pending or scheduled signal).
|
|
44465
|
+
*
|
|
44466
|
+
* Purely informational. Override to track idle heartbeats. The default logs.
|
|
44467
|
+
*
|
|
44468
|
+
* @param payload - Idle ping details: symbol, currentPrice, context, backtest
|
|
44469
|
+
*/
|
|
44470
|
+
async onSignalIdlePing(payload) {
|
|
44471
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_IDLE_PING, {
|
|
44472
|
+
symbol: payload.symbol,
|
|
44473
|
+
context: payload.context,
|
|
44474
|
+
});
|
|
44475
|
+
}
|
|
44476
|
+
/**
|
|
44477
|
+
* Called when a new scheduled signal is created and starts waiting for priceOpen activation.
|
|
44478
|
+
*
|
|
44479
|
+
* The scheduled -> active transition is reported via `onSignalOpenCommit`, not here. Override to
|
|
44480
|
+
* place a resting/limit order on the exchange. The default logs.
|
|
44481
|
+
*
|
|
44482
|
+
* Manual wiring — EVENT-BASED: fires ONCE at creation — place the real resting order (tag it with
|
|
44483
|
+
* `payload.signalId`) and optionally `commitActivateScheduled` / `commitCancelScheduled`. See
|
|
44484
|
+
* {@link IBroker.onSignalScheduleOpen} for full guidance and example.
|
|
44485
|
+
*
|
|
44486
|
+
* @param payload - Scheduled open details: symbol, signalId, position, prices, context, backtest
|
|
44487
|
+
*/
|
|
44488
|
+
async onSignalScheduleOpen(payload) {
|
|
44489
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_OPEN, {
|
|
44490
|
+
symbol: payload.symbol,
|
|
44491
|
+
context: payload.context,
|
|
44492
|
+
});
|
|
44493
|
+
}
|
|
44494
|
+
/**
|
|
44495
|
+
* Called when a scheduled signal is cancelled before activation (timeout / price_reject / user).
|
|
44496
|
+
*
|
|
44497
|
+
* Override to cancel the resting/limit order on the exchange. The default logs.
|
|
44498
|
+
*
|
|
44499
|
+
* Manual wiring — EVENT-BASED (outbound): the strategy already dropped the scheduled signal — cancel the matching
|
|
44500
|
+
* exchange order by `payload.signalId`. See {@link IBroker.onSignalScheduleCancelled}.
|
|
44501
|
+
*
|
|
44502
|
+
* @param payload - Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
|
|
44503
|
+
*/
|
|
44504
|
+
async onSignalScheduleCancelled(payload) {
|
|
44505
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_SCHEDULE_CANCELLED, {
|
|
44506
|
+
symbol: payload.symbol,
|
|
44507
|
+
context: payload.context,
|
|
44508
|
+
});
|
|
44509
|
+
}
|
|
44510
|
+
/**
|
|
44511
|
+
* Called when a pending position is opened (new signal / immediate / scheduled or user activation).
|
|
44512
|
+
*
|
|
44513
|
+
* Informational lifecycle hook. Override to mirror the open into your own systems. The default logs.
|
|
44514
|
+
*
|
|
44515
|
+
* Manual wiring — EVENT-BASED: fires ONCE at open — place entry + protective TP/SL orders (tag with
|
|
44516
|
+
* `payload.signalId`), then drive per-tick from `onSignalActivePing`. See
|
|
44517
|
+
* {@link IBroker.onSignalPendingOpen}.
|
|
44518
|
+
*
|
|
44519
|
+
* @param payload - Pending open details: symbol, signalId, position, prices, context, backtest
|
|
44520
|
+
*/
|
|
44521
|
+
async onSignalPendingOpen(payload) {
|
|
44522
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_OPEN, {
|
|
44523
|
+
symbol: payload.symbol,
|
|
44524
|
+
context: payload.context,
|
|
44525
|
+
});
|
|
44526
|
+
}
|
|
44527
|
+
/**
|
|
44528
|
+
* Called when a pending position is closed (take_profit / stop_loss / time_expired / closed).
|
|
44529
|
+
*
|
|
44530
|
+
* Informational lifecycle hook. Override to mirror the close into your own systems. The default logs.
|
|
44531
|
+
*
|
|
44532
|
+
* Manual wiring — EVENT-BASED (outbound): the strategy already removed the pending signal — flatten the real
|
|
44533
|
+
* position and cancel leftover TP/SL orders by `payload.signalId`. See
|
|
44534
|
+
* {@link IBroker.onSignalPendingClose}.
|
|
44535
|
+
*
|
|
44536
|
+
* @param payload - Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
|
|
44537
|
+
*/
|
|
44538
|
+
async onSignalPendingClose(payload) {
|
|
44539
|
+
bt.loggerService.info(BROKER_BASE_METHOD_NAME_ON_PENDING_CLOSE, {
|
|
44540
|
+
symbol: payload.symbol,
|
|
44541
|
+
context: payload.context,
|
|
44542
|
+
});
|
|
44543
|
+
}
|
|
44544
|
+
/**
|
|
44545
|
+
* Called when a position is being closed (SL/TP hit or manual close).
|
|
43341
44546
|
*
|
|
43342
44547
|
* Triggered automatically via syncSubject when a pending signal is closed.
|
|
43343
44548
|
* Use to place the exit order and record final PnL.
|
|
43344
44549
|
*
|
|
43345
44550
|
* Default implementation: Logs signal-close event.
|
|
43346
44551
|
*
|
|
44552
|
+
* Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state, so a THROW here
|
|
44553
|
+
* (e.g. exit order failed) SKIPS the close — the position stays open and the close retries next
|
|
44554
|
+
* tick; return normally to let it close. Live-only (backtest short-circuits). See
|
|
44555
|
+
* {@link IBroker.onSignalCloseCommit} for the full semantics.
|
|
44556
|
+
*
|
|
43347
44557
|
* @param payload - Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, context, backtest
|
|
43348
44558
|
*
|
|
43349
44559
|
* @example
|
|
43350
44560
|
* ```typescript
|
|
43351
44561
|
* async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
|
|
43352
44562
|
* super.onSignalCloseCommit(payload); // Keep parent logging
|
|
43353
|
-
* await this.exchange.closePosition(payload.symbol);
|
|
44563
|
+
* const ok = await this.exchange.closePosition(payload.symbol);
|
|
44564
|
+
* if (!ok) {
|
|
44565
|
+
* throw new Error(`Exit not filled for ${payload.symbol}`); // -> keep position open, retry next tick
|
|
44566
|
+
* }
|
|
43354
44567
|
* await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
|
|
43355
44568
|
* }
|
|
43356
44569
|
* ```
|
|
@@ -43606,6 +44819,8 @@ const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
|
|
|
43606
44819
|
const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
|
|
43607
44820
|
const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
|
|
43608
44821
|
const COMMIT_CREATE_SIGNAL_METHOD_NAME = "strategy.commitCreateSignal";
|
|
44822
|
+
const COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME = "strategy.commitCreateTakeProfit";
|
|
44823
|
+
const COMMIT_CREATE_STOP_LOSS_METHOD_NAME = "strategy.commitCreateStopLoss";
|
|
43609
44824
|
const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
|
|
43610
44825
|
/**
|
|
43611
44826
|
* Cancels the scheduled signal without stopping the strategy.
|
|
@@ -45715,6 +46930,74 @@ async function commitCreateSignal(symbol, dto) {
|
|
|
45715
46930
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
45716
46931
|
await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
|
|
45717
46932
|
}
|
|
46933
|
+
/**
|
|
46934
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
46935
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
|
|
46936
|
+
*
|
|
46937
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
46938
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
46939
|
+
* "take_profit" on the next tick. No-op if no pending signal exists.
|
|
46940
|
+
*
|
|
46941
|
+
* Automatically detects backtest/live mode from execution context.
|
|
46942
|
+
*
|
|
46943
|
+
* @param symbol - Trading pair symbol
|
|
46944
|
+
* @param payload - Optional commit payload with id and note
|
|
46945
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
46946
|
+
*
|
|
46947
|
+
* @example
|
|
46948
|
+
* ```typescript
|
|
46949
|
+
* import { commitCreateTakeProfit } from "backtest-kit";
|
|
46950
|
+
*
|
|
46951
|
+
* // Report TP fill confirmed on the exchange
|
|
46952
|
+
* await commitCreateTakeProfit("BTCUSDT", { id: "tp-fill-001" });
|
|
46953
|
+
* ```
|
|
46954
|
+
*/
|
|
46955
|
+
async function commitCreateTakeProfit(symbol, payload = {}) {
|
|
46956
|
+
bt.loggerService.info(COMMIT_CREATE_TAKE_PROFIT_METHOD_NAME, { symbol, payload });
|
|
46957
|
+
if (!ExecutionContextService.hasContext()) {
|
|
46958
|
+
throw new Error("commitCreateTakeProfit requires an execution context");
|
|
46959
|
+
}
|
|
46960
|
+
if (!MethodContextService.hasContext()) {
|
|
46961
|
+
throw new Error("commitCreateTakeProfit requires a method context");
|
|
46962
|
+
}
|
|
46963
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
46964
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
46965
|
+
await bt.strategyCoreService.createTakeProfit(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
|
|
46966
|
+
}
|
|
46967
|
+
/**
|
|
46968
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
46969
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
|
|
46970
|
+
*
|
|
46971
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
46972
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
46973
|
+
* "stop_loss" on the next tick. No-op if no pending signal exists.
|
|
46974
|
+
*
|
|
46975
|
+
* Automatically detects backtest/live mode from execution context.
|
|
46976
|
+
*
|
|
46977
|
+
* @param symbol - Trading pair symbol
|
|
46978
|
+
* @param payload - Optional commit payload with id and note
|
|
46979
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
46980
|
+
*
|
|
46981
|
+
* @example
|
|
46982
|
+
* ```typescript
|
|
46983
|
+
* import { commitCreateStopLoss } from "backtest-kit";
|
|
46984
|
+
*
|
|
46985
|
+
* // Report SL fill confirmed on the exchange
|
|
46986
|
+
* await commitCreateStopLoss("BTCUSDT", { id: "sl-fill-001" });
|
|
46987
|
+
* ```
|
|
46988
|
+
*/
|
|
46989
|
+
async function commitCreateStopLoss(symbol, payload = {}) {
|
|
46990
|
+
bt.loggerService.info(COMMIT_CREATE_STOP_LOSS_METHOD_NAME, { symbol, payload });
|
|
46991
|
+
if (!ExecutionContextService.hasContext()) {
|
|
46992
|
+
throw new Error("commitCreateStopLoss requires an execution context");
|
|
46993
|
+
}
|
|
46994
|
+
if (!MethodContextService.hasContext()) {
|
|
46995
|
+
throw new Error("commitCreateStopLoss requires a method context");
|
|
46996
|
+
}
|
|
46997
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
46998
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
46999
|
+
await bt.strategyCoreService.createStopLoss(isBacktest, symbol, { exchangeName, frameName, strategyName }, payload);
|
|
47000
|
+
}
|
|
45718
47001
|
/**
|
|
45719
47002
|
* Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
|
|
45720
47003
|
* createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
|
|
@@ -45817,6 +47100,10 @@ const LISTEN_RISK_METHOD_NAME = "event.listenRisk";
|
|
|
45817
47100
|
const LISTEN_RISK_ONCE_METHOD_NAME = "event.listenRiskOnce";
|
|
45818
47101
|
const LISTEN_SCHEDULE_PING_METHOD_NAME = "event.listenSchedulePing";
|
|
45819
47102
|
const LISTEN_SCHEDULE_PING_ONCE_METHOD_NAME = "event.listenSchedulePingOnce";
|
|
47103
|
+
const LISTEN_SCHEDULE_EVENT_METHOD_NAME = "event.listenScheduleEvent";
|
|
47104
|
+
const LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME = "event.listenScheduleEventOnce";
|
|
47105
|
+
const LISTEN_SIGNAL_EVENT_METHOD_NAME = "event.listenSignalEvent";
|
|
47106
|
+
const LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME = "event.listenSignalEventOnce";
|
|
45820
47107
|
const LISTEN_ACTIVE_PING_METHOD_NAME = "event.listenActivePing";
|
|
45821
47108
|
const LISTEN_ACTIVE_PING_ONCE_METHOD_NAME = "event.listenActivePingOnce";
|
|
45822
47109
|
const LISTEN_IDLE_PING_METHOD_NAME = "event.listenIdlePing";
|
|
@@ -46919,6 +48206,137 @@ function listenSchedulePingOnce(filterFn, fn) {
|
|
|
46919
48206
|
};
|
|
46920
48207
|
return disposeFn = listenSchedulePing(wrappedFn);
|
|
46921
48208
|
}
|
|
48209
|
+
/**
|
|
48210
|
+
* Subscribes to scheduled signal lifecycle events (creation and cancellation) with queued async processing.
|
|
48211
|
+
*
|
|
48212
|
+
* Emitted when a scheduled signal is created (action "scheduled") or cancelled before activation
|
|
48213
|
+
* (action "cancelled" with reason "timeout" / "price_reject" / "user"), in both live and backtest.
|
|
48214
|
+
*
|
|
48215
|
+
* IMPORTANT: The scheduled -> active transition (activation) is NOT reported here. Activation
|
|
48216
|
+
* produces an "opened" event on the regular signal emitters (listenSignal) instead.
|
|
48217
|
+
*
|
|
48218
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
48219
|
+
*
|
|
48220
|
+
* @param fn - Callback function to handle scheduled lifecycle events
|
|
48221
|
+
* @returns Unsubscribe function to stop listening
|
|
48222
|
+
*
|
|
48223
|
+
* @example
|
|
48224
|
+
* ```typescript
|
|
48225
|
+
* import { listenScheduleEvent } from "./function/event";
|
|
48226
|
+
*
|
|
48227
|
+
* const unsubscribe = listenScheduleEvent((event) => {
|
|
48228
|
+
* if (event.action === "scheduled") {
|
|
48229
|
+
* console.log(`Scheduled ${event.symbol} @ ${event.data.priceOpen}`);
|
|
48230
|
+
* } else {
|
|
48231
|
+
* console.log(`Cancelled ${event.symbol} (reason: ${event.reason})`);
|
|
48232
|
+
* }
|
|
48233
|
+
* });
|
|
48234
|
+
*
|
|
48235
|
+
* // Later: stop listening
|
|
48236
|
+
* unsubscribe();
|
|
48237
|
+
* ```
|
|
48238
|
+
*/
|
|
48239
|
+
function listenScheduleEvent(fn) {
|
|
48240
|
+
bt.loggerService.log(LISTEN_SCHEDULE_EVENT_METHOD_NAME);
|
|
48241
|
+
return scheduleEventSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
48242
|
+
}
|
|
48243
|
+
/**
|
|
48244
|
+
* Subscribes to filtered scheduled lifecycle events with one-time execution.
|
|
48245
|
+
*
|
|
48246
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
48247
|
+
* and automatically unsubscribes. Useful for waiting for a specific scheduled creation
|
|
48248
|
+
* or cancellation.
|
|
48249
|
+
*
|
|
48250
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
48251
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
48252
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
48253
|
+
*
|
|
48254
|
+
* @example
|
|
48255
|
+
* ```typescript
|
|
48256
|
+
* import { listenScheduleEventOnce } from "./function/event";
|
|
48257
|
+
*
|
|
48258
|
+
* // Wait for the first cancellation on BTCUSDT
|
|
48259
|
+
* listenScheduleEventOnce(
|
|
48260
|
+
* (event) => event.symbol === "BTCUSDT" && event.action === "cancelled",
|
|
48261
|
+
* (event) => console.log("BTCUSDT scheduled cancelled:", event.reason)
|
|
48262
|
+
* );
|
|
48263
|
+
* ```
|
|
48264
|
+
*/
|
|
48265
|
+
function listenScheduleEventOnce(filterFn, fn) {
|
|
48266
|
+
bt.loggerService.log(LISTEN_SCHEDULE_EVENT_ONCE_METHOD_NAME);
|
|
48267
|
+
let disposeFn;
|
|
48268
|
+
const wrappedFn = async (event) => {
|
|
48269
|
+
if (filterFn(event)) {
|
|
48270
|
+
await fn(event);
|
|
48271
|
+
disposeFn && disposeFn();
|
|
48272
|
+
}
|
|
48273
|
+
};
|
|
48274
|
+
return disposeFn = listenScheduleEvent(wrappedFn);
|
|
48275
|
+
}
|
|
48276
|
+
/**
|
|
48277
|
+
* Subscribes to pending signal lifecycle events (open and close) with queued async processing.
|
|
48278
|
+
*
|
|
48279
|
+
* Emitted when a pending position is opened (action "opened": new signal / immediate / scheduled
|
|
48280
|
+
* or user activation) or closed (action "closed" with closeReason "take_profit" / "stop_loss" /
|
|
48281
|
+
* "time_expired" / "closed"), in both live and backtest.
|
|
48282
|
+
*
|
|
48283
|
+
* Events are processed sequentially in order received, even if callback is async.
|
|
48284
|
+
*
|
|
48285
|
+
* @param fn - Callback function to handle pending lifecycle events
|
|
48286
|
+
* @returns Unsubscribe function to stop listening
|
|
48287
|
+
*
|
|
48288
|
+
* @example
|
|
48289
|
+
* ```typescript
|
|
48290
|
+
* import { listenSignalEvent } from "./function/event";
|
|
48291
|
+
*
|
|
48292
|
+
* const unsubscribe = listenSignalEvent((event) => {
|
|
48293
|
+
* if (event.action === "opened") {
|
|
48294
|
+
* console.log(`Opened ${event.symbol} @ ${event.data.priceOpen}`);
|
|
48295
|
+
* } else {
|
|
48296
|
+
* console.log(`Closed ${event.symbol} (reason: ${event.closeReason})`);
|
|
48297
|
+
* }
|
|
48298
|
+
* });
|
|
48299
|
+
*
|
|
48300
|
+
* // Later: stop listening
|
|
48301
|
+
* unsubscribe();
|
|
48302
|
+
* ```
|
|
48303
|
+
*/
|
|
48304
|
+
function listenSignalEvent(fn) {
|
|
48305
|
+
bt.loggerService.log(LISTEN_SIGNAL_EVENT_METHOD_NAME);
|
|
48306
|
+
return signalEventSubject.subscribe(functoolsKit.queued(async (event) => fn(event)));
|
|
48307
|
+
}
|
|
48308
|
+
/**
|
|
48309
|
+
* Subscribes to filtered pending lifecycle events with one-time execution.
|
|
48310
|
+
*
|
|
48311
|
+
* Listens for events matching the filter predicate, then executes callback once
|
|
48312
|
+
* and automatically unsubscribes. Useful for waiting for a specific open or close.
|
|
48313
|
+
*
|
|
48314
|
+
* @param filterFn - Predicate to filter which events trigger the callback
|
|
48315
|
+
* @param fn - Callback function to handle the filtered event (called only once)
|
|
48316
|
+
* @returns Unsubscribe function to cancel the listener before it fires
|
|
48317
|
+
*
|
|
48318
|
+
* @example
|
|
48319
|
+
* ```typescript
|
|
48320
|
+
* import { listenSignalEventOnce } from "./function/event";
|
|
48321
|
+
*
|
|
48322
|
+
* // Wait for the first close on BTCUSDT
|
|
48323
|
+
* listenSignalEventOnce(
|
|
48324
|
+
* (event) => event.symbol === "BTCUSDT" && event.action === "closed",
|
|
48325
|
+
* (event) => console.log("BTCUSDT closed:", event.closeReason)
|
|
48326
|
+
* );
|
|
48327
|
+
* ```
|
|
48328
|
+
*/
|
|
48329
|
+
function listenSignalEventOnce(filterFn, fn) {
|
|
48330
|
+
bt.loggerService.log(LISTEN_SIGNAL_EVENT_ONCE_METHOD_NAME);
|
|
48331
|
+
let disposeFn;
|
|
48332
|
+
const wrappedFn = async (event) => {
|
|
48333
|
+
if (filterFn(event)) {
|
|
48334
|
+
await fn(event);
|
|
48335
|
+
disposeFn && disposeFn();
|
|
48336
|
+
}
|
|
48337
|
+
};
|
|
48338
|
+
return disposeFn = listenSignalEvent(wrappedFn);
|
|
48339
|
+
}
|
|
46922
48340
|
/**
|
|
46923
48341
|
* Subscribes to active ping events with queued async processing.
|
|
46924
48342
|
*
|
|
@@ -47416,6 +48834,8 @@ const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
|
47416
48834
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
47417
48835
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
47418
48836
|
const BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Backtest.commitCreateSignal";
|
|
48837
|
+
const BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Backtest.commitCreateTakeProfit";
|
|
48838
|
+
const BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Backtest.commitCreateStopLoss";
|
|
47419
48839
|
const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
|
|
47420
48840
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
47421
48841
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
@@ -49152,6 +50572,90 @@ class BacktestUtils {
|
|
|
49152
50572
|
}
|
|
49153
50573
|
await bt.strategyCoreService.createSignal(true, symbol, dto, context);
|
|
49154
50574
|
};
|
|
50575
|
+
/**
|
|
50576
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
50577
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
|
|
50578
|
+
*
|
|
50579
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
50580
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
50581
|
+
* "take_profit" on the next backtest tick. No-op if no pending signal exists.
|
|
50582
|
+
*
|
|
50583
|
+
* @param symbol - Trading pair symbol
|
|
50584
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
50585
|
+
* @param payload - Optional commit payload with id and note
|
|
50586
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
50587
|
+
*
|
|
50588
|
+
* @example
|
|
50589
|
+
* ```typescript
|
|
50590
|
+
* // Report TP fill confirmed on the exchange
|
|
50591
|
+
* await Backtest.commitCreateTakeProfit("BTCUSDT", {
|
|
50592
|
+
* exchangeName: "binance",
|
|
50593
|
+
* strategyName: "my-strategy",
|
|
50594
|
+
* frameName: "1m"
|
|
50595
|
+
* }, { id: "tp-fill-001" });
|
|
50596
|
+
* ```
|
|
50597
|
+
*/
|
|
50598
|
+
this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
|
|
50599
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
|
|
50600
|
+
symbol,
|
|
50601
|
+
context,
|
|
50602
|
+
payload,
|
|
50603
|
+
});
|
|
50604
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
50605
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
50606
|
+
{
|
|
50607
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
50608
|
+
riskName &&
|
|
50609
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
50610
|
+
riskList &&
|
|
50611
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
|
|
50612
|
+
actions &&
|
|
50613
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
|
|
50614
|
+
}
|
|
50615
|
+
await bt.strategyCoreService.createTakeProfit(true, symbol, context, payload);
|
|
50616
|
+
};
|
|
50617
|
+
/**
|
|
50618
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
50619
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
|
|
50620
|
+
*
|
|
50621
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
50622
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
50623
|
+
* "stop_loss" on the next backtest tick. No-op if no pending signal exists.
|
|
50624
|
+
*
|
|
50625
|
+
* @param symbol - Trading pair symbol
|
|
50626
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
50627
|
+
* @param payload - Optional commit payload with id and note
|
|
50628
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
50629
|
+
*
|
|
50630
|
+
* @example
|
|
50631
|
+
* ```typescript
|
|
50632
|
+
* // Report SL fill confirmed on the exchange
|
|
50633
|
+
* await Backtest.commitCreateStopLoss("BTCUSDT", {
|
|
50634
|
+
* exchangeName: "binance",
|
|
50635
|
+
* strategyName: "my-strategy",
|
|
50636
|
+
* frameName: "1m"
|
|
50637
|
+
* }, { id: "sl-fill-001" });
|
|
50638
|
+
* ```
|
|
50639
|
+
*/
|
|
50640
|
+
this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
|
|
50641
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
|
|
50642
|
+
symbol,
|
|
50643
|
+
context,
|
|
50644
|
+
payload,
|
|
50645
|
+
});
|
|
50646
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
50647
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
50648
|
+
{
|
|
50649
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
50650
|
+
riskName &&
|
|
50651
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
50652
|
+
riskList &&
|
|
50653
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
|
|
50654
|
+
actions &&
|
|
50655
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
|
|
50656
|
+
}
|
|
50657
|
+
await bt.strategyCoreService.createStopLoss(true, symbol, context, payload);
|
|
50658
|
+
};
|
|
49155
50659
|
/**
|
|
49156
50660
|
* Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
|
|
49157
50661
|
*
|
|
@@ -50137,6 +51641,8 @@ const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
|
50137
51641
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
50138
51642
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
50139
51643
|
const LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Live.commitCreateSignal";
|
|
51644
|
+
const LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT = "Live.commitCreateTakeProfit";
|
|
51645
|
+
const LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS = "Live.commitCreateStopLoss";
|
|
50140
51646
|
const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
|
|
50141
51647
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
50142
51648
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
@@ -52050,6 +53556,78 @@ class LiveUtils {
|
|
|
52050
53556
|
frameName: "",
|
|
52051
53557
|
});
|
|
52052
53558
|
};
|
|
53559
|
+
/**
|
|
53560
|
+
* Reports that the pending position's take-profit order was actually filled on the exchange
|
|
53561
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based TP check.
|
|
53562
|
+
*
|
|
53563
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
53564
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
53565
|
+
* "take_profit" on the next live tick. No-op if no pending signal exists.
|
|
53566
|
+
*
|
|
53567
|
+
* @param symbol - Trading pair symbol
|
|
53568
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
53569
|
+
* @param payload - Optional commit payload with id and note
|
|
53570
|
+
* @returns Promise that resolves when the take-profit fill is queued
|
|
53571
|
+
*/
|
|
53572
|
+
this.commitCreateTakeProfit = async (symbol, context, payload = {}) => {
|
|
53573
|
+
bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT, {
|
|
53574
|
+
symbol,
|
|
53575
|
+
context,
|
|
53576
|
+
payload,
|
|
53577
|
+
});
|
|
53578
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
53579
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
53580
|
+
{
|
|
53581
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
53582
|
+
riskName &&
|
|
53583
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT);
|
|
53584
|
+
riskList &&
|
|
53585
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
|
|
53586
|
+
actions &&
|
|
53587
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_TAKE_PROFIT));
|
|
53588
|
+
}
|
|
53589
|
+
await bt.strategyCoreService.createTakeProfit(false, symbol, {
|
|
53590
|
+
strategyName: context.strategyName,
|
|
53591
|
+
exchangeName: context.exchangeName,
|
|
53592
|
+
frameName: "",
|
|
53593
|
+
}, payload);
|
|
53594
|
+
};
|
|
53595
|
+
/**
|
|
53596
|
+
* Reports that the pending position's stop-loss order was actually filled on the exchange
|
|
53597
|
+
* (e.g. by candle high/low), forcing a close that bypasses the VWAP-based SL check.
|
|
53598
|
+
*
|
|
53599
|
+
* The exchange and the strategy are parallel states: the framework evaluates TP/SL against VWAP,
|
|
53600
|
+
* but the real order may fill on high/low. The close is deferred and emitted with closeReason
|
|
53601
|
+
* "stop_loss" on the next live tick. No-op if no pending signal exists.
|
|
53602
|
+
*
|
|
53603
|
+
* @param symbol - Trading pair symbol
|
|
53604
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
53605
|
+
* @param payload - Optional commit payload with id and note
|
|
53606
|
+
* @returns Promise that resolves when the stop-loss fill is queued
|
|
53607
|
+
*/
|
|
53608
|
+
this.commitCreateStopLoss = async (symbol, context, payload = {}) => {
|
|
53609
|
+
bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS, {
|
|
53610
|
+
symbol,
|
|
53611
|
+
context,
|
|
53612
|
+
payload,
|
|
53613
|
+
});
|
|
53614
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
53615
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
53616
|
+
{
|
|
53617
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
53618
|
+
riskName &&
|
|
53619
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS);
|
|
53620
|
+
riskList &&
|
|
53621
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
|
|
53622
|
+
actions &&
|
|
53623
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_STOP_LOSS));
|
|
53624
|
+
}
|
|
53625
|
+
await bt.strategyCoreService.createStopLoss(false, symbol, {
|
|
53626
|
+
strategyName: context.strategyName,
|
|
53627
|
+
exchangeName: context.exchangeName,
|
|
53628
|
+
frameName: "",
|
|
53629
|
+
}, payload);
|
|
53630
|
+
};
|
|
52053
53631
|
/**
|
|
52054
53632
|
* Returns the in-memory deferred strategy-state snapshot for the current live iteration.
|
|
52055
53633
|
*
|
|
@@ -60389,7 +61967,9 @@ const SUBJECT_ISOLATION_LIST = [
|
|
|
60389
61967
|
validationSubject,
|
|
60390
61968
|
signalNotifySubject,
|
|
60391
61969
|
beforeStartSubject,
|
|
60392
|
-
afterEndSubject
|
|
61970
|
+
afterEndSubject,
|
|
61971
|
+
scheduleEventSubject,
|
|
61972
|
+
signalEventSubject,
|
|
60393
61973
|
];
|
|
60394
61974
|
/**
|
|
60395
61975
|
* Creates a snapshot function for a given subject by clearing its internal
|
|
@@ -68837,6 +70417,8 @@ exports.commitBreakeven = commitBreakeven;
|
|
|
68837
70417
|
exports.commitCancelScheduled = commitCancelScheduled;
|
|
68838
70418
|
exports.commitClosePending = commitClosePending;
|
|
68839
70419
|
exports.commitCreateSignal = commitCreateSignal;
|
|
70420
|
+
exports.commitCreateStopLoss = commitCreateStopLoss;
|
|
70421
|
+
exports.commitCreateTakeProfit = commitCreateTakeProfit;
|
|
68840
70422
|
exports.commitPartialLoss = commitPartialLoss;
|
|
68841
70423
|
exports.commitPartialLossCost = commitPartialLossCost;
|
|
68842
70424
|
exports.commitPartialProfit = commitPartialProfit;
|
|
@@ -68970,11 +70552,15 @@ exports.listenPartialProfitAvailableOnce = listenPartialProfitAvailableOnce;
|
|
|
68970
70552
|
exports.listenPerformance = listenPerformance;
|
|
68971
70553
|
exports.listenRisk = listenRisk;
|
|
68972
70554
|
exports.listenRiskOnce = listenRiskOnce;
|
|
70555
|
+
exports.listenScheduleEvent = listenScheduleEvent;
|
|
70556
|
+
exports.listenScheduleEventOnce = listenScheduleEventOnce;
|
|
68973
70557
|
exports.listenSchedulePing = listenSchedulePing;
|
|
68974
70558
|
exports.listenSchedulePingOnce = listenSchedulePingOnce;
|
|
68975
70559
|
exports.listenSignal = listenSignal;
|
|
68976
70560
|
exports.listenSignalBacktest = listenSignalBacktest;
|
|
68977
70561
|
exports.listenSignalBacktestOnce = listenSignalBacktestOnce;
|
|
70562
|
+
exports.listenSignalEvent = listenSignalEvent;
|
|
70563
|
+
exports.listenSignalEventOnce = listenSignalEventOnce;
|
|
68978
70564
|
exports.listenSignalLive = listenSignalLive;
|
|
68979
70565
|
exports.listenSignalLiveOnce = listenSignalLiveOnce;
|
|
68980
70566
|
exports.listenSignalNotify = listenSignalNotify;
|