backtest-kit 4.0.1 → 5.0.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 +575 -25
- package/build/index.mjs +574 -26
- package/package.json +1 -1
- package/types.d.ts +358 -16
package/build/index.cjs
CHANGED
|
@@ -470,9 +470,17 @@ const GLOBAL_CONFIG = {
|
|
|
470
470
|
* Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
|
|
471
471
|
* This can help improve average entry price in cases where price has rebounded after entry but is still below priceOpen, without waiting for a new lower price.
|
|
472
472
|
*
|
|
473
|
-
* Default:
|
|
473
|
+
* Default: false (DCA logic enabled only when antirecord is broken)
|
|
474
474
|
*/
|
|
475
475
|
CC_ENABLE_DCA_EVERYWHERE: false,
|
|
476
|
+
/**
|
|
477
|
+
* Enables PPPL (Partial Profit, Partial Loss) logic even if this breaks a direction of exits
|
|
478
|
+
* Allows to take partial profit or loss on a position even if it results in a mix of profit and loss exits
|
|
479
|
+
* This can help lock in profits or cut losses on part of the position without waiting for a perfect exit scenario.
|
|
480
|
+
*
|
|
481
|
+
* Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
|
|
482
|
+
*/
|
|
483
|
+
CC_ENABLE_PPPL_EVERYWHERE: false,
|
|
476
484
|
/**
|
|
477
485
|
* Cost of entering a position (in USD).
|
|
478
486
|
* This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
|
|
@@ -6630,6 +6638,18 @@ class ClientStrategy {
|
|
|
6630
6638
|
}
|
|
6631
6639
|
return entries.map((e) => e.price);
|
|
6632
6640
|
}
|
|
6641
|
+
/**
|
|
6642
|
+
* Returns the list of partial closes for the current pending signal.
|
|
6643
|
+
*
|
|
6644
|
+
* Each entry records a partial profit or loss close event with its type,
|
|
6645
|
+
* percent closed, price at close, cost basis snapshot, and entry count at close.
|
|
6646
|
+
*
|
|
6647
|
+
* Returns null if no pending signal exists.
|
|
6648
|
+
* Returns an empty array if no partial closes have been executed.
|
|
6649
|
+
*
|
|
6650
|
+
* @param symbol - Trading pair symbol
|
|
6651
|
+
* @returns Promise resolving to array of partial close records or null
|
|
6652
|
+
*/
|
|
6633
6653
|
async getPositionPartials(symbol) {
|
|
6634
6654
|
this.params.logger.debug("ClientStrategy getPositionPartials", { symbol });
|
|
6635
6655
|
if (!this._pendingSignal) {
|
|
@@ -6637,6 +6657,34 @@ class ClientStrategy {
|
|
|
6637
6657
|
}
|
|
6638
6658
|
return this._pendingSignal._partial ?? [];
|
|
6639
6659
|
}
|
|
6660
|
+
/**
|
|
6661
|
+
* Returns the list of DCA entry prices and costs for the current pending signal.
|
|
6662
|
+
*
|
|
6663
|
+
* Each entry records the price and cost of a single position entry.
|
|
6664
|
+
* The first element is always the original priceOpen (initial entry).
|
|
6665
|
+
* Each subsequent element is an entry added by averageBuy().
|
|
6666
|
+
*
|
|
6667
|
+
* Returns null if no pending signal exists.
|
|
6668
|
+
* Returns a single-element array [{ price: priceOpen, cost }] if no DCA entries were made.
|
|
6669
|
+
*
|
|
6670
|
+
* @param symbol - Trading pair symbol
|
|
6671
|
+
* @returns Promise resolving to array of entry records or null
|
|
6672
|
+
*
|
|
6673
|
+
* @example
|
|
6674
|
+
* // No DCA: [{ price: 43000, cost: 100 }]
|
|
6675
|
+
* // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
|
|
6676
|
+
*/
|
|
6677
|
+
async getPositionEntries(symbol) {
|
|
6678
|
+
this.params.logger.debug("ClientStrategy getPositionEntries", { symbol });
|
|
6679
|
+
if (!this._pendingSignal) {
|
|
6680
|
+
return null;
|
|
6681
|
+
}
|
|
6682
|
+
const entries = this._pendingSignal._entry;
|
|
6683
|
+
if (!entries || entries.length === 0) {
|
|
6684
|
+
return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }];
|
|
6685
|
+
}
|
|
6686
|
+
return entries.map(({ price, cost }) => ({ price, cost }));
|
|
6687
|
+
}
|
|
6640
6688
|
/**
|
|
6641
6689
|
* Performs a single tick of strategy execution.
|
|
6642
6690
|
*
|
|
@@ -7408,10 +7456,12 @@ class ClientStrategy {
|
|
|
7408
7456
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
|
|
7409
7457
|
return false;
|
|
7410
7458
|
const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
|
|
7411
|
-
if (
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7459
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
|
|
7460
|
+
if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
|
|
7461
|
+
return false;
|
|
7462
|
+
if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
|
|
7463
|
+
return false;
|
|
7464
|
+
}
|
|
7415
7465
|
const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
|
|
7416
7466
|
if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
|
|
7417
7467
|
return false;
|
|
@@ -7495,7 +7545,7 @@ class ClientStrategy {
|
|
|
7495
7545
|
throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
7496
7546
|
}
|
|
7497
7547
|
// Validation: currentPrice must be moving toward TP (profit direction)
|
|
7498
|
-
{
|
|
7548
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
|
|
7499
7549
|
const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
|
|
7500
7550
|
if (this._pendingSignal.position === "long") {
|
|
7501
7551
|
// For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
|
|
@@ -7589,10 +7639,12 @@ class ClientStrategy {
|
|
|
7589
7639
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
|
|
7590
7640
|
return false;
|
|
7591
7641
|
const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
|
|
7592
|
-
if (
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7642
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
|
|
7643
|
+
if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
|
|
7644
|
+
return false;
|
|
7645
|
+
if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
|
|
7646
|
+
return false;
|
|
7647
|
+
}
|
|
7596
7648
|
const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
|
|
7597
7649
|
if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
|
|
7598
7650
|
return false;
|
|
@@ -7676,7 +7728,7 @@ class ClientStrategy {
|
|
|
7676
7728
|
throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
7677
7729
|
}
|
|
7678
7730
|
// Validation: currentPrice must be moving toward SL (loss direction)
|
|
7679
|
-
{
|
|
7731
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
|
|
7680
7732
|
const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
|
|
7681
7733
|
if (this._pendingSignal.position === "long") {
|
|
7682
7734
|
// For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
|
|
@@ -9291,6 +9343,15 @@ class StrategyConnectionService {
|
|
|
9291
9343
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9292
9344
|
return await strategy.getPositionPartials(symbol);
|
|
9293
9345
|
};
|
|
9346
|
+
this.getPositionEntries = async (backtest, symbol, context) => {
|
|
9347
|
+
this.loggerService.log("strategyConnectionService getPositionEntries", {
|
|
9348
|
+
symbol,
|
|
9349
|
+
context,
|
|
9350
|
+
backtest,
|
|
9351
|
+
});
|
|
9352
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
9353
|
+
return await strategy.getPositionEntries(symbol);
|
|
9354
|
+
};
|
|
9294
9355
|
/**
|
|
9295
9356
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
9296
9357
|
* If no scheduled signal exists, returns null.
|
|
@@ -10815,7 +10876,6 @@ const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
|
|
|
10815
10876
|
const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
|
|
10816
10877
|
const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
|
|
10817
10878
|
const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
|
|
10818
|
-
const METHOD_NAME_SIGNAL_SYNC = "ActionBase.signalSync";
|
|
10819
10879
|
const METHOD_NAME_DISPOSE = "ActionBase.dispose";
|
|
10820
10880
|
const DEFAULT_SOURCE = "default";
|
|
10821
10881
|
/**
|
|
@@ -11227,6 +11287,11 @@ class ActionProxy {
|
|
|
11227
11287
|
*/
|
|
11228
11288
|
async signalSync(event) {
|
|
11229
11289
|
if (this._target.signalSync) {
|
|
11290
|
+
console.error("Action::signalSync is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
|
|
11291
|
+
console.error("If you need to implement custom logic on signal open/close, please use signal(), signalBacktest(), signalLive()");
|
|
11292
|
+
console.error("If Action::signalSync throws the exchange will not execute the order!");
|
|
11293
|
+
console.error("");
|
|
11294
|
+
console.error("You have been warned!");
|
|
11230
11295
|
await this._target.signalSync(event);
|
|
11231
11296
|
}
|
|
11232
11297
|
}
|
|
@@ -11689,19 +11754,6 @@ class ActionBase {
|
|
|
11689
11754
|
source,
|
|
11690
11755
|
});
|
|
11691
11756
|
}
|
|
11692
|
-
/**
|
|
11693
|
-
* Gate for position open/close via limit order. Default allows all.
|
|
11694
|
-
* Throw to reject — framework retries next tick.
|
|
11695
|
-
*
|
|
11696
|
-
* NOTE: Exceptions are NOT swallowed — they propagate to CREATE_SYNC_FN.
|
|
11697
|
-
*
|
|
11698
|
-
* @param event - Sync event with action "signal-open" or "signal-close"
|
|
11699
|
-
*/
|
|
11700
|
-
signalSync(_event, source = DEFAULT_SOURCE) {
|
|
11701
|
-
bt.loggerService.info(METHOD_NAME_SIGNAL_SYNC, {
|
|
11702
|
-
source,
|
|
11703
|
-
});
|
|
11704
|
-
}
|
|
11705
11757
|
/**
|
|
11706
11758
|
* Cleans up resources and subscriptions when action handler is disposed.
|
|
11707
11759
|
*
|
|
@@ -12980,6 +13032,14 @@ class StrategyCoreService {
|
|
|
12980
13032
|
await this.validate(context);
|
|
12981
13033
|
return await this.strategyConnectionService.getPositionPartials(backtest, symbol, context);
|
|
12982
13034
|
};
|
|
13035
|
+
this.getPositionEntries = async (backtest, symbol, context) => {
|
|
13036
|
+
this.loggerService.log("strategyCoreService getPositionEntries", {
|
|
13037
|
+
symbol,
|
|
13038
|
+
context,
|
|
13039
|
+
});
|
|
13040
|
+
await this.validate(context);
|
|
13041
|
+
return await this.strategyConnectionService.getPositionEntries(backtest, symbol, context);
|
|
13042
|
+
};
|
|
12983
13043
|
/**
|
|
12984
13044
|
* Retrieves the currently active scheduled signal for the symbol.
|
|
12985
13045
|
* If no scheduled signal exists, returns null.
|
|
@@ -31724,6 +31784,11 @@ BrokerBase = functoolsKit.makeExtendable(BrokerBase);
|
|
|
31724
31784
|
*/
|
|
31725
31785
|
const Broker = new BrokerAdapter();
|
|
31726
31786
|
|
|
31787
|
+
const POSITION_OVERLAP_LADDER_DEFAULT = {
|
|
31788
|
+
upperPercent: 1.5,
|
|
31789
|
+
lowerPercent: 1.5,
|
|
31790
|
+
};
|
|
31791
|
+
|
|
31727
31792
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
31728
31793
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
31729
31794
|
const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
|
|
@@ -31749,6 +31814,8 @@ const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
|
|
|
31749
31814
|
const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
|
|
31750
31815
|
const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
|
|
31751
31816
|
const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
|
|
31817
|
+
const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
|
|
31818
|
+
const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
|
|
31752
31819
|
/**
|
|
31753
31820
|
* Cancels the scheduled signal without stopping the strategy.
|
|
31754
31821
|
*
|
|
@@ -32025,6 +32092,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
|
|
|
32025
32092
|
percentShift,
|
|
32026
32093
|
currentPrice,
|
|
32027
32094
|
newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
|
|
32095
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
32028
32096
|
position: signal.position,
|
|
32029
32097
|
context: { exchangeName, frameName, strategyName },
|
|
32030
32098
|
backtest: isBacktest,
|
|
@@ -32104,6 +32172,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
|
|
|
32104
32172
|
percentShift,
|
|
32105
32173
|
currentPrice,
|
|
32106
32174
|
newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
|
|
32175
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
32107
32176
|
position: signal.position,
|
|
32108
32177
|
context: { exchangeName, frameName, strategyName },
|
|
32109
32178
|
backtest: isBacktest,
|
|
@@ -32155,6 +32224,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
|
|
|
32155
32224
|
currentPrice,
|
|
32156
32225
|
newStopLossPrice,
|
|
32157
32226
|
position: signal.position,
|
|
32227
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
32158
32228
|
context: { exchangeName, frameName, strategyName },
|
|
32159
32229
|
backtest: isBacktest,
|
|
32160
32230
|
});
|
|
@@ -32204,6 +32274,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
|
|
|
32204
32274
|
percentShift,
|
|
32205
32275
|
currentPrice,
|
|
32206
32276
|
newTakeProfitPrice,
|
|
32277
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
32207
32278
|
position: signal.position,
|
|
32208
32279
|
context: { exchangeName, frameName, strategyName },
|
|
32209
32280
|
backtest: isBacktest,
|
|
@@ -32530,6 +32601,31 @@ async function getBreakeven(symbol, currentPrice) {
|
|
|
32530
32601
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
32531
32602
|
return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
32532
32603
|
}
|
|
32604
|
+
/**
|
|
32605
|
+
* Returns the effective (DCA-weighted) entry price for the current pending signal.
|
|
32606
|
+
*
|
|
32607
|
+
* Uses cost-weighted harmonic mean: Σcost / Σ(cost/price).
|
|
32608
|
+
* When partial closes exist, the price is computed iteratively using
|
|
32609
|
+
* costBasisAtClose snapshots from each partial, then blended with any
|
|
32610
|
+
* DCA entries added after the last partial.
|
|
32611
|
+
* With no DCA entries, equals the original priceOpen.
|
|
32612
|
+
*
|
|
32613
|
+
* Returns null if no pending signal exists.
|
|
32614
|
+
*
|
|
32615
|
+
* Automatically detects backtest/live mode from execution context.
|
|
32616
|
+
*
|
|
32617
|
+
* @param symbol - Trading pair symbol
|
|
32618
|
+
* @returns Promise resolving to effective entry price or null
|
|
32619
|
+
*
|
|
32620
|
+
* @example
|
|
32621
|
+
* ```typescript
|
|
32622
|
+
* import { getPositionAveragePrice } from "backtest-kit";
|
|
32623
|
+
*
|
|
32624
|
+
* const avgPrice = await getPositionAveragePrice("BTCUSDT");
|
|
32625
|
+
* // No DCA: avgPrice === priceOpen
|
|
32626
|
+
* // After DCA at lower price: avgPrice < priceOpen
|
|
32627
|
+
* ```
|
|
32628
|
+
*/
|
|
32533
32629
|
async function getPositionAveragePrice(symbol) {
|
|
32534
32630
|
bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
|
|
32535
32631
|
symbol,
|
|
@@ -32544,6 +32640,28 @@ async function getPositionAveragePrice(symbol) {
|
|
|
32544
32640
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
32545
32641
|
return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32546
32642
|
}
|
|
32643
|
+
/**
|
|
32644
|
+
* Returns the number of DCA entries made for the current pending signal.
|
|
32645
|
+
*
|
|
32646
|
+
* 1 = original entry only (no DCA).
|
|
32647
|
+
* Increases by 1 with each successful commitAverageBuy().
|
|
32648
|
+
*
|
|
32649
|
+
* Returns null if no pending signal exists.
|
|
32650
|
+
*
|
|
32651
|
+
* Automatically detects backtest/live mode from execution context.
|
|
32652
|
+
*
|
|
32653
|
+
* @param symbol - Trading pair symbol
|
|
32654
|
+
* @returns Promise resolving to entry count or null
|
|
32655
|
+
*
|
|
32656
|
+
* @example
|
|
32657
|
+
* ```typescript
|
|
32658
|
+
* import { getPositionInvestedCount } from "backtest-kit";
|
|
32659
|
+
*
|
|
32660
|
+
* const count = await getPositionInvestedCount("BTCUSDT");
|
|
32661
|
+
* // No DCA: count === 1
|
|
32662
|
+
* // After one DCA: count === 2
|
|
32663
|
+
* ```
|
|
32664
|
+
*/
|
|
32547
32665
|
async function getPositionInvestedCount(symbol) {
|
|
32548
32666
|
bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
|
|
32549
32667
|
symbol,
|
|
@@ -32558,6 +32676,28 @@ async function getPositionInvestedCount(symbol) {
|
|
|
32558
32676
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
32559
32677
|
return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32560
32678
|
}
|
|
32679
|
+
/**
|
|
32680
|
+
* Returns the total invested cost basis in dollars for the current pending signal.
|
|
32681
|
+
*
|
|
32682
|
+
* Equal to the sum of all _entry costs (Σ entry.cost).
|
|
32683
|
+
* Each entry cost is set at the time of commitAverageBuy (defaults to CC_POSITION_ENTRY_COST).
|
|
32684
|
+
*
|
|
32685
|
+
* Returns null if no pending signal exists.
|
|
32686
|
+
*
|
|
32687
|
+
* Automatically detects backtest/live mode from execution context.
|
|
32688
|
+
*
|
|
32689
|
+
* @param symbol - Trading pair symbol
|
|
32690
|
+
* @returns Promise resolving to total invested cost in dollars or null
|
|
32691
|
+
*
|
|
32692
|
+
* @example
|
|
32693
|
+
* ```typescript
|
|
32694
|
+
* import { getPositionInvestedCost } from "backtest-kit";
|
|
32695
|
+
*
|
|
32696
|
+
* const cost = await getPositionInvestedCost("BTCUSDT");
|
|
32697
|
+
* // No DCA, default cost: cost === 100
|
|
32698
|
+
* // After one DCA with default cost: cost === 200
|
|
32699
|
+
* ```
|
|
32700
|
+
*/
|
|
32561
32701
|
async function getPositionInvestedCost(symbol) {
|
|
32562
32702
|
bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
|
|
32563
32703
|
symbol,
|
|
@@ -32572,6 +32712,29 @@ async function getPositionInvestedCost(symbol) {
|
|
|
32572
32712
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
32573
32713
|
return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32574
32714
|
}
|
|
32715
|
+
/**
|
|
32716
|
+
* Returns the unrealized PNL percentage for the current pending signal at current market price.
|
|
32717
|
+
*
|
|
32718
|
+
* Accounts for partial closes, DCA entries, slippage and fees
|
|
32719
|
+
* (delegates to toProfitLossDto).
|
|
32720
|
+
*
|
|
32721
|
+
* Returns null if no pending signal exists.
|
|
32722
|
+
*
|
|
32723
|
+
* Automatically detects backtest/live mode from execution context.
|
|
32724
|
+
* Automatically fetches current price via getAveragePrice.
|
|
32725
|
+
*
|
|
32726
|
+
* @param symbol - Trading pair symbol
|
|
32727
|
+
* @returns Promise resolving to PNL percentage or null
|
|
32728
|
+
*
|
|
32729
|
+
* @example
|
|
32730
|
+
* ```typescript
|
|
32731
|
+
* import { getPositionPnlPercent } from "backtest-kit";
|
|
32732
|
+
*
|
|
32733
|
+
* const pnlPct = await getPositionPnlPercent("BTCUSDT");
|
|
32734
|
+
* // LONG at 100, current=105: pnlPct ≈ 5
|
|
32735
|
+
* // LONG at 100, current=95: pnlPct ≈ -5
|
|
32736
|
+
* ```
|
|
32737
|
+
*/
|
|
32575
32738
|
async function getPositionPnlPercent(symbol) {
|
|
32576
32739
|
bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
|
|
32577
32740
|
if (!ExecutionContextService.hasContext()) {
|
|
@@ -32721,6 +32884,29 @@ async function commitPartialLossCost(symbol, dollarAmount) {
|
|
|
32721
32884
|
});
|
|
32722
32885
|
return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
32723
32886
|
}
|
|
32887
|
+
/**
|
|
32888
|
+
* Returns the unrealized PNL in dollars for the current pending signal at current market price.
|
|
32889
|
+
*
|
|
32890
|
+
* Calculated as: pnlPercentage / 100 × totalInvestedCost.
|
|
32891
|
+
* Accounts for partial closes, DCA entries, slippage and fees.
|
|
32892
|
+
*
|
|
32893
|
+
* Returns null if no pending signal exists.
|
|
32894
|
+
*
|
|
32895
|
+
* Automatically detects backtest/live mode from execution context.
|
|
32896
|
+
* Automatically fetches current price via getAveragePrice.
|
|
32897
|
+
*
|
|
32898
|
+
* @param symbol - Trading pair symbol
|
|
32899
|
+
* @returns Promise resolving to PNL in dollars or null
|
|
32900
|
+
*
|
|
32901
|
+
* @example
|
|
32902
|
+
* ```typescript
|
|
32903
|
+
* import { getPositionPnlCost } from "backtest-kit";
|
|
32904
|
+
*
|
|
32905
|
+
* const pnlCost = await getPositionPnlCost("BTCUSDT");
|
|
32906
|
+
* // LONG at 100, invested $100, current=105: pnlCost ≈ 5
|
|
32907
|
+
* // LONG at 100, invested $200 (DCA), current=95: pnlCost ≈ -10
|
|
32908
|
+
* ```
|
|
32909
|
+
*/
|
|
32724
32910
|
async function getPositionPnlCost(symbol) {
|
|
32725
32911
|
bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
|
|
32726
32912
|
if (!ExecutionContextService.hasContext()) {
|
|
@@ -32807,6 +32993,104 @@ async function getPositionPartials(symbol) {
|
|
|
32807
32993
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
32808
32994
|
return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
32809
32995
|
}
|
|
32996
|
+
/**
|
|
32997
|
+
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
32998
|
+
* Use this to prevent duplicate DCA entries at the same price area.
|
|
32999
|
+
*
|
|
33000
|
+
* Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
|
|
33001
|
+
* where step = level * percent / 100.
|
|
33002
|
+
* Returns false if no pending signal exists.
|
|
33003
|
+
*
|
|
33004
|
+
* @param symbol - Trading pair symbol
|
|
33005
|
+
* @param currentPrice - Price to check against existing DCA levels
|
|
33006
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
33007
|
+
* @returns Promise<boolean> - true if price overlaps an existing entry level (DCA not recommended)
|
|
33008
|
+
*
|
|
33009
|
+
* @example
|
|
33010
|
+
* ```typescript
|
|
33011
|
+
* import { getPositionEntryOverlap } from "backtest-kit";
|
|
33012
|
+
*
|
|
33013
|
+
* // LONG with levels [43000, 42000], check if 42100 is too close to 42000
|
|
33014
|
+
* const overlap = await getPositionEntryOverlap("BTCUSDT", 42100, { upperPercent: 5, lowerPercent: 5 });
|
|
33015
|
+
* // overlap = true (42100 is within 5% of 42000 = [39900, 44100])
|
|
33016
|
+
* if (!overlap) {
|
|
33017
|
+
* await commitAverageBuy("BTCUSDT");
|
|
33018
|
+
* }
|
|
33019
|
+
* ```
|
|
33020
|
+
*/
|
|
33021
|
+
async function getPositionEntryOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
|
|
33022
|
+
bt.loggerService.info(GET_POSITION_ENTRY_OVERLAP_METHOD_NAME, {
|
|
33023
|
+
symbol,
|
|
33024
|
+
currentPrice,
|
|
33025
|
+
ladder,
|
|
33026
|
+
});
|
|
33027
|
+
if (!ExecutionContextService.hasContext()) {
|
|
33028
|
+
throw new Error("getPositionEntryOverlap requires an execution context");
|
|
33029
|
+
}
|
|
33030
|
+
if (!MethodContextService.hasContext()) {
|
|
33031
|
+
throw new Error("getPositionEntryOverlap requires a method context");
|
|
33032
|
+
}
|
|
33033
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
33034
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
33035
|
+
const levels = await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
33036
|
+
if (!levels) {
|
|
33037
|
+
return false;
|
|
33038
|
+
}
|
|
33039
|
+
return levels.some((level) => {
|
|
33040
|
+
const upperStep = (level * ladder.upperPercent) / 100;
|
|
33041
|
+
const lowerStep = (level * ladder.lowerPercent) / 100;
|
|
33042
|
+
return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
|
|
33043
|
+
});
|
|
33044
|
+
}
|
|
33045
|
+
/**
|
|
33046
|
+
* Checks whether the current price falls within the tolerance zone of any existing partial close price.
|
|
33047
|
+
* Use this to prevent duplicate partial closes at the same price area.
|
|
33048
|
+
*
|
|
33049
|
+
* Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
|
|
33050
|
+
* for any partial, where step = partial.currentPrice * percent / 100.
|
|
33051
|
+
* Returns false if no pending signal exists or no partials have been executed yet.
|
|
33052
|
+
*
|
|
33053
|
+
* @param symbol - Trading pair symbol
|
|
33054
|
+
* @param currentPrice - Price to check against existing partial close prices
|
|
33055
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
33056
|
+
* @returns Promise<boolean> - true if price overlaps an existing partial price (partial not recommended)
|
|
33057
|
+
*
|
|
33058
|
+
* @example
|
|
33059
|
+
* ```typescript
|
|
33060
|
+
* import { getPositionPartialOverlap } from "backtest-kit";
|
|
33061
|
+
*
|
|
33062
|
+
* // Partials at [45000], check if 45100 is too close
|
|
33063
|
+
* const overlap = await getPositionPartialOverlap("BTCUSDT", 45100, { upperPercent: 1.5, lowerPercent: 1.5 });
|
|
33064
|
+
* // overlap = true (45100 is within 1.5% of 45000)
|
|
33065
|
+
* if (!overlap) {
|
|
33066
|
+
* await commitPartialProfit("BTCUSDT", 50);
|
|
33067
|
+
* }
|
|
33068
|
+
* ```
|
|
33069
|
+
*/
|
|
33070
|
+
async function getPositionPartialOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
|
|
33071
|
+
bt.loggerService.info(GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME, {
|
|
33072
|
+
symbol,
|
|
33073
|
+
currentPrice,
|
|
33074
|
+
ladder,
|
|
33075
|
+
});
|
|
33076
|
+
if (!ExecutionContextService.hasContext()) {
|
|
33077
|
+
throw new Error("getPositionPartialOverlap requires an execution context");
|
|
33078
|
+
}
|
|
33079
|
+
if (!MethodContextService.hasContext()) {
|
|
33080
|
+
throw new Error("getPositionPartialOverlap requires a method context");
|
|
33081
|
+
}
|
|
33082
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
33083
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
33084
|
+
const partials = await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
33085
|
+
if (!partials) {
|
|
33086
|
+
return false;
|
|
33087
|
+
}
|
|
33088
|
+
return partials.some((partial) => {
|
|
33089
|
+
const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
|
|
33090
|
+
const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
|
|
33091
|
+
return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
|
|
33092
|
+
});
|
|
33093
|
+
}
|
|
32810
33094
|
|
|
32811
33095
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
32812
33096
|
/**
|
|
@@ -34032,6 +34316,9 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT = "BacktestUtils.getPosition
|
|
|
34032
34316
|
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnlCost";
|
|
34033
34317
|
const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
|
|
34034
34318
|
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
|
|
34319
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES = "BacktestUtils.getPositionEntries";
|
|
34320
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
|
|
34321
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
|
|
34035
34322
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
34036
34323
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
34037
34324
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
@@ -34787,6 +35074,125 @@ class BacktestUtils {
|
|
|
34787
35074
|
}
|
|
34788
35075
|
return await bt.strategyCoreService.getPositionPartials(true, symbol, context);
|
|
34789
35076
|
};
|
|
35077
|
+
/**
|
|
35078
|
+
* Returns the list of DCA entry prices and costs for the current pending signal.
|
|
35079
|
+
*
|
|
35080
|
+
* Each element represents a single position entry — the initial open or a subsequent
|
|
35081
|
+
* DCA entry added via commitAverageBuy.
|
|
35082
|
+
*
|
|
35083
|
+
* Returns null if no pending signal exists.
|
|
35084
|
+
* Returns a single-element array if no DCA entries were made.
|
|
35085
|
+
*
|
|
35086
|
+
* Each entry contains:
|
|
35087
|
+
* - `price` — execution price of this entry
|
|
35088
|
+
* - `cost` — dollar cost allocated to this entry (e.g. 100 for $100)
|
|
35089
|
+
*
|
|
35090
|
+
* @param symbol - Trading pair symbol
|
|
35091
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
35092
|
+
* @returns Array of entry records, or null if no active position
|
|
35093
|
+
*/
|
|
35094
|
+
this.getPositionEntries = async (symbol, context) => {
|
|
35095
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES, {
|
|
35096
|
+
symbol,
|
|
35097
|
+
context,
|
|
35098
|
+
});
|
|
35099
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
35100
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
35101
|
+
{
|
|
35102
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
35103
|
+
riskName &&
|
|
35104
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
35105
|
+
riskList &&
|
|
35106
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES));
|
|
35107
|
+
actions &&
|
|
35108
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES));
|
|
35109
|
+
}
|
|
35110
|
+
return await bt.strategyCoreService.getPositionEntries(true, symbol, context);
|
|
35111
|
+
};
|
|
35112
|
+
/**
|
|
35113
|
+
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
35114
|
+
* Use this to prevent duplicate DCA entries at the same price area.
|
|
35115
|
+
*
|
|
35116
|
+
* Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
|
|
35117
|
+
* where step = level * percent / 100.
|
|
35118
|
+
* Returns false if no pending signal exists.
|
|
35119
|
+
*
|
|
35120
|
+
* @param symbol - Trading pair symbol
|
|
35121
|
+
* @param currentPrice - Price to check against existing DCA levels
|
|
35122
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
35123
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
35124
|
+
* @returns true if price overlaps an existing entry level (DCA not recommended)
|
|
35125
|
+
*/
|
|
35126
|
+
this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
|
|
35127
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
|
|
35128
|
+
symbol,
|
|
35129
|
+
currentPrice,
|
|
35130
|
+
context,
|
|
35131
|
+
ladder,
|
|
35132
|
+
});
|
|
35133
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
35134
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
35135
|
+
{
|
|
35136
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
35137
|
+
riskName &&
|
|
35138
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
35139
|
+
riskList &&
|
|
35140
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
|
|
35141
|
+
actions &&
|
|
35142
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
|
|
35143
|
+
}
|
|
35144
|
+
const levels = await bt.strategyCoreService.getPositionLevels(true, symbol, context);
|
|
35145
|
+
if (!levels) {
|
|
35146
|
+
return false;
|
|
35147
|
+
}
|
|
35148
|
+
return levels.some((level) => {
|
|
35149
|
+
const upperStep = (level * ladder.upperPercent) / 100;
|
|
35150
|
+
const lowerStep = (level * ladder.lowerPercent) / 100;
|
|
35151
|
+
return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
|
|
35152
|
+
});
|
|
35153
|
+
};
|
|
35154
|
+
/**
|
|
35155
|
+
* Checks whether the current price falls within the tolerance zone of any existing partial close price.
|
|
35156
|
+
* Use this to prevent duplicate partial closes at the same price area.
|
|
35157
|
+
*
|
|
35158
|
+
* Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
|
|
35159
|
+
* for any partial, where step = partial.currentPrice * percent / 100.
|
|
35160
|
+
* Returns false if no pending signal exists or no partials have been executed yet.
|
|
35161
|
+
*
|
|
35162
|
+
* @param symbol - Trading pair symbol
|
|
35163
|
+
* @param currentPrice - Price to check against existing partial close prices
|
|
35164
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
35165
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
35166
|
+
* @returns true if price overlaps an existing partial price (partial not recommended)
|
|
35167
|
+
*/
|
|
35168
|
+
this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
|
|
35169
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
|
|
35170
|
+
symbol,
|
|
35171
|
+
currentPrice,
|
|
35172
|
+
context,
|
|
35173
|
+
ladder,
|
|
35174
|
+
});
|
|
35175
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
35176
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
35177
|
+
{
|
|
35178
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
35179
|
+
riskName &&
|
|
35180
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
35181
|
+
riskList &&
|
|
35182
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
|
|
35183
|
+
actions &&
|
|
35184
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
|
|
35185
|
+
}
|
|
35186
|
+
const partials = await bt.strategyCoreService.getPositionPartials(true, symbol, context);
|
|
35187
|
+
if (!partials) {
|
|
35188
|
+
return false;
|
|
35189
|
+
}
|
|
35190
|
+
return partials.some((partial) => {
|
|
35191
|
+
const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
|
|
35192
|
+
const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
|
|
35193
|
+
return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
|
|
35194
|
+
});
|
|
35195
|
+
};
|
|
34790
35196
|
/**
|
|
34791
35197
|
* Stops the strategy from generating new signals.
|
|
34792
35198
|
*
|
|
@@ -35276,6 +35682,7 @@ class BacktestUtils {
|
|
|
35276
35682
|
percentShift,
|
|
35277
35683
|
currentPrice,
|
|
35278
35684
|
newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
|
|
35685
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
35279
35686
|
position: signal.position,
|
|
35280
35687
|
context,
|
|
35281
35688
|
backtest: true,
|
|
@@ -35360,6 +35767,7 @@ class BacktestUtils {
|
|
|
35360
35767
|
percentShift,
|
|
35361
35768
|
currentPrice,
|
|
35362
35769
|
newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
|
|
35770
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
35363
35771
|
position: signal.position,
|
|
35364
35772
|
context,
|
|
35365
35773
|
backtest: true,
|
|
@@ -35414,6 +35822,7 @@ class BacktestUtils {
|
|
|
35414
35822
|
currentPrice,
|
|
35415
35823
|
newStopLossPrice,
|
|
35416
35824
|
position: signal.position,
|
|
35825
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
35417
35826
|
context,
|
|
35418
35827
|
backtest: true,
|
|
35419
35828
|
});
|
|
@@ -35467,6 +35876,7 @@ class BacktestUtils {
|
|
|
35467
35876
|
currentPrice,
|
|
35468
35877
|
newTakeProfitPrice,
|
|
35469
35878
|
position: signal.position,
|
|
35879
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
35470
35880
|
context,
|
|
35471
35881
|
backtest: true,
|
|
35472
35882
|
});
|
|
@@ -35809,6 +36219,9 @@ const LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT = "LiveUtils.getPositionPnlPerce
|
|
|
35809
36219
|
const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
|
|
35810
36220
|
const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
|
|
35811
36221
|
const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
|
|
36222
|
+
const LIVE_METHOD_NAME_GET_POSITION_ENTRIES = "LiveUtils.getPositionEntries";
|
|
36223
|
+
const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
|
|
36224
|
+
const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
|
|
35812
36225
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
35813
36226
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
35814
36227
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
@@ -36623,6 +37036,137 @@ class LiveUtils {
|
|
|
36623
37036
|
frameName: "",
|
|
36624
37037
|
});
|
|
36625
37038
|
};
|
|
37039
|
+
/**
|
|
37040
|
+
* Returns the list of DCA entry prices and costs for the current pending signal.
|
|
37041
|
+
*
|
|
37042
|
+
* Each element represents a single position entry — the initial open or a subsequent
|
|
37043
|
+
* DCA entry added via commitAverageBuy.
|
|
37044
|
+
*
|
|
37045
|
+
* Returns null if no pending signal exists.
|
|
37046
|
+
* Returns a single-element array if no DCA entries were made.
|
|
37047
|
+
*
|
|
37048
|
+
* Each entry contains:
|
|
37049
|
+
* - `price` — execution price of this entry
|
|
37050
|
+
* - `cost` — dollar cost allocated to this entry (e.g. 100 for $100)
|
|
37051
|
+
*
|
|
37052
|
+
* @param symbol - Trading pair symbol
|
|
37053
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
37054
|
+
* @returns Array of entry records, or null if no active position
|
|
37055
|
+
*/
|
|
37056
|
+
this.getPositionEntries = async (symbol, context) => {
|
|
37057
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ENTRIES, {
|
|
37058
|
+
symbol,
|
|
37059
|
+
context,
|
|
37060
|
+
});
|
|
37061
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
37062
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
37063
|
+
{
|
|
37064
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
37065
|
+
riskName &&
|
|
37066
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRIES);
|
|
37067
|
+
riskList &&
|
|
37068
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRIES));
|
|
37069
|
+
actions &&
|
|
37070
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ENTRIES));
|
|
37071
|
+
}
|
|
37072
|
+
return await bt.strategyCoreService.getPositionEntries(false, symbol, {
|
|
37073
|
+
strategyName: context.strategyName,
|
|
37074
|
+
exchangeName: context.exchangeName,
|
|
37075
|
+
frameName: "",
|
|
37076
|
+
});
|
|
37077
|
+
};
|
|
37078
|
+
/**
|
|
37079
|
+
* Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
|
|
37080
|
+
* Use this to prevent duplicate DCA entries at the same price area.
|
|
37081
|
+
*
|
|
37082
|
+
* Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
|
|
37083
|
+
* where step = level * percent / 100.
|
|
37084
|
+
* Returns false if no pending signal exists.
|
|
37085
|
+
*
|
|
37086
|
+
* @param symbol - Trading pair symbol
|
|
37087
|
+
* @param currentPrice - Price to check against existing DCA levels
|
|
37088
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
37089
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
37090
|
+
* @returns true if price overlaps an existing entry level (DCA not recommended)
|
|
37091
|
+
*/
|
|
37092
|
+
this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
|
|
37093
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
|
|
37094
|
+
symbol,
|
|
37095
|
+
currentPrice,
|
|
37096
|
+
context,
|
|
37097
|
+
ladder,
|
|
37098
|
+
});
|
|
37099
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
37100
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
37101
|
+
{
|
|
37102
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
37103
|
+
riskName &&
|
|
37104
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
|
|
37105
|
+
riskList &&
|
|
37106
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
|
|
37107
|
+
actions &&
|
|
37108
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
|
|
37109
|
+
}
|
|
37110
|
+
const levels = await bt.strategyCoreService.getPositionLevels(false, symbol, {
|
|
37111
|
+
strategyName: context.strategyName,
|
|
37112
|
+
exchangeName: context.exchangeName,
|
|
37113
|
+
frameName: "",
|
|
37114
|
+
});
|
|
37115
|
+
if (!levels) {
|
|
37116
|
+
return false;
|
|
37117
|
+
}
|
|
37118
|
+
return levels.some((level) => {
|
|
37119
|
+
const upperStep = (level * ladder.upperPercent) / 100;
|
|
37120
|
+
const lowerStep = (level * ladder.lowerPercent) / 100;
|
|
37121
|
+
return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
|
|
37122
|
+
});
|
|
37123
|
+
};
|
|
37124
|
+
/**
|
|
37125
|
+
* Checks whether the current price falls within the tolerance zone of any existing partial close price.
|
|
37126
|
+
* Use this to prevent duplicate partial closes at the same price area.
|
|
37127
|
+
*
|
|
37128
|
+
* Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
|
|
37129
|
+
* for any partial, where step = partial.currentPrice * percent / 100.
|
|
37130
|
+
* Returns false if no pending signal exists or no partials have been executed yet.
|
|
37131
|
+
*
|
|
37132
|
+
* @param symbol - Trading pair symbol
|
|
37133
|
+
* @param currentPrice - Price to check against existing partial close prices
|
|
37134
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
37135
|
+
* @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
|
|
37136
|
+
* @returns true if price overlaps an existing partial price (partial not recommended)
|
|
37137
|
+
*/
|
|
37138
|
+
this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
|
|
37139
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
|
|
37140
|
+
symbol,
|
|
37141
|
+
currentPrice,
|
|
37142
|
+
context,
|
|
37143
|
+
ladder,
|
|
37144
|
+
});
|
|
37145
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
37146
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
37147
|
+
{
|
|
37148
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
37149
|
+
riskName &&
|
|
37150
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
|
|
37151
|
+
riskList &&
|
|
37152
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
|
|
37153
|
+
actions &&
|
|
37154
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
|
|
37155
|
+
}
|
|
37156
|
+
const partials = await bt.strategyCoreService.getPositionPartials(false, symbol, {
|
|
37157
|
+
strategyName: context.strategyName,
|
|
37158
|
+
exchangeName: context.exchangeName,
|
|
37159
|
+
frameName: "",
|
|
37160
|
+
});
|
|
37161
|
+
if (!partials) {
|
|
37162
|
+
return false;
|
|
37163
|
+
}
|
|
37164
|
+
return partials.some((partial) => {
|
|
37165
|
+
const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
|
|
37166
|
+
const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
|
|
37167
|
+
return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
|
|
37168
|
+
});
|
|
37169
|
+
};
|
|
36626
37170
|
/**
|
|
36627
37171
|
* Stops the strategy from generating new signals.
|
|
36628
37172
|
*
|
|
@@ -37205,6 +37749,7 @@ class LiveUtils {
|
|
|
37205
37749
|
percentShift,
|
|
37206
37750
|
currentPrice,
|
|
37207
37751
|
newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
|
|
37752
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
37208
37753
|
position: signal.position,
|
|
37209
37754
|
context,
|
|
37210
37755
|
backtest: false,
|
|
@@ -37304,6 +37849,7 @@ class LiveUtils {
|
|
|
37304
37849
|
percentShift,
|
|
37305
37850
|
currentPrice,
|
|
37306
37851
|
newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
|
|
37852
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
37307
37853
|
position: signal.position,
|
|
37308
37854
|
context,
|
|
37309
37855
|
backtest: false,
|
|
@@ -37373,6 +37919,7 @@ class LiveUtils {
|
|
|
37373
37919
|
percentShift,
|
|
37374
37920
|
currentPrice,
|
|
37375
37921
|
newStopLossPrice,
|
|
37922
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
37376
37923
|
position: signal.position,
|
|
37377
37924
|
context,
|
|
37378
37925
|
backtest: false,
|
|
@@ -37442,6 +37989,7 @@ class LiveUtils {
|
|
|
37442
37989
|
percentShift,
|
|
37443
37990
|
currentPrice,
|
|
37444
37991
|
newTakeProfitPrice,
|
|
37992
|
+
takeProfitPrice: signal.priceTakeProfit,
|
|
37445
37993
|
position: signal.position,
|
|
37446
37994
|
context,
|
|
37447
37995
|
backtest: false,
|
|
@@ -45442,9 +45990,11 @@ exports.getNextCandles = getNextCandles;
|
|
|
45442
45990
|
exports.getOrderBook = getOrderBook;
|
|
45443
45991
|
exports.getPendingSignal = getPendingSignal;
|
|
45444
45992
|
exports.getPositionAveragePrice = getPositionAveragePrice;
|
|
45993
|
+
exports.getPositionEntryOverlap = getPositionEntryOverlap;
|
|
45445
45994
|
exports.getPositionInvestedCost = getPositionInvestedCost;
|
|
45446
45995
|
exports.getPositionInvestedCount = getPositionInvestedCount;
|
|
45447
45996
|
exports.getPositionLevels = getPositionLevels;
|
|
45997
|
+
exports.getPositionPartialOverlap = getPositionPartialOverlap;
|
|
45448
45998
|
exports.getPositionPartials = getPositionPartials;
|
|
45449
45999
|
exports.getPositionPnlCost = getPositionPnlCost;
|
|
45450
46000
|
exports.getPositionPnlPercent = getPositionPnlPercent;
|