backtest-kit 13.0.0 → 13.2.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/build/index.cjs +505 -131
- package/build/index.mjs +504 -132
- package/package.json +1 -1
- package/types.d.ts +231 -2
package/build/index.cjs
CHANGED
|
@@ -6837,6 +6837,119 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6837
6837
|
}
|
|
6838
6838
|
};
|
|
6839
6839
|
|
|
6840
|
+
/**
|
|
6841
|
+
* Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
|
|
6842
|
+
*
|
|
6843
|
+
* When priceOpen is provided:
|
|
6844
|
+
* - If currentPrice already reached priceOpen (shouldActivateImmediately) →
|
|
6845
|
+
* validates as pending: currentPrice must be between SL and TP
|
|
6846
|
+
* - Otherwise → validates as scheduled: priceOpen must be between SL and TP
|
|
6847
|
+
*
|
|
6848
|
+
* When priceOpen is absent:
|
|
6849
|
+
* - Validates as pending: currentPrice must be between SL and TP
|
|
6850
|
+
*
|
|
6851
|
+
* Checks:
|
|
6852
|
+
* - currentPrice is a finite positive number
|
|
6853
|
+
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
6854
|
+
* - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
|
|
6855
|
+
*
|
|
6856
|
+
* @param signal - Signal DTO returned by getSignal
|
|
6857
|
+
* @param currentPrice - Current market price at the moment of signal creation
|
|
6858
|
+
* @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
|
|
6859
|
+
*/
|
|
6860
|
+
const validateSignal = (signal, currentPrice) => {
|
|
6861
|
+
const errors = [];
|
|
6862
|
+
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
6863
|
+
{
|
|
6864
|
+
if (typeof currentPrice !== "number") {
|
|
6865
|
+
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
6866
|
+
}
|
|
6867
|
+
if (!isFinite(currentPrice)) {
|
|
6868
|
+
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
6869
|
+
}
|
|
6870
|
+
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
6871
|
+
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
if (errors.length > 0) {
|
|
6875
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6876
|
+
return false;
|
|
6877
|
+
}
|
|
6878
|
+
try {
|
|
6879
|
+
validateCommonSignal(signal);
|
|
6880
|
+
}
|
|
6881
|
+
catch (error) {
|
|
6882
|
+
console.error(functoolsKit.getErrorMessage(error));
|
|
6883
|
+
return false;
|
|
6884
|
+
}
|
|
6885
|
+
// Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
|
|
6886
|
+
// - нет priceOpen → pending (открывается по currentPrice)
|
|
6887
|
+
// - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
|
|
6888
|
+
// - priceOpen задан и ещё не достигнут → scheduled
|
|
6889
|
+
const hasPriceOpen = signal.priceOpen !== undefined;
|
|
6890
|
+
const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
|
|
6891
|
+
(signal.position === "short" && currentPrice >= signal.priceOpen));
|
|
6892
|
+
const isScheduled = hasPriceOpen && !shouldActivateImmediately;
|
|
6893
|
+
if (isScheduled) {
|
|
6894
|
+
// Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
|
|
6895
|
+
if (signal.position === "long") {
|
|
6896
|
+
if (isFinite(signal.priceOpen)) {
|
|
6897
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
6898
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6899
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
6900
|
+
}
|
|
6901
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
6902
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6903
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
6904
|
+
}
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
if (signal.position === "short") {
|
|
6908
|
+
if (isFinite(signal.priceOpen)) {
|
|
6909
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
6910
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6911
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
6912
|
+
}
|
|
6913
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
6914
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6915
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
else {
|
|
6921
|
+
// Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
|
|
6922
|
+
if (signal.position === "long") {
|
|
6923
|
+
if (isFinite(currentPrice)) {
|
|
6924
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
6925
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6926
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
6927
|
+
}
|
|
6928
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
6929
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6930
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
if (signal.position === "short") {
|
|
6935
|
+
if (isFinite(currentPrice)) {
|
|
6936
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
6937
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6938
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
6939
|
+
}
|
|
6940
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
6941
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6942
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
if (errors.length > 0) {
|
|
6948
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6949
|
+
}
|
|
6950
|
+
return !errors.length;
|
|
6951
|
+
};
|
|
6952
|
+
|
|
6840
6953
|
const INTERVAL_MINUTES$8 = {
|
|
6841
6954
|
"1m": 1,
|
|
6842
6955
|
"3m": 3,
|
|
@@ -7303,11 +7416,27 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7303
7416
|
self._lastSignalTimestamp = alignedTime;
|
|
7304
7417
|
}
|
|
7305
7418
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7419
|
+
// PRIORITY: a user-queued createPending/createScheduled DTO (set out of async-hooks
|
|
7420
|
+
// context) takes precedence over params.getSignal. When present it is consumed once
|
|
7421
|
+
// here — the slot is cleared and the snapshot rewritten — and then flows through the
|
|
7422
|
+
// exact same pipeline (risk check, priceOpen branching, onSignalSync on open) as a
|
|
7423
|
+
// signal returned by getSignal would.
|
|
7424
|
+
let signal;
|
|
7425
|
+
{
|
|
7426
|
+
if (!self._userSignal) {
|
|
7427
|
+
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
7428
|
+
signal = await Promise.race([
|
|
7429
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
|
|
7430
|
+
functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
|
|
7431
|
+
]);
|
|
7432
|
+
}
|
|
7433
|
+
if (self._userSignal) {
|
|
7434
|
+
signal = self._userSignal;
|
|
7435
|
+
self._userSignal = null;
|
|
7436
|
+
await PERSIST_STRATEGY_FN(self);
|
|
7437
|
+
}
|
|
7438
|
+
self._userSignal = null;
|
|
7439
|
+
}
|
|
7311
7440
|
if (typeof signal === "symbol") {
|
|
7312
7441
|
throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
|
|
7313
7442
|
}
|
|
@@ -7462,18 +7591,20 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7462
7591
|
self._lastPendingId = recentSignal.id;
|
|
7463
7592
|
}
|
|
7464
7593
|
}
|
|
7465
|
-
//
|
|
7594
|
+
// Read deferred strategy state (commit queue + deferred user actions) so any
|
|
7466
7595
|
// confirmed-but-not-yet-forwarded broker operation survives a live crash and is
|
|
7467
|
-
// re-drained on the next tick.
|
|
7468
|
-
//
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7596
|
+
// re-drained on the next tick. Read here, before the pending restore which may
|
|
7597
|
+
// early-return on exchange/strategy mismatch.
|
|
7598
|
+
const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7599
|
+
if (strategyData) {
|
|
7600
|
+
// Deferred user actions are restored unconditionally: a deferred close belongs to
|
|
7601
|
+
// an already-cleared _pendingSignal, and cancel/activate belong to the scheduled
|
|
7602
|
+
// signal — none of them are tied to the currently-pending signal's id. A queued
|
|
7603
|
+
// createSignal is a not-yet-consumed signal source and likewise stands on its own.
|
|
7604
|
+
self._userSignal = strategyData.createSignal;
|
|
7605
|
+
self._closedSignal = strategyData.closedSignal;
|
|
7606
|
+
self._cancelledSignal = strategyData.cancelledSignal;
|
|
7607
|
+
self._activatedSignal = strategyData.activatedSignal;
|
|
7477
7608
|
}
|
|
7478
7609
|
// Restore pending signal
|
|
7479
7610
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
@@ -7491,6 +7622,14 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7491
7622
|
pendingSignal.minuteEstimatedTime = Infinity;
|
|
7492
7623
|
}
|
|
7493
7624
|
self._pendingSignal = pendingSignal;
|
|
7625
|
+
// Restore the commit queue only if the snapshot belongs to this exact pending
|
|
7626
|
+
// signal (pendingSignalId === restored id). The queue holds confirmed-but-not-yet
|
|
7627
|
+
// forwarded broker ops (average-buy / partial-* / trailing-* / breakeven) that are
|
|
7628
|
+
// always tied to the active position; a mismatch means the snapshot is stale and
|
|
7629
|
+
// the queue is dropped to avoid replaying ops against the wrong position.
|
|
7630
|
+
if (strategyData && strategyData.pendingSignalId === pendingSignal.id) {
|
|
7631
|
+
self._commitQueue = strategyData.commitQueue ?? [];
|
|
7632
|
+
}
|
|
7494
7633
|
// Call onActive callback for restored signal
|
|
7495
7634
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
7496
7635
|
const currentTime = self.params.execution.context.when.getTime();
|
|
@@ -7540,6 +7679,8 @@ const PERSIST_STRATEGY_FN = async (self) => {
|
|
|
7540
7679
|
return;
|
|
7541
7680
|
}
|
|
7542
7681
|
await PersistStrategyAdapter.writeStrategyData({
|
|
7682
|
+
pendingSignalId: self._pendingSignal?.id ?? null,
|
|
7683
|
+
createSignal: self._userSignal,
|
|
7543
7684
|
commitQueue: self._commitQueue,
|
|
7544
7685
|
closedSignal: self._closedSignal,
|
|
7545
7686
|
cancelledSignal: self._cancelledSignal,
|
|
@@ -9675,6 +9816,13 @@ class ClientStrategy {
|
|
|
9675
9816
|
this._cancelledSignal = null;
|
|
9676
9817
|
this._closedSignal = null;
|
|
9677
9818
|
this._activatedSignal = null;
|
|
9819
|
+
/**
|
|
9820
|
+
* User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
|
|
9821
|
+
* params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
|
|
9822
|
+
* and the existing pipeline (priceOpen decides pending vs scheduled, onSignalSync on open)
|
|
9823
|
+
* is reused.
|
|
9824
|
+
*/
|
|
9825
|
+
this._userSignal = null;
|
|
9678
9826
|
/** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
|
|
9679
9827
|
this._commitQueue = [];
|
|
9680
9828
|
/**
|
|
@@ -11325,6 +11473,88 @@ class ClientStrategy {
|
|
|
11325
11473
|
await PERSIST_STRATEGY_FN(this);
|
|
11326
11474
|
// Commit will be emitted in tick() with correct currentTime
|
|
11327
11475
|
}
|
|
11476
|
+
/**
|
|
11477
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of
|
|
11478
|
+
* params.getSignal.
|
|
11479
|
+
*
|
|
11480
|
+
* Works OUT of the async-hooks execution context (uses this.params.symbol directly).
|
|
11481
|
+
* priceOpen decides the outcome in the existing pipeline: when omitted the position opens
|
|
11482
|
+
* immediately at currentPrice; when provided the pipeline opens immediately if priceOpen is
|
|
11483
|
+
* already reached, otherwise registers a scheduled (priceOpen-awaiting) signal. On the next
|
|
11484
|
+
* tick GET_SIGNAL_FN consumes _userSignal and runs the normal pipeline, so onSignalSync
|
|
11485
|
+
* delivery is checked by OPEN_NEW_PENDING_SIGNAL_FN exactly as for a getSignal-produced signal.
|
|
11486
|
+
*
|
|
11487
|
+
* Validation (BEFORE any state mutation — a rejected call stores nothing):
|
|
11488
|
+
* - The DTO must pass the intrinsic shape/price checks.
|
|
11489
|
+
* - There must be NO signal already in flight: no active pending signal, no scheduled signal,
|
|
11490
|
+
* no already-queued createSignal, and no deferred activate/close/cancel awaiting drain.
|
|
11491
|
+
* Creating a new signal on top of an existing one is rejected.
|
|
11492
|
+
*
|
|
11493
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11494
|
+
* @param currentPrice - Current market price (priceOpen fallback for immediate signals)
|
|
11495
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
11496
|
+
* @returns Promise that resolves when the DTO is queued (and persisted in live mode)
|
|
11497
|
+
* @throws {Error} If the DTO is invalid or a signal/deferred action is already in flight
|
|
11498
|
+
*/
|
|
11499
|
+
async createSignal(symbol, currentPrice, dto) {
|
|
11500
|
+
this.params.logger.debug("ClientStrategy createSignal", { symbol, currentPrice });
|
|
11501
|
+
// Validate BEFORE mutating state — a bad DTO or a busy strategy must store nothing.
|
|
11502
|
+
// Reuse validateSignal (the canonical getSignal-output validator, branching
|
|
11503
|
+
// pending-vs-scheduled by the same rule as GET_SIGNAL_FN). It forwards to
|
|
11504
|
+
// validateCommonSignal which requires priceOpen and minuteEstimatedTime, so normalize the
|
|
11505
|
+
// DTO to the defaults GET_SIGNAL_FN would apply (priceOpen → currentPrice for an immediate
|
|
11506
|
+
// signal, minuteEstimatedTime → CC_MAX_SIGNAL_LIFETIME_MINUTES). The original dto stored in
|
|
11507
|
+
// _userSignal is left untouched so GET_SIGNAL_FN applies its own defaults on consume.
|
|
11508
|
+
if (!validateSignal({
|
|
11509
|
+
...dto,
|
|
11510
|
+
priceOpen: dto.priceOpen ?? currentPrice,
|
|
11511
|
+
minuteEstimatedTime: dto.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
|
|
11512
|
+
}, currentPrice)) {
|
|
11513
|
+
throw new Error(`ClientStrategy createSignal: invalid signal DTO for symbol=${symbol}`);
|
|
11514
|
+
}
|
|
11515
|
+
// Reject if any signal is already in flight or any deferred action is awaiting drain.
|
|
11516
|
+
if (this._pendingSignal) {
|
|
11517
|
+
throw new Error(`ClientStrategy createSignal: a pending signal already exists for symbol=${symbol}`);
|
|
11518
|
+
}
|
|
11519
|
+
if (this._scheduledSignal) {
|
|
11520
|
+
throw new Error(`ClientStrategy createSignal: a scheduled signal already exists for symbol=${symbol}`);
|
|
11521
|
+
}
|
|
11522
|
+
if (this._userSignal) {
|
|
11523
|
+
throw new Error(`ClientStrategy createSignal: a signal is already queued for creation for symbol=${symbol}`);
|
|
11524
|
+
}
|
|
11525
|
+
if (this._activatedSignal) {
|
|
11526
|
+
throw new Error(`ClientStrategy createSignal: a scheduled activation is pending for symbol=${symbol}`);
|
|
11527
|
+
}
|
|
11528
|
+
if (this._closedSignal) {
|
|
11529
|
+
throw new Error(`ClientStrategy createSignal: a pending close is awaiting for symbol=${symbol}`);
|
|
11530
|
+
}
|
|
11531
|
+
if (this._cancelledSignal) {
|
|
11532
|
+
throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
|
|
11533
|
+
}
|
|
11534
|
+
this._userSignal = dto;
|
|
11535
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11536
|
+
}
|
|
11537
|
+
/**
|
|
11538
|
+
* Returns the deferred strategy-state snapshot exactly as it would be written to persist on
|
|
11539
|
+
* this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
|
|
11540
|
+
* plus the current pending signal id.
|
|
11541
|
+
*
|
|
11542
|
+
* Synchronous in-memory read (no disk access), so it works OUT of the async-hooks context.
|
|
11543
|
+
*
|
|
11544
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11545
|
+
* @returns The current StrategyData snapshot held in memory
|
|
11546
|
+
*/
|
|
11547
|
+
async getStatus(symbol) {
|
|
11548
|
+
this.params.logger.debug("ClientStrategy getStatus", { symbol });
|
|
11549
|
+
return {
|
|
11550
|
+
pendingSignalId: this._pendingSignal?.id ?? null,
|
|
11551
|
+
createSignal: this._userSignal,
|
|
11552
|
+
commitQueue: this._commitQueue,
|
|
11553
|
+
closedSignal: this._closedSignal,
|
|
11554
|
+
cancelledSignal: this._cancelledSignal,
|
|
11555
|
+
activatedSignal: this._activatedSignal,
|
|
11556
|
+
};
|
|
11557
|
+
}
|
|
11328
11558
|
/**
|
|
11329
11559
|
* Validates preconditions for partialProfit without mutating state.
|
|
11330
11560
|
*
|
|
@@ -12623,6 +12853,8 @@ class MergeRisk {
|
|
|
12623
12853
|
|
|
12624
12854
|
/** Default interval for strategies that do not specify one */
|
|
12625
12855
|
const STRATEGY_DEFAULT_INTERVAL = "1m";
|
|
12856
|
+
/** If not specified strategy will not open positions until createSignal is called manually */
|
|
12857
|
+
const STRATEGY_DEFAULT_SIGNAL = () => null;
|
|
12626
12858
|
/**
|
|
12627
12859
|
* If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
|
|
12628
12860
|
* to the exchange (e.g. limit order failed to fill).
|
|
@@ -13056,7 +13288,7 @@ class StrategyConnectionService {
|
|
|
13056
13288
|
* @returns Configured ClientStrategy instance
|
|
13057
13289
|
*/
|
|
13058
13290
|
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
13059
|
-
const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
13291
|
+
const { riskName = "", riskList = [], getSignal = STRATEGY_DEFAULT_SIGNAL, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
13060
13292
|
return new ClientStrategy({
|
|
13061
13293
|
symbol,
|
|
13062
13294
|
interval,
|
|
@@ -14075,6 +14307,47 @@ class StrategyConnectionService {
|
|
|
14075
14307
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14076
14308
|
await strategy.closePending(symbol, backtest, payload);
|
|
14077
14309
|
};
|
|
14310
|
+
/**
|
|
14311
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
|
|
14312
|
+
*
|
|
14313
|
+
* Delegates to ClientStrategy.createSignal(). Validated and rejected if a signal/deferred
|
|
14314
|
+
* action is already in flight. Works out of the async-hooks execution context.
|
|
14315
|
+
*
|
|
14316
|
+
* @param backtest - Whether running in backtest mode
|
|
14317
|
+
* @param symbol - Trading pair symbol
|
|
14318
|
+
* @param dto - Signal DTO to open
|
|
14319
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14320
|
+
* @returns Promise that resolves when the DTO is queued
|
|
14321
|
+
*/
|
|
14322
|
+
this.createSignal = async (backtest, symbol, dto, context) => {
|
|
14323
|
+
this.loggerService.log("strategyConnectionService createSignal", {
|
|
14324
|
+
symbol,
|
|
14325
|
+
context,
|
|
14326
|
+
backtest,
|
|
14327
|
+
});
|
|
14328
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14329
|
+
const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
|
|
14330
|
+
await strategy.createSignal(symbol, currentPrice, dto);
|
|
14331
|
+
};
|
|
14332
|
+
/**
|
|
14333
|
+
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
14334
|
+
*
|
|
14335
|
+
* Delegates to ClientStrategy.getStatus(). Synchronous in-memory read; works out of context.
|
|
14336
|
+
*
|
|
14337
|
+
* @param backtest - Whether running in backtest mode
|
|
14338
|
+
* @param symbol - Trading pair symbol
|
|
14339
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14340
|
+
* @returns Promise resolving to the current StrategyData snapshot
|
|
14341
|
+
*/
|
|
14342
|
+
this.getStatus = async (backtest, symbol, context) => {
|
|
14343
|
+
this.loggerService.log("strategyConnectionService getStatus", {
|
|
14344
|
+
symbol,
|
|
14345
|
+
context,
|
|
14346
|
+
backtest,
|
|
14347
|
+
});
|
|
14348
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14349
|
+
return await strategy.getStatus(symbol);
|
|
14350
|
+
};
|
|
14078
14351
|
/**
|
|
14079
14352
|
* Checks whether `partialProfit` would succeed without executing it.
|
|
14080
14353
|
* Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
|
|
@@ -17678,6 +17951,47 @@ class StrategyCoreService {
|
|
|
17678
17951
|
await this.validate(context);
|
|
17679
17952
|
return await this.strategyConnectionService.closePending(backtest, symbol, context, payload);
|
|
17680
17953
|
};
|
|
17954
|
+
/**
|
|
17955
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
|
|
17956
|
+
*
|
|
17957
|
+
* Validates the context, then delegates to StrategyConnectionService.createSignal().
|
|
17958
|
+
* Rejected if a signal or deferred action is already in flight. Does not require execution context.
|
|
17959
|
+
*
|
|
17960
|
+
* @param backtest - Whether running in backtest mode
|
|
17961
|
+
* @param symbol - Trading pair symbol
|
|
17962
|
+
* @param dto - Signal DTO to open
|
|
17963
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
17964
|
+
* @returns Promise that resolves when the DTO is queued
|
|
17965
|
+
*/
|
|
17966
|
+
this.createSignal = async (backtest, symbol, dto, context) => {
|
|
17967
|
+
this.loggerService.log("strategyCoreService createSignal", {
|
|
17968
|
+
symbol,
|
|
17969
|
+
context,
|
|
17970
|
+
backtest,
|
|
17971
|
+
});
|
|
17972
|
+
await this.validate(context);
|
|
17973
|
+
return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
|
|
17974
|
+
};
|
|
17975
|
+
/**
|
|
17976
|
+
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
17977
|
+
*
|
|
17978
|
+
* Validates the context, then delegates to StrategyConnectionService.getStatus().
|
|
17979
|
+
* Does not require execution context.
|
|
17980
|
+
*
|
|
17981
|
+
* @param backtest - Whether running in backtest mode
|
|
17982
|
+
* @param symbol - Trading pair symbol
|
|
17983
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
17984
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
17985
|
+
*/
|
|
17986
|
+
this.getStatus = async (backtest, symbol, context) => {
|
|
17987
|
+
this.loggerService.log("strategyCoreService getStatus", {
|
|
17988
|
+
symbol,
|
|
17989
|
+
context,
|
|
17990
|
+
backtest,
|
|
17991
|
+
});
|
|
17992
|
+
await this.validate(context);
|
|
17993
|
+
return await this.strategyConnectionService.getStatus(backtest, symbol, context);
|
|
17994
|
+
};
|
|
17681
17995
|
/**
|
|
17682
17996
|
* Disposes the ClientStrategy instance for the given context.
|
|
17683
17997
|
*
|
|
@@ -19202,7 +19516,7 @@ class StrategySchemaService {
|
|
|
19202
19516
|
if (strategySchema.interval && typeof strategySchema.interval !== "string") {
|
|
19203
19517
|
throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
|
|
19204
19518
|
}
|
|
19205
|
-
if (typeof strategySchema.getSignal !== "function") {
|
|
19519
|
+
if (strategySchema.getSignal && typeof strategySchema.getSignal !== "function") {
|
|
19206
19520
|
throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
|
|
19207
19521
|
}
|
|
19208
19522
|
};
|
|
@@ -42916,6 +43230,8 @@ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOve
|
|
|
42916
43230
|
const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
|
|
42917
43231
|
const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
|
|
42918
43232
|
const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
|
|
43233
|
+
const COMMIT_CREATE_SIGNAL_METHOD_NAME = "strategy.commitCreateSignal";
|
|
43234
|
+
const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
|
|
42919
43235
|
/**
|
|
42920
43236
|
* Cancels the scheduled signal without stopping the strategy.
|
|
42921
43237
|
*
|
|
@@ -44980,6 +45296,69 @@ async function commitSignalNotify(symbol, payload = {}) {
|
|
|
44980
45296
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
44981
45297
|
await bt.notificationHelperService.commitSignalNotify(payload, symbol, currentPrice, { strategyName, exchangeName, frameName }, isBacktest);
|
|
44982
45298
|
}
|
|
45299
|
+
/**
|
|
45300
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of params.getSignal.
|
|
45301
|
+
*
|
|
45302
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
45303
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
45304
|
+
* scheduled (priceOpen-awaiting) signal. The DTO is validated (reusing validateSignal) and the
|
|
45305
|
+
* call is rejected if a signal or deferred action is already in flight.
|
|
45306
|
+
*
|
|
45307
|
+
* Automatically detects backtest/live mode from execution context.
|
|
45308
|
+
*
|
|
45309
|
+
* @param symbol - Trading pair symbol
|
|
45310
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
45311
|
+
* @returns Promise that resolves when the DTO is queued
|
|
45312
|
+
*
|
|
45313
|
+
* @example
|
|
45314
|
+
* ```typescript
|
|
45315
|
+
* import { createSignal } from "backtest-kit";
|
|
45316
|
+
*
|
|
45317
|
+
* // Open immediately at current price
|
|
45318
|
+
* await commitCreateSignal("BTCUSDT", { position: "long", priceTakeProfit: 110, priceStopLoss: 90 });
|
|
45319
|
+
* ```
|
|
45320
|
+
*/
|
|
45321
|
+
async function commitCreateSignal(symbol, dto) {
|
|
45322
|
+
bt.loggerService.info(COMMIT_CREATE_SIGNAL_METHOD_NAME, { symbol });
|
|
45323
|
+
if (!ExecutionContextService.hasContext()) {
|
|
45324
|
+
throw new Error("createSignal requires an execution context");
|
|
45325
|
+
}
|
|
45326
|
+
if (!MethodContextService.hasContext()) {
|
|
45327
|
+
throw new Error("createSignal requires a method context");
|
|
45328
|
+
}
|
|
45329
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
45330
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
45331
|
+
await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
|
|
45332
|
+
}
|
|
45333
|
+
/**
|
|
45334
|
+
* Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
|
|
45335
|
+
* createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
|
|
45336
|
+
*
|
|
45337
|
+
* Automatically detects backtest/live mode from execution context.
|
|
45338
|
+
*
|
|
45339
|
+
* @param symbol - Trading pair symbol
|
|
45340
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
45341
|
+
*
|
|
45342
|
+
* @example
|
|
45343
|
+
* ```typescript
|
|
45344
|
+
* import { getStrategyStatus } from "backtest-kit";
|
|
45345
|
+
*
|
|
45346
|
+
* const status = await getStrategyStatus("BTCUSDT");
|
|
45347
|
+
* console.log(status.pendingSignalId, status.commitQueue.length);
|
|
45348
|
+
* ```
|
|
45349
|
+
*/
|
|
45350
|
+
async function getStrategyStatus(symbol) {
|
|
45351
|
+
bt.loggerService.info(GET_STRATEGY_STATUS_METHOD_NAME, { symbol });
|
|
45352
|
+
if (!ExecutionContextService.hasContext()) {
|
|
45353
|
+
throw new Error("getStrategyStatus requires an execution context");
|
|
45354
|
+
}
|
|
45355
|
+
if (!MethodContextService.hasContext()) {
|
|
45356
|
+
throw new Error("getStrategyStatus requires a method context");
|
|
45357
|
+
}
|
|
45358
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
45359
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
45360
|
+
return await bt.strategyCoreService.getStatus(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
45361
|
+
}
|
|
44983
45362
|
|
|
44984
45363
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
44985
45364
|
/**
|
|
@@ -46651,6 +47030,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPosi
|
|
|
46651
47030
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
46652
47031
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
46653
47032
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
47033
|
+
const BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Backtest.commitCreateSignal";
|
|
47034
|
+
const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
|
|
46654
47035
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
46655
47036
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
46656
47037
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
|
|
@@ -48355,6 +48736,53 @@ class BacktestUtils {
|
|
|
48355
48736
|
}
|
|
48356
48737
|
await bt.strategyCoreService.closePending(true, symbol, context, payload);
|
|
48357
48738
|
};
|
|
48739
|
+
/**
|
|
48740
|
+
* Queues a user-supplied signal DTO to be consumed by the next backtest tick instead of
|
|
48741
|
+
* params.getSignal.
|
|
48742
|
+
*
|
|
48743
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
48744
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
48745
|
+
* scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
|
|
48746
|
+
*
|
|
48747
|
+
* @param symbol - Trading pair symbol
|
|
48748
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
48749
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
48750
|
+
* @returns Promise that resolves when the DTO is queued
|
|
48751
|
+
*/
|
|
48752
|
+
this.commitCreateSignal = async (symbol, context, dto) => {
|
|
48753
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL, {
|
|
48754
|
+
symbol,
|
|
48755
|
+
context,
|
|
48756
|
+
});
|
|
48757
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
48758
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
48759
|
+
{
|
|
48760
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
48761
|
+
riskName &&
|
|
48762
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
48763
|
+
riskList &&
|
|
48764
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL));
|
|
48765
|
+
actions &&
|
|
48766
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_COMMIT_CREATE_SIGNAL));
|
|
48767
|
+
}
|
|
48768
|
+
await bt.strategyCoreService.createSignal(true, symbol, dto, context);
|
|
48769
|
+
};
|
|
48770
|
+
/**
|
|
48771
|
+
* Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
|
|
48772
|
+
*
|
|
48773
|
+
* @param symbol - Trading pair symbol
|
|
48774
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
48775
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
48776
|
+
*/
|
|
48777
|
+
this.getStrategyStatus = async (symbol, context) => {
|
|
48778
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS, {
|
|
48779
|
+
symbol,
|
|
48780
|
+
context,
|
|
48781
|
+
});
|
|
48782
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
48783
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
48784
|
+
return await bt.strategyCoreService.getStatus(true, symbol, context);
|
|
48785
|
+
};
|
|
48358
48786
|
/**
|
|
48359
48787
|
* Executes partial close at profit level (moving toward TP).
|
|
48360
48788
|
*
|
|
@@ -49313,6 +49741,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPart
|
|
|
49313
49741
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
49314
49742
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
49315
49743
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
49744
|
+
const LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL = "Live.commitCreateSignal";
|
|
49745
|
+
const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
|
|
49316
49746
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
49317
49747
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
49318
49748
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
|
|
@@ -51190,6 +51620,61 @@ class LiveUtils {
|
|
|
51190
51620
|
frameName: "",
|
|
51191
51621
|
}, payload);
|
|
51192
51622
|
};
|
|
51623
|
+
/**
|
|
51624
|
+
* Queues a user-supplied signal DTO to be consumed by the next live tick instead of
|
|
51625
|
+
* params.getSignal.
|
|
51626
|
+
*
|
|
51627
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
51628
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
51629
|
+
* scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
|
|
51630
|
+
*
|
|
51631
|
+
* @param symbol - Trading pair symbol
|
|
51632
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
51633
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
51634
|
+
* @returns Promise that resolves when the DTO is queued
|
|
51635
|
+
*/
|
|
51636
|
+
this.commitCreateSignal = async (symbol, context, dto) => {
|
|
51637
|
+
bt.loggerService.info(LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL, {
|
|
51638
|
+
symbol,
|
|
51639
|
+
context,
|
|
51640
|
+
});
|
|
51641
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
51642
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
51643
|
+
{
|
|
51644
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
51645
|
+
riskName &&
|
|
51646
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL);
|
|
51647
|
+
riskList &&
|
|
51648
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL));
|
|
51649
|
+
actions &&
|
|
51650
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_COMMIT_CREATE_SIGNAL));
|
|
51651
|
+
}
|
|
51652
|
+
await bt.strategyCoreService.createSignal(false, symbol, dto, {
|
|
51653
|
+
strategyName: context.strategyName,
|
|
51654
|
+
exchangeName: context.exchangeName,
|
|
51655
|
+
frameName: "",
|
|
51656
|
+
});
|
|
51657
|
+
};
|
|
51658
|
+
/**
|
|
51659
|
+
* Returns the in-memory deferred strategy-state snapshot for the current live iteration.
|
|
51660
|
+
*
|
|
51661
|
+
* @param symbol - Trading pair symbol
|
|
51662
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
51663
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
51664
|
+
*/
|
|
51665
|
+
this.getStrategyStatus = async (symbol, context) => {
|
|
51666
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_STRATEGY_STATUS, {
|
|
51667
|
+
symbol,
|
|
51668
|
+
context,
|
|
51669
|
+
});
|
|
51670
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
51671
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
51672
|
+
return await bt.strategyCoreService.getStatus(false, symbol, {
|
|
51673
|
+
strategyName: context.strategyName,
|
|
51674
|
+
exchangeName: context.exchangeName,
|
|
51675
|
+
frameName: "",
|
|
51676
|
+
});
|
|
51677
|
+
};
|
|
51193
51678
|
/**
|
|
51194
51679
|
* Executes partial close at profit level (moving toward TP).
|
|
51195
51680
|
*
|
|
@@ -67836,119 +68321,6 @@ const percentValue = (yesterdayValue, todayValue) => {
|
|
|
67836
68321
|
return yesterdayValue / todayValue - 1;
|
|
67837
68322
|
};
|
|
67838
68323
|
|
|
67839
|
-
/**
|
|
67840
|
-
* Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
|
|
67841
|
-
*
|
|
67842
|
-
* When priceOpen is provided:
|
|
67843
|
-
* - If currentPrice already reached priceOpen (shouldActivateImmediately) →
|
|
67844
|
-
* validates as pending: currentPrice must be between SL and TP
|
|
67845
|
-
* - Otherwise → validates as scheduled: priceOpen must be between SL and TP
|
|
67846
|
-
*
|
|
67847
|
-
* When priceOpen is absent:
|
|
67848
|
-
* - Validates as pending: currentPrice must be between SL and TP
|
|
67849
|
-
*
|
|
67850
|
-
* Checks:
|
|
67851
|
-
* - currentPrice is a finite positive number
|
|
67852
|
-
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
67853
|
-
* - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
|
|
67854
|
-
*
|
|
67855
|
-
* @param signal - Signal DTO returned by getSignal
|
|
67856
|
-
* @param currentPrice - Current market price at the moment of signal creation
|
|
67857
|
-
* @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
|
|
67858
|
-
*/
|
|
67859
|
-
const validateSignal = (signal, currentPrice) => {
|
|
67860
|
-
const errors = [];
|
|
67861
|
-
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
67862
|
-
{
|
|
67863
|
-
if (typeof currentPrice !== "number") {
|
|
67864
|
-
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
67865
|
-
}
|
|
67866
|
-
if (!isFinite(currentPrice)) {
|
|
67867
|
-
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
67868
|
-
}
|
|
67869
|
-
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
67870
|
-
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
67871
|
-
}
|
|
67872
|
-
}
|
|
67873
|
-
if (errors.length > 0) {
|
|
67874
|
-
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
67875
|
-
return false;
|
|
67876
|
-
}
|
|
67877
|
-
try {
|
|
67878
|
-
validateCommonSignal(signal);
|
|
67879
|
-
}
|
|
67880
|
-
catch (error) {
|
|
67881
|
-
console.error(functoolsKit.getErrorMessage(error));
|
|
67882
|
-
return false;
|
|
67883
|
-
}
|
|
67884
|
-
// Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
|
|
67885
|
-
// - нет priceOpen → pending (открывается по currentPrice)
|
|
67886
|
-
// - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
|
|
67887
|
-
// - priceOpen задан и ещё не достигнут → scheduled
|
|
67888
|
-
const hasPriceOpen = signal.priceOpen !== undefined;
|
|
67889
|
-
const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
|
|
67890
|
-
(signal.position === "short" && currentPrice >= signal.priceOpen));
|
|
67891
|
-
const isScheduled = hasPriceOpen && !shouldActivateImmediately;
|
|
67892
|
-
if (isScheduled) {
|
|
67893
|
-
// Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
|
|
67894
|
-
if (signal.position === "long") {
|
|
67895
|
-
if (isFinite(signal.priceOpen)) {
|
|
67896
|
-
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
67897
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67898
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
67899
|
-
}
|
|
67900
|
-
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
67901
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67902
|
-
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
67903
|
-
}
|
|
67904
|
-
}
|
|
67905
|
-
}
|
|
67906
|
-
if (signal.position === "short") {
|
|
67907
|
-
if (isFinite(signal.priceOpen)) {
|
|
67908
|
-
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
67909
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67910
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
67911
|
-
}
|
|
67912
|
-
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
67913
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67914
|
-
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
67915
|
-
}
|
|
67916
|
-
}
|
|
67917
|
-
}
|
|
67918
|
-
}
|
|
67919
|
-
else {
|
|
67920
|
-
// Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
|
|
67921
|
-
if (signal.position === "long") {
|
|
67922
|
-
if (isFinite(currentPrice)) {
|
|
67923
|
-
if (currentPrice <= signal.priceStopLoss) {
|
|
67924
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67925
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
67926
|
-
}
|
|
67927
|
-
if (currentPrice >= signal.priceTakeProfit) {
|
|
67928
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67929
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
67930
|
-
}
|
|
67931
|
-
}
|
|
67932
|
-
}
|
|
67933
|
-
if (signal.position === "short") {
|
|
67934
|
-
if (isFinite(currentPrice)) {
|
|
67935
|
-
if (currentPrice >= signal.priceStopLoss) {
|
|
67936
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67937
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
67938
|
-
}
|
|
67939
|
-
if (currentPrice <= signal.priceTakeProfit) {
|
|
67940
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67941
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
67942
|
-
}
|
|
67943
|
-
}
|
|
67944
|
-
}
|
|
67945
|
-
}
|
|
67946
|
-
if (errors.length > 0) {
|
|
67947
|
-
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
67948
|
-
}
|
|
67949
|
-
return !errors.length;
|
|
67950
|
-
};
|
|
67951
|
-
|
|
67952
68324
|
exports.ActionBase = ActionBase;
|
|
67953
68325
|
exports.Backtest = Backtest;
|
|
67954
68326
|
exports.Breakeven = Breakeven;
|
|
@@ -68058,6 +68430,7 @@ exports.commitAverageBuy = commitAverageBuy;
|
|
|
68058
68430
|
exports.commitBreakeven = commitBreakeven;
|
|
68059
68431
|
exports.commitCancelScheduled = commitCancelScheduled;
|
|
68060
68432
|
exports.commitClosePending = commitClosePending;
|
|
68433
|
+
exports.commitCreateSignal = commitCreateSignal;
|
|
68061
68434
|
exports.commitPartialLoss = commitPartialLoss;
|
|
68062
68435
|
exports.commitPartialLossCost = commitPartialLossCost;
|
|
68063
68436
|
exports.commitPartialProfit = commitPartialProfit;
|
|
@@ -68141,6 +68514,7 @@ exports.getSessionData = getSessionData;
|
|
|
68141
68514
|
exports.getSignalState = getSignalState;
|
|
68142
68515
|
exports.getSizingSchema = getSizingSchema;
|
|
68143
68516
|
exports.getStrategySchema = getStrategySchema;
|
|
68517
|
+
exports.getStrategyStatus = getStrategyStatus;
|
|
68144
68518
|
exports.getSymbol = getSymbol;
|
|
68145
68519
|
exports.getTimestamp = getTimestamp;
|
|
68146
68520
|
exports.getTotalClosed = getTotalClosed;
|