backtest-kit 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -61
- package/build/index.cjs +1654 -147
- package/build/index.mjs +1635 -148
- package/package.json +1 -1
- package/types.d.ts +810 -2
package/build/index.mjs
CHANGED
|
@@ -443,6 +443,14 @@ const GLOBAL_CONFIG = {
|
|
|
443
443
|
* Default: true (mutex locking enabled for candle fetching)
|
|
444
444
|
*/
|
|
445
445
|
CC_ENABLE_CANDLE_FETCH_MUTEX: true,
|
|
446
|
+
/**
|
|
447
|
+
* Enables DCA (Dollar-Cost Averaging) logic even if antirecord is not broken.
|
|
448
|
+
* Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
|
|
449
|
+
* 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.
|
|
450
|
+
*
|
|
451
|
+
* Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
|
|
452
|
+
*/
|
|
453
|
+
CC_ENABLE_DCA_EVERYWHERE: false,
|
|
446
454
|
};
|
|
447
455
|
const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
|
|
448
456
|
|
|
@@ -3174,75 +3182,90 @@ class ExchangeConnectionService {
|
|
|
3174
3182
|
}
|
|
3175
3183
|
}
|
|
3176
3184
|
|
|
3185
|
+
const COST_BASIS_PER_ENTRY$3 = 100;
|
|
3177
3186
|
/**
|
|
3178
3187
|
* Returns the effective entry price for price calculations.
|
|
3179
3188
|
*
|
|
3180
|
-
*
|
|
3181
|
-
* the simple arithmetic mean of all entry prices (DCA average).
|
|
3182
|
-
* Otherwise returns the original signal.priceOpen.
|
|
3189
|
+
* Uses harmonic mean (correct for fixed-dollar DCA: $100 per entry).
|
|
3183
3190
|
*
|
|
3184
|
-
*
|
|
3185
|
-
*
|
|
3186
|
-
* effective averaged price returned by this function.
|
|
3191
|
+
* When partial closes exist, replays the partial sequence to reconstruct
|
|
3192
|
+
* the running cost basis at each partial — no extra stored fields needed.
|
|
3187
3193
|
*
|
|
3188
|
-
*
|
|
3189
|
-
*
|
|
3194
|
+
* Cost basis replay:
|
|
3195
|
+
* costBasis starts at 0
|
|
3196
|
+
* for each partial[i]:
|
|
3197
|
+
* newEntries = entryCountAtClose[i] - entryCountAtClose[i-1] (or entryCountAtClose[0] for i=0)
|
|
3198
|
+
* costBasis += newEntries × $100 ← add DCA entries up to this partial
|
|
3199
|
+
* positionCostBasisAtClose[i] = costBasis ← snapshot BEFORE close
|
|
3200
|
+
* costBasis × = (1 - percent[i] / 100) ← reduce after close
|
|
3201
|
+
*
|
|
3202
|
+
* @param signal - Signal row
|
|
3203
|
+
* @returns Effective entry price for PNL calculations
|
|
3190
3204
|
*/
|
|
3191
3205
|
const getEffectivePriceOpen = (signal) => {
|
|
3192
|
-
if (signal._entry
|
|
3193
|
-
return signal.
|
|
3194
|
-
|
|
3195
|
-
|
|
3206
|
+
if (!signal._entry || signal._entry.length === 0)
|
|
3207
|
+
return signal.priceOpen;
|
|
3208
|
+
const entries = signal._entry;
|
|
3209
|
+
const partials = signal._partial ?? [];
|
|
3210
|
+
// No partial exits — pure harmonic mean of all entries
|
|
3211
|
+
if (partials.length === 0) {
|
|
3212
|
+
return harmonicMean(entries.map((e) => e.price));
|
|
3213
|
+
}
|
|
3214
|
+
// Replay cost basis through all partials to get snapshot at the last one
|
|
3215
|
+
let costBasis = 0;
|
|
3216
|
+
for (let i = 0; i < partials.length; i++) {
|
|
3217
|
+
const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
|
|
3218
|
+
const newEntryCount = partials[i].entryCountAtClose - prevCount;
|
|
3219
|
+
costBasis += newEntryCount * COST_BASIS_PER_ENTRY$3;
|
|
3220
|
+
// costBasis is now positionCostBasisAtClose for partials[i]
|
|
3221
|
+
if (i < partials.length - 1) {
|
|
3222
|
+
costBasis *= 1 - partials[i].percent / 100;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
const lastPartial = partials[partials.length - 1];
|
|
3226
|
+
// Dollar cost basis remaining after the last partial close
|
|
3227
|
+
const remainingCostBasis = costBasis * (1 - lastPartial.percent / 100);
|
|
3228
|
+
// Coins remaining from the old position
|
|
3229
|
+
const oldCoins = remainingCostBasis / lastPartial.effectivePrice;
|
|
3230
|
+
// New DCA entries added AFTER the last partial close
|
|
3231
|
+
const newEntries = entries.slice(lastPartial.entryCountAtClose);
|
|
3232
|
+
// Coins from new DCA entries (each costs $100)
|
|
3233
|
+
const newCoins = newEntries.reduce((sum, e) => sum + 100 / e.price, 0);
|
|
3234
|
+
const totalCoins = oldCoins + newCoins;
|
|
3235
|
+
if (totalCoins === 0)
|
|
3236
|
+
return lastPartial.effectivePrice;
|
|
3237
|
+
const totalCost = remainingCostBasis + newEntries.length * 100;
|
|
3238
|
+
return totalCost / totalCoins;
|
|
3239
|
+
};
|
|
3240
|
+
const harmonicMean = (prices) => {
|
|
3241
|
+
if (prices.length === 0)
|
|
3242
|
+
return 0;
|
|
3243
|
+
return prices.length / prices.reduce((sum, p) => sum + 1 / p, 0);
|
|
3196
3244
|
};
|
|
3197
3245
|
|
|
3246
|
+
const COST_BASIS_PER_ENTRY$2 = 100;
|
|
3198
3247
|
/**
|
|
3199
3248
|
* Calculates profit/loss for a closed signal with slippage and fees.
|
|
3200
3249
|
*
|
|
3201
3250
|
* For signals with partial closes:
|
|
3202
|
-
* -
|
|
3203
|
-
*
|
|
3204
|
-
*
|
|
3205
|
-
*
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
*
|
|
3211
|
-
*
|
|
3212
|
-
*
|
|
3213
|
-
*
|
|
3214
|
-
*
|
|
3251
|
+
* - Weights are calculated by ACTUAL DOLLAR VALUE of each partial relative to total invested.
|
|
3252
|
+
* This correctly handles DCA entries that occur before or after partial closes.
|
|
3253
|
+
*
|
|
3254
|
+
* Cost basis is reconstructed by replaying the partial sequence via entryCountAtClose + percent:
|
|
3255
|
+
* costBasis = 0
|
|
3256
|
+
* for each partial[i]:
|
|
3257
|
+
* costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
|
|
3258
|
+
* partialDollarValue[i] = (percent[i] / 100) × costBasis
|
|
3259
|
+
* weight[i] = partialDollarValue[i] / totalInvested
|
|
3260
|
+
* costBasis *= (1 - percent[i] / 100)
|
|
3261
|
+
*
|
|
3262
|
+
* Fee structure:
|
|
3263
|
+
* - Open fee: CC_PERCENT_FEE (charged once)
|
|
3264
|
+
* - Close fee: CC_PERCENT_FEE × weight × (closeWithSlip / openWithSlip) per partial/remaining
|
|
3215
3265
|
*
|
|
3216
3266
|
* @param signal - Closed signal with position details and optional partial history
|
|
3217
3267
|
* @param priceClose - Actual close price at final exit
|
|
3218
3268
|
* @returns PNL data with percentage and prices
|
|
3219
|
-
*
|
|
3220
|
-
* @example
|
|
3221
|
-
* ```typescript
|
|
3222
|
-
* // Signal without partial closes
|
|
3223
|
-
* const pnl = toProfitLossDto(
|
|
3224
|
-
* {
|
|
3225
|
-
* position: "long",
|
|
3226
|
-
* priceOpen: 100,
|
|
3227
|
-
* },
|
|
3228
|
-
* 110 // close at +10%
|
|
3229
|
-
* );
|
|
3230
|
-
* console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
|
|
3231
|
-
*
|
|
3232
|
-
* // Signal with partial closes
|
|
3233
|
-
* const pnlPartial = toProfitLossDto(
|
|
3234
|
-
* {
|
|
3235
|
-
* position: "long",
|
|
3236
|
-
* priceOpen: 100,
|
|
3237
|
-
* _partial: [
|
|
3238
|
-
* { type: "profit", percent: 30, price: 120 }, // +20% on 30%
|
|
3239
|
-
* { type: "profit", percent: 40, price: 115 }, // +15% on 40%
|
|
3240
|
-
* ],
|
|
3241
|
-
* },
|
|
3242
|
-
* 105 // final close at +5% for remaining 30%
|
|
3243
|
-
* );
|
|
3244
|
-
* // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
|
|
3245
|
-
* ```
|
|
3246
3269
|
*/
|
|
3247
3270
|
const toProfitLossDto = (signal, priceClose) => {
|
|
3248
3271
|
const priceOpen = getEffectivePriceOpen(signal);
|
|
@@ -3251,47 +3274,65 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3251
3274
|
let totalWeightedPnl = 0;
|
|
3252
3275
|
// Open fee is paid once for the whole position
|
|
3253
3276
|
let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
|
|
3254
|
-
//
|
|
3255
|
-
const
|
|
3256
|
-
|
|
3257
|
-
|
|
3277
|
+
// Total invested capital = number of DCA entries × $100 per entry
|
|
3278
|
+
const totalInvested = signal._entry ? signal._entry.length * 100 : 100;
|
|
3279
|
+
let closedDollarValue = 0;
|
|
3280
|
+
// Running cost basis — replayed from entryCountAtClose + percent
|
|
3281
|
+
let costBasis = 0;
|
|
3258
3282
|
// Calculate PNL for each partial close
|
|
3259
|
-
for (
|
|
3260
|
-
const
|
|
3283
|
+
for (let i = 0; i < signal._partial.length; i++) {
|
|
3284
|
+
const partial = signal._partial[i];
|
|
3285
|
+
// Add DCA entries that existed at this partial but not at the previous one
|
|
3286
|
+
const prevCount = i === 0 ? 0 : signal._partial[i - 1].entryCountAtClose;
|
|
3287
|
+
const newEntryCount = partial.entryCountAtClose - prevCount;
|
|
3288
|
+
costBasis += newEntryCount * COST_BASIS_PER_ENTRY$2;
|
|
3289
|
+
// Real dollar value sold in this partial
|
|
3290
|
+
const partialDollarValue = (partial.percent / 100) * costBasis;
|
|
3291
|
+
// Weight relative to total invested capital
|
|
3292
|
+
const weight = partialDollarValue / totalInvested;
|
|
3293
|
+
closedDollarValue += partialDollarValue;
|
|
3294
|
+
// Reduce cost basis after close
|
|
3295
|
+
costBasis *= 1 - partial.percent / 100;
|
|
3296
|
+
// Use the effective entry price snapshot captured at the time of this partial close
|
|
3297
|
+
const priceOpenWithSlippage = signal.position === "long"
|
|
3298
|
+
? partial.effectivePrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3299
|
+
: partial.effectivePrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3261
3300
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3262
|
-
? partial.
|
|
3263
|
-
: partial.
|
|
3264
|
-
// Calculate PNL for this partial
|
|
3301
|
+
? partial.currentPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3302
|
+
: partial.currentPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3265
3303
|
const partialPnl = signal.position === "long"
|
|
3266
3304
|
? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
|
|
3267
3305
|
: ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3306
|
+
totalWeightedPnl += weight * partialPnl;
|
|
3307
|
+
// Close fee proportional to real dollar weight
|
|
3308
|
+
totalFees +=
|
|
3309
|
+
GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3310
|
+
weight *
|
|
3311
|
+
(priceCloseWithSlippage / priceOpenWithSlippage);
|
|
3312
|
+
}
|
|
3313
|
+
if (closedDollarValue > totalInvested + 0.001) {
|
|
3314
|
+
throw new Error(`Partial closes dollar value (${closedDollarValue.toFixed(4)}) exceeds total invested (${totalInvested}) — signal id: ${signal.id}`);
|
|
3315
|
+
}
|
|
3316
|
+
// Remaining position
|
|
3317
|
+
const remainingDollarValue = totalInvested - closedDollarValue;
|
|
3318
|
+
const remainingWeight = remainingDollarValue / totalInvested;
|
|
3319
|
+
if (remainingWeight > 0) {
|
|
3320
|
+
// Use current effective price — reflects all DCA including post-partial entries
|
|
3321
|
+
const remainingOpenWithSlippage = signal.position === "long"
|
|
3322
|
+
? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3323
|
+
: priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3281
3324
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3282
3325
|
? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3283
3326
|
: priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3284
|
-
// Calculate PNL for remaining
|
|
3285
3327
|
const remainingPnl = signal.position === "long"
|
|
3286
|
-
? ((priceCloseWithSlippage -
|
|
3287
|
-
: ((
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3328
|
+
? ((priceCloseWithSlippage - remainingOpenWithSlippage) / remainingOpenWithSlippage) * 100
|
|
3329
|
+
: ((remainingOpenWithSlippage - priceCloseWithSlippage) / remainingOpenWithSlippage) * 100;
|
|
3330
|
+
totalWeightedPnl += remainingWeight * remainingPnl;
|
|
3331
|
+
totalFees +=
|
|
3332
|
+
GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3333
|
+
remainingWeight *
|
|
3334
|
+
(priceCloseWithSlippage / remainingOpenWithSlippage);
|
|
3292
3335
|
}
|
|
3293
|
-
// Subtract total fees from weighted PNL
|
|
3294
|
-
// totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
|
|
3295
3336
|
const pnlPercentage = totalWeightedPnl - totalFees;
|
|
3296
3337
|
return {
|
|
3297
3338
|
pnlPercentage,
|
|
@@ -3303,33 +3344,24 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3303
3344
|
let priceOpenWithSlippage;
|
|
3304
3345
|
let priceCloseWithSlippage;
|
|
3305
3346
|
if (signal.position === "long") {
|
|
3306
|
-
// LONG: покупаем дороже, продаем дешевле
|
|
3307
3347
|
priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3308
3348
|
priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3309
3349
|
}
|
|
3310
3350
|
else {
|
|
3311
|
-
// SHORT: продаем дешевле, покупаем дороже
|
|
3312
3351
|
priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3313
3352
|
priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3314
3353
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3354
|
+
const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3355
|
+
(1 + priceCloseWithSlippage / priceOpenWithSlippage);
|
|
3317
3356
|
let pnlPercentage;
|
|
3318
3357
|
if (signal.position === "long") {
|
|
3319
|
-
// LONG: прибыль при росте цены
|
|
3320
3358
|
pnlPercentage =
|
|
3321
|
-
((priceCloseWithSlippage - priceOpenWithSlippage) /
|
|
3322
|
-
priceOpenWithSlippage) *
|
|
3323
|
-
100;
|
|
3359
|
+
((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3324
3360
|
}
|
|
3325
3361
|
else {
|
|
3326
|
-
// SHORT: прибыль при падении цены
|
|
3327
3362
|
pnlPercentage =
|
|
3328
|
-
((priceOpenWithSlippage - priceCloseWithSlippage) /
|
|
3329
|
-
priceOpenWithSlippage) *
|
|
3330
|
-
100;
|
|
3363
|
+
((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3331
3364
|
}
|
|
3332
|
-
// Вычитаем комиссии
|
|
3333
3365
|
pnlPercentage -= totalFee;
|
|
3334
3366
|
return {
|
|
3335
3367
|
pnlPercentage,
|
|
@@ -3385,6 +3417,54 @@ const toPlainString = (content) => {
|
|
|
3385
3417
|
return text.trim();
|
|
3386
3418
|
};
|
|
3387
3419
|
|
|
3420
|
+
const COST_BASIS_PER_ENTRY$1 = 100;
|
|
3421
|
+
/**
|
|
3422
|
+
* Returns the total closed state of a position using cost-basis replay.
|
|
3423
|
+
*
|
|
3424
|
+
* Correctly accounts for DCA entries added between partial closes via averageBuy().
|
|
3425
|
+
* Simple percent summation (sum of _partial[i].percent) is INCORRECT when averageBuy()
|
|
3426
|
+
* is called between partials — this function uses the same cost-basis replay as
|
|
3427
|
+
* toProfitLossDto to compute the true dollar-weighted closed fraction.
|
|
3428
|
+
*
|
|
3429
|
+
* Cost-basis replay:
|
|
3430
|
+
* costBasis = 0
|
|
3431
|
+
* for each partial[i]:
|
|
3432
|
+
* costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
|
|
3433
|
+
* closedDollar += (percent[i] / 100) × costBasis
|
|
3434
|
+
* costBasis ×= (1 - percent[i] / 100)
|
|
3435
|
+
* // then add entries added AFTER the last partial
|
|
3436
|
+
* costBasis += (currentEntryCount - lastPartialEntryCount) × $100
|
|
3437
|
+
*
|
|
3438
|
+
* @param signal - Signal row with _partial and _entry arrays
|
|
3439
|
+
* @returns Object with totalClosedPercent (0–100+) and remainingCostBasis (dollar value still open)
|
|
3440
|
+
*/
|
|
3441
|
+
const getTotalClosed = (signal) => {
|
|
3442
|
+
const partials = signal._partial ?? [];
|
|
3443
|
+
const currentEntryCount = signal._entry?.length ?? 1;
|
|
3444
|
+
const totalInvested = currentEntryCount * COST_BASIS_PER_ENTRY$1;
|
|
3445
|
+
if (partials.length === 0) {
|
|
3446
|
+
return {
|
|
3447
|
+
totalClosedPercent: 0,
|
|
3448
|
+
remainingCostBasis: totalInvested,
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
let costBasis = 0;
|
|
3452
|
+
let closedDollarValue = 0;
|
|
3453
|
+
for (let i = 0; i < partials.length; i++) {
|
|
3454
|
+
const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
|
|
3455
|
+
costBasis += (partials[i].entryCountAtClose - prevCount) * COST_BASIS_PER_ENTRY$1;
|
|
3456
|
+
closedDollarValue += (partials[i].percent / 100) * costBasis;
|
|
3457
|
+
costBasis *= 1 - partials[i].percent / 100;
|
|
3458
|
+
}
|
|
3459
|
+
// Add entries added AFTER the last partial (not yet accounted for in the loop)
|
|
3460
|
+
const lastEntryCount = partials[partials.length - 1].entryCountAtClose;
|
|
3461
|
+
costBasis += (currentEntryCount - lastEntryCount) * COST_BASIS_PER_ENTRY$1;
|
|
3462
|
+
return {
|
|
3463
|
+
totalClosedPercent: totalInvested > 0 ? (closedDollarValue / totalInvested) * 100 : 0,
|
|
3464
|
+
remainingCostBasis: costBasis,
|
|
3465
|
+
};
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3388
3468
|
/**
|
|
3389
3469
|
* Wraps a function to execute it outside of the current execution context if one exists.
|
|
3390
3470
|
*
|
|
@@ -3427,6 +3507,19 @@ const beginTime = (run) => (...args) => {
|
|
|
3427
3507
|
return fn();
|
|
3428
3508
|
};
|
|
3429
3509
|
|
|
3510
|
+
/**
|
|
3511
|
+
* Retrieves the current timestamp for debugging purposes.
|
|
3512
|
+
* If an execution context is active (e.g., during a backtest), it returns the timestamp from the context to ensure consistency with the simulated time.
|
|
3513
|
+
* Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
|
|
3514
|
+
* @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
|
|
3515
|
+
*/
|
|
3516
|
+
const getDebugTimestamp = () => {
|
|
3517
|
+
if (ExecutionContextService.hasContext()) {
|
|
3518
|
+
return bt.executionContextService.context.when.getTime();
|
|
3519
|
+
}
|
|
3520
|
+
return undefined;
|
|
3521
|
+
};
|
|
3522
|
+
|
|
3430
3523
|
const INTERVAL_MINUTES$6 = {
|
|
3431
3524
|
"1m": 1,
|
|
3432
3525
|
"3m": 3,
|
|
@@ -3435,6 +3528,7 @@ const INTERVAL_MINUTES$6 = {
|
|
|
3435
3528
|
"30m": 30,
|
|
3436
3529
|
"1h": 60,
|
|
3437
3530
|
};
|
|
3531
|
+
const COST_BASIS_PER_ENTRY = 100;
|
|
3438
3532
|
/**
|
|
3439
3533
|
* Mock value for scheduled signal pendingAt timestamp.
|
|
3440
3534
|
* Used to indicate that the actual pendingAt will be set upon activation.
|
|
@@ -3690,8 +3784,8 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
|
|
|
3690
3784
|
const TO_PUBLIC_SIGNAL = (signal) => {
|
|
3691
3785
|
const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
|
|
3692
3786
|
const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
|
|
3693
|
-
const partialExecuted =
|
|
3694
|
-
? signal
|
|
3787
|
+
const partialExecuted = "_partial" in signal
|
|
3788
|
+
? getTotalClosed(signal).totalClosedPercent
|
|
3695
3789
|
: 0;
|
|
3696
3790
|
const totalEntries = ("_entry" in signal && Array.isArray(signal._entry))
|
|
3697
3791
|
? signal._entry.length
|
|
@@ -4036,7 +4130,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4036
4130
|
scheduledAt: currentTime,
|
|
4037
4131
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
4038
4132
|
_isScheduled: false,
|
|
4039
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4133
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
4040
4134
|
};
|
|
4041
4135
|
// Валидируем сигнал перед возвратом
|
|
4042
4136
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4058,7 +4152,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4058
4152
|
scheduledAt: currentTime,
|
|
4059
4153
|
pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
|
|
4060
4154
|
_isScheduled: true,
|
|
4061
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4155
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
4062
4156
|
};
|
|
4063
4157
|
// Валидируем сигнал перед возвратом
|
|
4064
4158
|
VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
|
|
@@ -4076,7 +4170,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
4076
4170
|
scheduledAt: currentTime,
|
|
4077
4171
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
4078
4172
|
_isScheduled: false,
|
|
4079
|
-
_entry: [{ price: currentPrice }],
|
|
4173
|
+
_entry: [{ price: currentPrice, debugTimestamp: currentTime }],
|
|
4080
4174
|
};
|
|
4081
4175
|
// Валидируем сигнал перед возвратом
|
|
4082
4176
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4150,37 +4244,39 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4150
4244
|
// Initialize partial array if not present
|
|
4151
4245
|
if (!signal._partial)
|
|
4152
4246
|
signal._partial = [];
|
|
4153
|
-
//
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
const
|
|
4158
|
-
|
|
4159
|
-
.
|
|
4160
|
-
const totalClosed = tpClosed + slClosed;
|
|
4161
|
-
// Check if would exceed 100% total closed
|
|
4162
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4163
|
-
if (newTotalClosed > 100) {
|
|
4164
|
-
self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
|
|
4247
|
+
// Check if would exceed 100% total closed (dollar-basis, DCA-aware)
|
|
4248
|
+
const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
|
|
4249
|
+
const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
4250
|
+
const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
|
|
4251
|
+
const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
|
|
4252
|
+
if (newTotalClosedDollar > totalInvested) {
|
|
4253
|
+
self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed (dollar basis), skipping", {
|
|
4165
4254
|
signalId: signal.id,
|
|
4166
|
-
|
|
4255
|
+
totalClosedPercent,
|
|
4256
|
+
remainingCostBasis,
|
|
4167
4257
|
percentToClose,
|
|
4168
|
-
|
|
4258
|
+
newPartialDollar,
|
|
4259
|
+
totalInvested,
|
|
4169
4260
|
});
|
|
4170
4261
|
return false;
|
|
4171
4262
|
}
|
|
4263
|
+
// Capture effective entry price at the moment of partial close (for DCA-aware PNL)
|
|
4264
|
+
const effectivePrice = getEffectivePriceOpen(signal);
|
|
4265
|
+
const entryCountAtClose = signal._entry ? signal._entry.length : 1;
|
|
4172
4266
|
// Add new partial close entry
|
|
4173
4267
|
signal._partial.push({
|
|
4174
4268
|
type: "profit",
|
|
4175
4269
|
percent: percentToClose,
|
|
4176
|
-
|
|
4270
|
+
entryCountAtClose,
|
|
4271
|
+
currentPrice,
|
|
4272
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4273
|
+
effectivePrice,
|
|
4177
4274
|
});
|
|
4178
4275
|
self.params.logger.info("PARTIAL_PROFIT_FN executed", {
|
|
4179
4276
|
signalId: signal.id,
|
|
4180
4277
|
percentClosed: percentToClose,
|
|
4181
|
-
|
|
4278
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4182
4279
|
currentPrice,
|
|
4183
|
-
tpClosed: tpClosed + percentToClose,
|
|
4184
4280
|
});
|
|
4185
4281
|
return true;
|
|
4186
4282
|
};
|
|
@@ -4188,37 +4284,39 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4188
4284
|
// Initialize partial array if not present
|
|
4189
4285
|
if (!signal._partial)
|
|
4190
4286
|
signal._partial = [];
|
|
4191
|
-
//
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
const
|
|
4196
|
-
|
|
4197
|
-
.
|
|
4198
|
-
const totalClosed = tpClosed + slClosed;
|
|
4199
|
-
// Check if would exceed 100% total closed
|
|
4200
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4201
|
-
if (newTotalClosed > 100) {
|
|
4202
|
-
self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
|
|
4287
|
+
// Check if would exceed 100% total closed (dollar-basis, DCA-aware)
|
|
4288
|
+
const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
|
|
4289
|
+
const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
4290
|
+
const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
|
|
4291
|
+
const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
|
|
4292
|
+
if (newTotalClosedDollar > totalInvested) {
|
|
4293
|
+
self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed (dollar basis), skipping", {
|
|
4203
4294
|
signalId: signal.id,
|
|
4204
|
-
|
|
4295
|
+
totalClosedPercent,
|
|
4296
|
+
remainingCostBasis,
|
|
4205
4297
|
percentToClose,
|
|
4206
|
-
|
|
4298
|
+
newPartialDollar,
|
|
4299
|
+
totalInvested,
|
|
4207
4300
|
});
|
|
4208
4301
|
return false;
|
|
4209
4302
|
}
|
|
4303
|
+
// Capture effective entry price at the moment of partial close (for DCA-aware PNL)
|
|
4304
|
+
const effectivePrice = getEffectivePriceOpen(signal);
|
|
4305
|
+
const entryCountAtClose = signal._entry ? signal._entry.length : 1;
|
|
4210
4306
|
// Add new partial close entry
|
|
4211
4307
|
signal._partial.push({
|
|
4212
4308
|
type: "loss",
|
|
4213
4309
|
percent: percentToClose,
|
|
4214
|
-
|
|
4310
|
+
currentPrice,
|
|
4311
|
+
entryCountAtClose,
|
|
4312
|
+
effectivePrice,
|
|
4313
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4215
4314
|
});
|
|
4216
4315
|
self.params.logger.warn("PARTIAL_LOSS_FN executed", {
|
|
4217
4316
|
signalId: signal.id,
|
|
4218
4317
|
percentClosed: percentToClose,
|
|
4219
|
-
|
|
4318
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4220
4319
|
currentPrice,
|
|
4221
|
-
slClosed: slClosed + percentToClose,
|
|
4222
4320
|
});
|
|
4223
4321
|
return true;
|
|
4224
4322
|
};
|
|
@@ -4612,12 +4710,12 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
|
|
|
4612
4710
|
const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
4613
4711
|
// Ensure _entry is initialized (handles signals loaded from disk without _entry)
|
|
4614
4712
|
if (!signal._entry || signal._entry.length === 0) {
|
|
4615
|
-
signal._entry = [{ price: signal.priceOpen }];
|
|
4713
|
+
signal._entry = [{ price: signal.priceOpen, debugTimestamp: getDebugTimestamp() }];
|
|
4616
4714
|
}
|
|
4617
4715
|
const lastEntry = signal._entry[signal._entry.length - 1];
|
|
4618
4716
|
if (signal.position === "long") {
|
|
4619
4717
|
// LONG: averaging down = currentPrice must be strictly lower than last entry
|
|
4620
|
-
if (currentPrice >= lastEntry.price) {
|
|
4718
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= lastEntry.price) {
|
|
4621
4719
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
|
|
4622
4720
|
signalId: signal.id,
|
|
4623
4721
|
position: signal.position,
|
|
@@ -4630,7 +4728,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4630
4728
|
}
|
|
4631
4729
|
else {
|
|
4632
4730
|
// SHORT: averaging down = currentPrice must be strictly higher than last entry
|
|
4633
|
-
if (currentPrice <= lastEntry.price) {
|
|
4731
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= lastEntry.price) {
|
|
4634
4732
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
|
|
4635
4733
|
signalId: signal.id,
|
|
4636
4734
|
position: signal.position,
|
|
@@ -4641,7 +4739,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4641
4739
|
return false;
|
|
4642
4740
|
}
|
|
4643
4741
|
}
|
|
4644
|
-
signal._entry.push({ price: currentPrice });
|
|
4742
|
+
signal._entry.push({ price: currentPrice, debugTimestamp: getDebugTimestamp() });
|
|
4645
4743
|
self.params.logger.info("AVERAGE_BUY_FN executed", {
|
|
4646
4744
|
signalId: signal.id,
|
|
4647
4745
|
position: signal.position,
|
|
@@ -6106,6 +6204,183 @@ class ClientStrategy {
|
|
|
6106
6204
|
});
|
|
6107
6205
|
return this._isStopped;
|
|
6108
6206
|
}
|
|
6207
|
+
/**
|
|
6208
|
+
* Returns how much of the position is still held, as a percentage of totalInvested.
|
|
6209
|
+
*
|
|
6210
|
+
* Uses dollar-basis cost-basis replay (DCA-aware).
|
|
6211
|
+
* 100% means nothing was closed yet. Decreases with each partial close.
|
|
6212
|
+
*
|
|
6213
|
+
* Example: 1 entry $100, partialProfit(30%) → returns 70
|
|
6214
|
+
* Example: 2 entries $200, partialProfit(50%) → returns 50
|
|
6215
|
+
*
|
|
6216
|
+
* Returns 100 if no pending signal or no partial closes.
|
|
6217
|
+
*
|
|
6218
|
+
* @param symbol - Trading pair symbol
|
|
6219
|
+
* @returns Promise resolving to held percentage (0–100)
|
|
6220
|
+
*/
|
|
6221
|
+
async getTotalPercentClosed(symbol) {
|
|
6222
|
+
this.params.logger.debug("ClientStrategy getTotalPercentClosed", { symbol });
|
|
6223
|
+
if (!this._pendingSignal) {
|
|
6224
|
+
return null;
|
|
6225
|
+
}
|
|
6226
|
+
const { totalClosedPercent } = getTotalClosed(this._pendingSignal);
|
|
6227
|
+
return 100 - totalClosedPercent;
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* Returns how many dollars of cost basis are still held (not yet closed by partials).
|
|
6231
|
+
*
|
|
6232
|
+
* Equal to remainingCostBasis from getTotalClosed.
|
|
6233
|
+
* Full position open: equals totalInvested (entries × $100).
|
|
6234
|
+
* Decreases with each partial close, increases with each averageBuy().
|
|
6235
|
+
*
|
|
6236
|
+
* Example: 1 entry $100, no partials → returns 100
|
|
6237
|
+
* Example: 1 entry $100, partialProfit(30%) → returns 70
|
|
6238
|
+
* Example: 2 entries $200, partialProfit(50%) → returns 100
|
|
6239
|
+
*
|
|
6240
|
+
* Returns totalInvested if no pending signal or no partial closes.
|
|
6241
|
+
*
|
|
6242
|
+
* @param symbol - Trading pair symbol
|
|
6243
|
+
* @returns Promise resolving to held cost basis in dollars
|
|
6244
|
+
*/
|
|
6245
|
+
async getTotalCostClosed(symbol) {
|
|
6246
|
+
this.params.logger.debug("ClientStrategy getTotalCostClosed", { symbol });
|
|
6247
|
+
if (!this._pendingSignal) {
|
|
6248
|
+
return null;
|
|
6249
|
+
}
|
|
6250
|
+
const { remainingCostBasis } = getTotalClosed(this._pendingSignal);
|
|
6251
|
+
return remainingCostBasis;
|
|
6252
|
+
}
|
|
6253
|
+
/**
|
|
6254
|
+
* Returns the effective (DCA-averaged) entry price for the current pending signal.
|
|
6255
|
+
*
|
|
6256
|
+
* This is the harmonic mean of all _entry prices, which is the correct
|
|
6257
|
+
* cost-basis price used in all PNL calculations.
|
|
6258
|
+
* With no DCA entries, equals the original priceOpen.
|
|
6259
|
+
*
|
|
6260
|
+
* Returns null if no pending signal exists.
|
|
6261
|
+
*
|
|
6262
|
+
* @param symbol - Trading pair symbol
|
|
6263
|
+
* @returns Promise resolving to effective entry price or null
|
|
6264
|
+
*/
|
|
6265
|
+
async getPositionAveragePrice(symbol) {
|
|
6266
|
+
this.params.logger.debug("ClientStrategy getPositionAveragePrice", { symbol });
|
|
6267
|
+
if (!this._pendingSignal) {
|
|
6268
|
+
return null;
|
|
6269
|
+
}
|
|
6270
|
+
return getEffectivePriceOpen(this._pendingSignal);
|
|
6271
|
+
}
|
|
6272
|
+
/**
|
|
6273
|
+
* Returns the number of DCA entries made for the current pending signal.
|
|
6274
|
+
*
|
|
6275
|
+
* 1 = original entry only (no DCA).
|
|
6276
|
+
* Increases by 1 with each successful commitAverageBuy().
|
|
6277
|
+
*
|
|
6278
|
+
* Returns null if no pending signal exists.
|
|
6279
|
+
*
|
|
6280
|
+
* @param symbol - Trading pair symbol
|
|
6281
|
+
* @returns Promise resolving to entry count or null
|
|
6282
|
+
*/
|
|
6283
|
+
async getPositionInvestedCount(symbol) {
|
|
6284
|
+
this.params.logger.debug("ClientStrategy getPositionInvestedCount", { symbol });
|
|
6285
|
+
if (!this._pendingSignal) {
|
|
6286
|
+
return null;
|
|
6287
|
+
}
|
|
6288
|
+
return this._pendingSignal._entry?.length ?? 1;
|
|
6289
|
+
}
|
|
6290
|
+
/**
|
|
6291
|
+
* Returns the total invested cost basis in dollars for the current pending signal.
|
|
6292
|
+
*
|
|
6293
|
+
* Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
|
|
6294
|
+
* 1 entry = $100, 2 entries = $200, etc.
|
|
6295
|
+
*
|
|
6296
|
+
* Returns null if no pending signal exists.
|
|
6297
|
+
*
|
|
6298
|
+
* @param symbol - Trading pair symbol
|
|
6299
|
+
* @returns Promise resolving to total invested cost in dollars or null
|
|
6300
|
+
*/
|
|
6301
|
+
async getPositionInvestedCost(symbol) {
|
|
6302
|
+
this.params.logger.debug("ClientStrategy getPositionInvestedCost", { symbol });
|
|
6303
|
+
if (!this._pendingSignal) {
|
|
6304
|
+
return null;
|
|
6305
|
+
}
|
|
6306
|
+
return (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
6307
|
+
}
|
|
6308
|
+
/**
|
|
6309
|
+
* Returns the unrealized PNL percentage for the current pending signal at currentPrice.
|
|
6310
|
+
*
|
|
6311
|
+
* Accounts for partial closes, DCA entries, slippage and fees
|
|
6312
|
+
* (delegates to toProfitLossDto).
|
|
6313
|
+
*
|
|
6314
|
+
* Returns null if no pending signal exists.
|
|
6315
|
+
*
|
|
6316
|
+
* @param symbol - Trading pair symbol
|
|
6317
|
+
* @param currentPrice - Current market price
|
|
6318
|
+
* @returns Promise resolving to pnlPercentage or null
|
|
6319
|
+
*/
|
|
6320
|
+
async getPositionPnlPercent(symbol, currentPrice) {
|
|
6321
|
+
this.params.logger.debug("ClientStrategy getPositionPnlPercent", { symbol, currentPrice });
|
|
6322
|
+
if (!this._pendingSignal) {
|
|
6323
|
+
return null;
|
|
6324
|
+
}
|
|
6325
|
+
const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
|
|
6326
|
+
return pnl.pnlPercentage;
|
|
6327
|
+
}
|
|
6328
|
+
/**
|
|
6329
|
+
* Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
|
|
6330
|
+
*
|
|
6331
|
+
* Calculated as: pnlPercentage / 100 × totalInvestedCost
|
|
6332
|
+
* Accounts for partial closes, DCA entries, slippage and fees.
|
|
6333
|
+
*
|
|
6334
|
+
* Returns null if no pending signal exists.
|
|
6335
|
+
*
|
|
6336
|
+
* @param symbol - Trading pair symbol
|
|
6337
|
+
* @param currentPrice - Current market price
|
|
6338
|
+
* @returns Promise resolving to pnl in dollars or null
|
|
6339
|
+
*/
|
|
6340
|
+
async getPositionPnlCost(symbol, currentPrice) {
|
|
6341
|
+
this.params.logger.debug("ClientStrategy getPositionPnlCost", { symbol, currentPrice });
|
|
6342
|
+
if (!this._pendingSignal) {
|
|
6343
|
+
return null;
|
|
6344
|
+
}
|
|
6345
|
+
const totalInvested = (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
6346
|
+
const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
|
|
6347
|
+
return (pnl.pnlPercentage / 100) * totalInvested;
|
|
6348
|
+
}
|
|
6349
|
+
/**
|
|
6350
|
+
* Returns the list of DCA entry prices for the current pending signal.
|
|
6351
|
+
*
|
|
6352
|
+
* The first element is always the original priceOpen (initial entry).
|
|
6353
|
+
* Each subsequent element is a price added by commitAverageBuy().
|
|
6354
|
+
*
|
|
6355
|
+
* Returns null if no pending signal exists.
|
|
6356
|
+
* Returns a single-element array [priceOpen] if no DCA entries were made.
|
|
6357
|
+
*
|
|
6358
|
+
* @param symbol - Trading pair symbol
|
|
6359
|
+
* @returns Promise resolving to array of entry prices or null
|
|
6360
|
+
*
|
|
6361
|
+
* @example
|
|
6362
|
+
* // No DCA: [43000]
|
|
6363
|
+
* // One DCA: [43000, 42000]
|
|
6364
|
+
* // Two DCA: [43000, 42000, 41500]
|
|
6365
|
+
*/
|
|
6366
|
+
async getPositionLevels(symbol) {
|
|
6367
|
+
this.params.logger.debug("ClientStrategy getPositionLevels", { symbol });
|
|
6368
|
+
if (!this._pendingSignal) {
|
|
6369
|
+
return null;
|
|
6370
|
+
}
|
|
6371
|
+
const entries = this._pendingSignal._entry;
|
|
6372
|
+
if (!entries || entries.length === 0) {
|
|
6373
|
+
return [this._pendingSignal.priceOpen];
|
|
6374
|
+
}
|
|
6375
|
+
return entries.map((e) => e.price);
|
|
6376
|
+
}
|
|
6377
|
+
async getPositionPartials(symbol) {
|
|
6378
|
+
this.params.logger.debug("ClientStrategy getPositionPartials", { symbol });
|
|
6379
|
+
if (!this._pendingSignal) {
|
|
6380
|
+
return null;
|
|
6381
|
+
}
|
|
6382
|
+
return this._pendingSignal._partial ?? [];
|
|
6383
|
+
}
|
|
6109
6384
|
/**
|
|
6110
6385
|
* Performs a single tick of strategy execution.
|
|
6111
6386
|
*
|
|
@@ -7566,14 +7841,6 @@ class ClientStrategy {
|
|
|
7566
7841
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
7567
7842
|
throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
7568
7843
|
}
|
|
7569
|
-
// Reject if any partial closes have already been executed
|
|
7570
|
-
if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
|
|
7571
|
-
this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
|
|
7572
|
-
symbol,
|
|
7573
|
-
partialCount: this._pendingSignal._partial.length,
|
|
7574
|
-
});
|
|
7575
|
-
return false;
|
|
7576
|
-
}
|
|
7577
7844
|
// Execute averaging logic
|
|
7578
7845
|
const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
|
|
7579
7846
|
if (!result) {
|
|
@@ -8252,6 +8519,108 @@ class StrategyConnectionService {
|
|
|
8252
8519
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8253
8520
|
return await strategy.getPendingSignal(symbol);
|
|
8254
8521
|
};
|
|
8522
|
+
/**
|
|
8523
|
+
* Returns the percentage of the position currently held (not closed).
|
|
8524
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
8525
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8526
|
+
*
|
|
8527
|
+
* @param backtest - Whether running in backtest mode
|
|
8528
|
+
* @param symbol - Trading pair symbol
|
|
8529
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8530
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
8531
|
+
*/
|
|
8532
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
8533
|
+
this.loggerService.log("strategyConnectionService getTotalPercentClosed", {
|
|
8534
|
+
symbol,
|
|
8535
|
+
context,
|
|
8536
|
+
backtest,
|
|
8537
|
+
});
|
|
8538
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8539
|
+
return await strategy.getTotalPercentClosed(symbol);
|
|
8540
|
+
};
|
|
8541
|
+
/**
|
|
8542
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
8543
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8544
|
+
*
|
|
8545
|
+
* @param backtest - Whether running in backtest mode
|
|
8546
|
+
* @param symbol - Trading pair symbol
|
|
8547
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8548
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
8549
|
+
*/
|
|
8550
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
8551
|
+
this.loggerService.log("strategyConnectionService getTotalCostClosed", {
|
|
8552
|
+
symbol,
|
|
8553
|
+
context,
|
|
8554
|
+
backtest,
|
|
8555
|
+
});
|
|
8556
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8557
|
+
return await strategy.getTotalCostClosed(symbol);
|
|
8558
|
+
};
|
|
8559
|
+
this.getPositionAveragePrice = async (backtest, symbol, context) => {
|
|
8560
|
+
this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
|
|
8561
|
+
symbol,
|
|
8562
|
+
context,
|
|
8563
|
+
backtest,
|
|
8564
|
+
});
|
|
8565
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8566
|
+
return await strategy.getPositionAveragePrice(symbol);
|
|
8567
|
+
};
|
|
8568
|
+
this.getPositionInvestedCount = async (backtest, symbol, context) => {
|
|
8569
|
+
this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
|
|
8570
|
+
symbol,
|
|
8571
|
+
context,
|
|
8572
|
+
backtest,
|
|
8573
|
+
});
|
|
8574
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8575
|
+
return await strategy.getPositionInvestedCount(symbol);
|
|
8576
|
+
};
|
|
8577
|
+
this.getPositionInvestedCost = async (backtest, symbol, context) => {
|
|
8578
|
+
this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
|
|
8579
|
+
symbol,
|
|
8580
|
+
context,
|
|
8581
|
+
backtest,
|
|
8582
|
+
});
|
|
8583
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8584
|
+
return await strategy.getPositionInvestedCost(symbol);
|
|
8585
|
+
};
|
|
8586
|
+
this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
|
|
8587
|
+
this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
|
|
8588
|
+
symbol,
|
|
8589
|
+
currentPrice,
|
|
8590
|
+
context,
|
|
8591
|
+
backtest,
|
|
8592
|
+
});
|
|
8593
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8594
|
+
return await strategy.getPositionPnlPercent(symbol, currentPrice);
|
|
8595
|
+
};
|
|
8596
|
+
this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
|
|
8597
|
+
this.loggerService.log("strategyConnectionService getPositionPnlCost", {
|
|
8598
|
+
symbol,
|
|
8599
|
+
currentPrice,
|
|
8600
|
+
context,
|
|
8601
|
+
backtest,
|
|
8602
|
+
});
|
|
8603
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8604
|
+
return await strategy.getPositionPnlCost(symbol, currentPrice);
|
|
8605
|
+
};
|
|
8606
|
+
this.getPositionLevels = async (backtest, symbol, context) => {
|
|
8607
|
+
this.loggerService.log("strategyConnectionService getPositionLevels", {
|
|
8608
|
+
symbol,
|
|
8609
|
+
context,
|
|
8610
|
+
backtest,
|
|
8611
|
+
});
|
|
8612
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8613
|
+
return await strategy.getPositionLevels(symbol);
|
|
8614
|
+
};
|
|
8615
|
+
this.getPositionPartials = async (backtest, symbol, context) => {
|
|
8616
|
+
this.loggerService.log("strategyConnectionService getPositionPartials", {
|
|
8617
|
+
symbol,
|
|
8618
|
+
context,
|
|
8619
|
+
backtest,
|
|
8620
|
+
});
|
|
8621
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8622
|
+
return await strategy.getPositionPartials(symbol);
|
|
8623
|
+
};
|
|
8255
8624
|
/**
|
|
8256
8625
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
8257
8626
|
* If no scheduled signal exists, returns null.
|
|
@@ -11655,6 +12024,99 @@ class StrategyCoreService {
|
|
|
11655
12024
|
await this.validate(context);
|
|
11656
12025
|
return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
|
|
11657
12026
|
};
|
|
12027
|
+
/**
|
|
12028
|
+
* Returns the percentage of the position currently held (not closed).
|
|
12029
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
12030
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
12031
|
+
*
|
|
12032
|
+
* @param backtest - Whether running in backtest mode
|
|
12033
|
+
* @param symbol - Trading pair symbol
|
|
12034
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
12035
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
12036
|
+
*/
|
|
12037
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
12038
|
+
this.loggerService.log("strategyCoreService getTotalPercentClosed", {
|
|
12039
|
+
symbol,
|
|
12040
|
+
context,
|
|
12041
|
+
});
|
|
12042
|
+
await this.validate(context);
|
|
12043
|
+
return await this.strategyConnectionService.getTotalPercentClosed(backtest, symbol, context);
|
|
12044
|
+
};
|
|
12045
|
+
/**
|
|
12046
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
12047
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
12048
|
+
*
|
|
12049
|
+
* @param backtest - Whether running in backtest mode
|
|
12050
|
+
* @param symbol - Trading pair symbol
|
|
12051
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
12052
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
12053
|
+
*/
|
|
12054
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
12055
|
+
this.loggerService.log("strategyCoreService getTotalCostClosed", {
|
|
12056
|
+
symbol,
|
|
12057
|
+
context,
|
|
12058
|
+
});
|
|
12059
|
+
await this.validate(context);
|
|
12060
|
+
return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
|
|
12061
|
+
};
|
|
12062
|
+
this.getPositionAveragePrice = async (backtest, symbol, context) => {
|
|
12063
|
+
this.loggerService.log("strategyCoreService getPositionAveragePrice", {
|
|
12064
|
+
symbol,
|
|
12065
|
+
context,
|
|
12066
|
+
});
|
|
12067
|
+
await this.validate(context);
|
|
12068
|
+
return await this.strategyConnectionService.getPositionAveragePrice(backtest, symbol, context);
|
|
12069
|
+
};
|
|
12070
|
+
this.getPositionInvestedCount = async (backtest, symbol, context) => {
|
|
12071
|
+
this.loggerService.log("strategyCoreService getPositionInvestedCount", {
|
|
12072
|
+
symbol,
|
|
12073
|
+
context,
|
|
12074
|
+
});
|
|
12075
|
+
await this.validate(context);
|
|
12076
|
+
return await this.strategyConnectionService.getPositionInvestedCount(backtest, symbol, context);
|
|
12077
|
+
};
|
|
12078
|
+
this.getPositionInvestedCost = async (backtest, symbol, context) => {
|
|
12079
|
+
this.loggerService.log("strategyCoreService getPositionInvestedCost", {
|
|
12080
|
+
symbol,
|
|
12081
|
+
context,
|
|
12082
|
+
});
|
|
12083
|
+
await this.validate(context);
|
|
12084
|
+
return await this.strategyConnectionService.getPositionInvestedCost(backtest, symbol, context);
|
|
12085
|
+
};
|
|
12086
|
+
this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
|
|
12087
|
+
this.loggerService.log("strategyCoreService getPositionPnlPercent", {
|
|
12088
|
+
symbol,
|
|
12089
|
+
currentPrice,
|
|
12090
|
+
context,
|
|
12091
|
+
});
|
|
12092
|
+
await this.validate(context);
|
|
12093
|
+
return await this.strategyConnectionService.getPositionPnlPercent(backtest, symbol, currentPrice, context);
|
|
12094
|
+
};
|
|
12095
|
+
this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
|
|
12096
|
+
this.loggerService.log("strategyCoreService getPositionPnlCost", {
|
|
12097
|
+
symbol,
|
|
12098
|
+
currentPrice,
|
|
12099
|
+
context,
|
|
12100
|
+
});
|
|
12101
|
+
await this.validate(context);
|
|
12102
|
+
return await this.strategyConnectionService.getPositionPnlCost(backtest, symbol, currentPrice, context);
|
|
12103
|
+
};
|
|
12104
|
+
this.getPositionLevels = async (backtest, symbol, context) => {
|
|
12105
|
+
this.loggerService.log("strategyCoreService getPositionLevels", {
|
|
12106
|
+
symbol,
|
|
12107
|
+
context,
|
|
12108
|
+
});
|
|
12109
|
+
await this.validate(context);
|
|
12110
|
+
return await this.strategyConnectionService.getPositionLevels(backtest, symbol, context);
|
|
12111
|
+
};
|
|
12112
|
+
this.getPositionPartials = async (backtest, symbol, context) => {
|
|
12113
|
+
this.loggerService.log("strategyCoreService getPositionPartials", {
|
|
12114
|
+
symbol,
|
|
12115
|
+
context,
|
|
12116
|
+
});
|
|
12117
|
+
await this.validate(context);
|
|
12118
|
+
return await this.strategyConnectionService.getPositionPartials(backtest, symbol, context);
|
|
12119
|
+
};
|
|
11658
12120
|
/**
|
|
11659
12121
|
* Retrieves the currently active scheduled signal for the symbol.
|
|
11660
12122
|
* If no scheduled signal exists, returns null.
|
|
@@ -28677,15 +29139,45 @@ async function getAggregatedTrades(symbol, limit) {
|
|
|
28677
29139
|
return await bt.exchangeConnectionService.getAggregatedTrades(symbol, limit);
|
|
28678
29140
|
}
|
|
28679
29141
|
|
|
29142
|
+
/**
|
|
29143
|
+
* Convert an absolute dollar amount to a percentage of the invested position cost.
|
|
29144
|
+
* Use the result as the `percent` argument to `commitPartialProfit` / `commitPartialLoss`.
|
|
29145
|
+
*
|
|
29146
|
+
* @param dollarAmount - Dollar value to close (e.g. 150)
|
|
29147
|
+
* @param investedCost - Total invested cost from `getPositionInvestedCost` (e.g. 300)
|
|
29148
|
+
* @returns Percentage of the position to close (0–100)
|
|
29149
|
+
*
|
|
29150
|
+
* @example
|
|
29151
|
+
* const percent = investedCostToPercent(150, 300); // 50
|
|
29152
|
+
* await commitPartialProfit("BTCUSDT", percent);
|
|
29153
|
+
*/
|
|
29154
|
+
const investedCostToPercent = (dollarAmount, investedCost) => {
|
|
29155
|
+
return (dollarAmount / investedCost) * 100;
|
|
29156
|
+
};
|
|
29157
|
+
|
|
28680
29158
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
28681
29159
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
28682
29160
|
const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
|
|
28683
29161
|
const PARTIAL_LOSS_METHOD_NAME = "strategy.commitPartialLoss";
|
|
29162
|
+
const PARTIAL_PROFIT_COST_METHOD_NAME = "strategy.commitPartialProfitCost";
|
|
29163
|
+
const PARTIAL_LOSS_COST_METHOD_NAME = "strategy.commitPartialLossCost";
|
|
28684
29164
|
const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
|
|
28685
29165
|
const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
|
|
28686
29166
|
const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
|
|
28687
29167
|
const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
|
|
28688
29168
|
const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
|
|
29169
|
+
const GET_TOTAL_PERCENT_CLOSED_METHOD_NAME = "strategy.getTotalPercentClosed";
|
|
29170
|
+
const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
|
|
29171
|
+
const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
|
|
29172
|
+
const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
|
|
29173
|
+
const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
|
|
29174
|
+
const GET_POSITION_AVERAGE_PRICE_METHOD_NAME = "strategy.getPositionAveragePrice";
|
|
29175
|
+
const GET_POSITION_INVESTED_COUNT_METHOD_NAME = "strategy.getPositionInvestedCount";
|
|
29176
|
+
const GET_POSITION_INVESTED_COST_METHOD_NAME = "strategy.getPositionInvestedCost";
|
|
29177
|
+
const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
|
|
29178
|
+
const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
|
|
29179
|
+
const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
|
|
29180
|
+
const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
|
|
28689
29181
|
/**
|
|
28690
29182
|
* Cancels the scheduled signal without stopping the strategy.
|
|
28691
29183
|
*
|
|
@@ -29077,6 +29569,372 @@ async function commitAverageBuy(symbol) {
|
|
|
29077
29569
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29078
29570
|
return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29079
29571
|
}
|
|
29572
|
+
/**
|
|
29573
|
+
* Returns the percentage of the position currently held (not closed).
|
|
29574
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
29575
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29576
|
+
*
|
|
29577
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29578
|
+
*
|
|
29579
|
+
* @param symbol - Trading pair symbol
|
|
29580
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
29581
|
+
*
|
|
29582
|
+
* @example
|
|
29583
|
+
* ```typescript
|
|
29584
|
+
* import { getTotalPercentClosed } from "backtest-kit";
|
|
29585
|
+
*
|
|
29586
|
+
* const heldPct = await getTotalPercentClosed("BTCUSDT");
|
|
29587
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
29588
|
+
* ```
|
|
29589
|
+
*/
|
|
29590
|
+
async function getTotalPercentClosed(symbol) {
|
|
29591
|
+
bt.loggerService.info(GET_TOTAL_PERCENT_CLOSED_METHOD_NAME, {
|
|
29592
|
+
symbol,
|
|
29593
|
+
});
|
|
29594
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29595
|
+
throw new Error("getTotalPercentClosed requires an execution context");
|
|
29596
|
+
}
|
|
29597
|
+
if (!MethodContextService.hasContext()) {
|
|
29598
|
+
throw new Error("getTotalPercentClosed requires a method context");
|
|
29599
|
+
}
|
|
29600
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29601
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29602
|
+
return await bt.strategyCoreService.getTotalPercentClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29603
|
+
}
|
|
29604
|
+
/**
|
|
29605
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
29606
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29607
|
+
*
|
|
29608
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29609
|
+
*
|
|
29610
|
+
* @param symbol - Trading pair symbol
|
|
29611
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
29612
|
+
*
|
|
29613
|
+
* @example
|
|
29614
|
+
* ```typescript
|
|
29615
|
+
* import { getTotalCostClosed } from "backtest-kit";
|
|
29616
|
+
*
|
|
29617
|
+
* const heldCost = await getTotalCostClosed("BTCUSDT");
|
|
29618
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
29619
|
+
* ```
|
|
29620
|
+
*/
|
|
29621
|
+
async function getTotalCostClosed(symbol) {
|
|
29622
|
+
bt.loggerService.info(GET_TOTAL_COST_CLOSED_METHOD_NAME, {
|
|
29623
|
+
symbol,
|
|
29624
|
+
});
|
|
29625
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29626
|
+
throw new Error("getTotalCostClosed requires an execution context");
|
|
29627
|
+
}
|
|
29628
|
+
if (!MethodContextService.hasContext()) {
|
|
29629
|
+
throw new Error("getTotalCostClosed requires a method context");
|
|
29630
|
+
}
|
|
29631
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29632
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29633
|
+
return await bt.strategyCoreService.getTotalCostClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29634
|
+
}
|
|
29635
|
+
/**
|
|
29636
|
+
* Returns the currently active pending signal for the strategy.
|
|
29637
|
+
* If no active signal exists, returns null.
|
|
29638
|
+
*
|
|
29639
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29640
|
+
*
|
|
29641
|
+
* @param symbol - Trading pair symbol
|
|
29642
|
+
* @returns Promise resolving to pending signal or null
|
|
29643
|
+
*
|
|
29644
|
+
* @example
|
|
29645
|
+
* ```typescript
|
|
29646
|
+
* import { getPendingSignal } from "backtest-kit";
|
|
29647
|
+
*
|
|
29648
|
+
* const pending = await getPendingSignal("BTCUSDT");
|
|
29649
|
+
* if (pending) {
|
|
29650
|
+
* console.log("Active signal:", pending.id);
|
|
29651
|
+
* }
|
|
29652
|
+
* ```
|
|
29653
|
+
*/
|
|
29654
|
+
async function getPendingSignal(symbol) {
|
|
29655
|
+
bt.loggerService.info(GET_PENDING_SIGNAL_METHOD_NAME, {
|
|
29656
|
+
symbol,
|
|
29657
|
+
});
|
|
29658
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29659
|
+
throw new Error("getPendingSignal requires an execution context");
|
|
29660
|
+
}
|
|
29661
|
+
if (!MethodContextService.hasContext()) {
|
|
29662
|
+
throw new Error("getPendingSignal requires a method context");
|
|
29663
|
+
}
|
|
29664
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29665
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29666
|
+
return await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29667
|
+
}
|
|
29668
|
+
/**
|
|
29669
|
+
* Returns the currently active scheduled signal for the strategy.
|
|
29670
|
+
* If no scheduled signal exists, returns null.
|
|
29671
|
+
*
|
|
29672
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29673
|
+
*
|
|
29674
|
+
* @param symbol - Trading pair symbol
|
|
29675
|
+
* @returns Promise resolving to scheduled signal or null
|
|
29676
|
+
*
|
|
29677
|
+
* @example
|
|
29678
|
+
* ```typescript
|
|
29679
|
+
* import { getScheduledSignal } from "backtest-kit";
|
|
29680
|
+
*
|
|
29681
|
+
* const scheduled = await getScheduledSignal("BTCUSDT");
|
|
29682
|
+
* if (scheduled) {
|
|
29683
|
+
* console.log("Scheduled signal:", scheduled.id);
|
|
29684
|
+
* }
|
|
29685
|
+
* ```
|
|
29686
|
+
*/
|
|
29687
|
+
async function getScheduledSignal(symbol) {
|
|
29688
|
+
bt.loggerService.info(GET_SCHEDULED_SIGNAL_METHOD_NAME, {
|
|
29689
|
+
symbol,
|
|
29690
|
+
});
|
|
29691
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29692
|
+
throw new Error("getScheduledSignal requires an execution context");
|
|
29693
|
+
}
|
|
29694
|
+
if (!MethodContextService.hasContext()) {
|
|
29695
|
+
throw new Error("getScheduledSignal requires a method context");
|
|
29696
|
+
}
|
|
29697
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29698
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29699
|
+
return await bt.strategyCoreService.getScheduledSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29700
|
+
}
|
|
29701
|
+
/**
|
|
29702
|
+
* Checks if breakeven threshold has been reached for the current pending signal.
|
|
29703
|
+
*
|
|
29704
|
+
* Returns true if price has moved far enough in profit direction to cover
|
|
29705
|
+
* transaction costs. Threshold is calculated as: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2
|
|
29706
|
+
*
|
|
29707
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29708
|
+
*
|
|
29709
|
+
* @param symbol - Trading pair symbol
|
|
29710
|
+
* @param currentPrice - Current market price to check against threshold
|
|
29711
|
+
* @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
|
|
29712
|
+
*
|
|
29713
|
+
* @example
|
|
29714
|
+
* ```typescript
|
|
29715
|
+
* import { getBreakeven, getAveragePrice } from "backtest-kit";
|
|
29716
|
+
*
|
|
29717
|
+
* const price = await getAveragePrice("BTCUSDT");
|
|
29718
|
+
* const canBreakeven = await getBreakeven("BTCUSDT", price);
|
|
29719
|
+
* if (canBreakeven) {
|
|
29720
|
+
* console.log("Breakeven available");
|
|
29721
|
+
* }
|
|
29722
|
+
* ```
|
|
29723
|
+
*/
|
|
29724
|
+
async function getBreakeven(symbol, currentPrice) {
|
|
29725
|
+
bt.loggerService.info(GET_BREAKEVEN_METHOD_NAME, {
|
|
29726
|
+
symbol,
|
|
29727
|
+
currentPrice,
|
|
29728
|
+
});
|
|
29729
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29730
|
+
throw new Error("getBreakeven requires an execution context");
|
|
29731
|
+
}
|
|
29732
|
+
if (!MethodContextService.hasContext()) {
|
|
29733
|
+
throw new Error("getBreakeven requires a method context");
|
|
29734
|
+
}
|
|
29735
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29736
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29737
|
+
return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29738
|
+
}
|
|
29739
|
+
async function getPositionAveragePrice(symbol) {
|
|
29740
|
+
bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, { symbol });
|
|
29741
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29742
|
+
throw new Error("getPositionAveragePrice requires an execution context");
|
|
29743
|
+
}
|
|
29744
|
+
if (!MethodContextService.hasContext()) {
|
|
29745
|
+
throw new Error("getPositionAveragePrice requires a method context");
|
|
29746
|
+
}
|
|
29747
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29748
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29749
|
+
return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29750
|
+
}
|
|
29751
|
+
async function getPositionInvestedCount(symbol) {
|
|
29752
|
+
bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, { symbol });
|
|
29753
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29754
|
+
throw new Error("getPositionInvestedCount requires an execution context");
|
|
29755
|
+
}
|
|
29756
|
+
if (!MethodContextService.hasContext()) {
|
|
29757
|
+
throw new Error("getPositionInvestedCount requires a method context");
|
|
29758
|
+
}
|
|
29759
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29760
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29761
|
+
return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29762
|
+
}
|
|
29763
|
+
async function getPositionInvestedCost(symbol) {
|
|
29764
|
+
bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, { symbol });
|
|
29765
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29766
|
+
throw new Error("getPositionInvestedCost requires an execution context");
|
|
29767
|
+
}
|
|
29768
|
+
if (!MethodContextService.hasContext()) {
|
|
29769
|
+
throw new Error("getPositionInvestedCost requires a method context");
|
|
29770
|
+
}
|
|
29771
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29772
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29773
|
+
return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29774
|
+
}
|
|
29775
|
+
async function getPositionPnlPercent(symbol) {
|
|
29776
|
+
bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
|
|
29777
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29778
|
+
throw new Error("getPositionPnlPercent requires an execution context");
|
|
29779
|
+
}
|
|
29780
|
+
if (!MethodContextService.hasContext()) {
|
|
29781
|
+
throw new Error("getPositionPnlPercent requires a method context");
|
|
29782
|
+
}
|
|
29783
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29784
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29785
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29786
|
+
return await bt.strategyCoreService.getPositionPnlPercent(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29787
|
+
}
|
|
29788
|
+
/**
|
|
29789
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
29790
|
+
*
|
|
29791
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
29792
|
+
* to a percentage of the invested position cost automatically.
|
|
29793
|
+
* Price must be moving toward take profit (in profit direction).
|
|
29794
|
+
*
|
|
29795
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29796
|
+
* Automatically fetches current price via getAveragePrice.
|
|
29797
|
+
*
|
|
29798
|
+
* @param symbol - Trading pair symbol
|
|
29799
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
29800
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
29801
|
+
*
|
|
29802
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
29803
|
+
* - LONG: currentPrice must be > priceOpen
|
|
29804
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
29805
|
+
*
|
|
29806
|
+
* @example
|
|
29807
|
+
* ```typescript
|
|
29808
|
+
* import { commitPartialProfitCost } from "backtest-kit";
|
|
29809
|
+
*
|
|
29810
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
29811
|
+
* const success = await commitPartialProfitCost("BTCUSDT", 150);
|
|
29812
|
+
* if (success) {
|
|
29813
|
+
* console.log('Partial profit executed');
|
|
29814
|
+
* }
|
|
29815
|
+
* ```
|
|
29816
|
+
*/
|
|
29817
|
+
async function commitPartialProfitCost(symbol, dollarAmount) {
|
|
29818
|
+
bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, { symbol, dollarAmount });
|
|
29819
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29820
|
+
throw new Error("commitPartialProfitCost requires an execution context");
|
|
29821
|
+
}
|
|
29822
|
+
if (!MethodContextService.hasContext()) {
|
|
29823
|
+
throw new Error("commitPartialProfitCost requires a method context");
|
|
29824
|
+
}
|
|
29825
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29826
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29827
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29828
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29829
|
+
if (investedCost === null)
|
|
29830
|
+
return false;
|
|
29831
|
+
const percentToClose = investedCostToPercent(dollarAmount, investedCost);
|
|
29832
|
+
return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
29833
|
+
}
|
|
29834
|
+
/**
|
|
29835
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
29836
|
+
*
|
|
29837
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
29838
|
+
* to a percentage of the invested position cost automatically.
|
|
29839
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
29840
|
+
*
|
|
29841
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29842
|
+
* Automatically fetches current price via getAveragePrice.
|
|
29843
|
+
*
|
|
29844
|
+
* @param symbol - Trading pair symbol
|
|
29845
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
29846
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
29847
|
+
*
|
|
29848
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
29849
|
+
* - LONG: currentPrice must be < priceOpen
|
|
29850
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
29851
|
+
*
|
|
29852
|
+
* @example
|
|
29853
|
+
* ```typescript
|
|
29854
|
+
* import { commitPartialLossCost } from "backtest-kit";
|
|
29855
|
+
*
|
|
29856
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
29857
|
+
* const success = await commitPartialLossCost("BTCUSDT", 100);
|
|
29858
|
+
* if (success) {
|
|
29859
|
+
* console.log('Partial loss executed');
|
|
29860
|
+
* }
|
|
29861
|
+
* ```
|
|
29862
|
+
*/
|
|
29863
|
+
async function commitPartialLossCost(symbol, dollarAmount) {
|
|
29864
|
+
bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, { symbol, dollarAmount });
|
|
29865
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29866
|
+
throw new Error("commitPartialLossCost requires an execution context");
|
|
29867
|
+
}
|
|
29868
|
+
if (!MethodContextService.hasContext()) {
|
|
29869
|
+
throw new Error("commitPartialLossCost requires a method context");
|
|
29870
|
+
}
|
|
29871
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29872
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29873
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29874
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29875
|
+
if (investedCost === null)
|
|
29876
|
+
return false;
|
|
29877
|
+
const percentToClose = investedCostToPercent(dollarAmount, investedCost);
|
|
29878
|
+
return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
29879
|
+
}
|
|
29880
|
+
async function getPositionPnlCost(symbol) {
|
|
29881
|
+
bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
|
|
29882
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29883
|
+
throw new Error("getPositionPnlCost requires an execution context");
|
|
29884
|
+
}
|
|
29885
|
+
if (!MethodContextService.hasContext()) {
|
|
29886
|
+
throw new Error("getPositionPnlCost requires a method context");
|
|
29887
|
+
}
|
|
29888
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29889
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29890
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29891
|
+
return await bt.strategyCoreService.getPositionPnlCost(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29892
|
+
}
|
|
29893
|
+
/**
|
|
29894
|
+
* Returns the list of DCA entry prices for the current pending signal.
|
|
29895
|
+
*
|
|
29896
|
+
* The first element is always the original priceOpen (initial entry).
|
|
29897
|
+
* Each subsequent element is a price added by commitAverageBuy().
|
|
29898
|
+
*
|
|
29899
|
+
* Returns null if no pending signal exists.
|
|
29900
|
+
* Returns a single-element array [priceOpen] if no DCA entries were made.
|
|
29901
|
+
*
|
|
29902
|
+
* @param symbol - Trading pair symbol
|
|
29903
|
+
* @returns Promise resolving to array of entry prices or null
|
|
29904
|
+
*
|
|
29905
|
+
* @example
|
|
29906
|
+
* ```typescript
|
|
29907
|
+
* import { getPositionLevels } from "backtest-kit";
|
|
29908
|
+
*
|
|
29909
|
+
* const levels = await getPositionLevels("BTCUSDT");
|
|
29910
|
+
* // No DCA: [43000]
|
|
29911
|
+
* // One DCA: [43000, 42000]
|
|
29912
|
+
* ```
|
|
29913
|
+
*/
|
|
29914
|
+
async function getPositionLevels(symbol) {
|
|
29915
|
+
bt.loggerService.info(GET_POSITION_LEVELS_METHOD_NAME, { symbol });
|
|
29916
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29917
|
+
throw new Error("getPositionLevels requires an execution context");
|
|
29918
|
+
}
|
|
29919
|
+
if (!MethodContextService.hasContext()) {
|
|
29920
|
+
throw new Error("getPositionLevels requires a method context");
|
|
29921
|
+
}
|
|
29922
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29923
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29924
|
+
return await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29925
|
+
}
|
|
29926
|
+
async function getPositionPartials(symbol) {
|
|
29927
|
+
bt.loggerService.info(GET_POSITION_PARTIALS_METHOD_NAME, { symbol });
|
|
29928
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29929
|
+
throw new Error("getPositionPartials requires an execution context");
|
|
29930
|
+
}
|
|
29931
|
+
if (!MethodContextService.hasContext()) {
|
|
29932
|
+
throw new Error("getPositionPartials requires a method context");
|
|
29933
|
+
}
|
|
29934
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29935
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29936
|
+
return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29937
|
+
}
|
|
29080
29938
|
|
|
29081
29939
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
29082
29940
|
/**
|
|
@@ -30264,13 +31122,24 @@ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
|
30264
31122
|
const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
|
|
30265
31123
|
const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
|
|
30266
31124
|
const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
|
|
31125
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPercentClosed";
|
|
31126
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
|
|
30267
31127
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
30268
31128
|
const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
|
|
31129
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "BacktestUtils.getPositionAveragePrice";
|
|
31130
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "BacktestUtils.getPositionInvestedCount";
|
|
31131
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST = "BacktestUtils.getPositionInvestedCost";
|
|
31132
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT = "BacktestUtils.getPositionPnlPercent";
|
|
31133
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnlCost";
|
|
31134
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
|
|
31135
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
|
|
30269
31136
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
30270
31137
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
30271
31138
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
30272
31139
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
30273
31140
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
31141
|
+
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
|
|
31142
|
+
const BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST = "BacktestUtils.commitPartialLossCost";
|
|
30274
31143
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
|
|
30275
31144
|
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
|
|
30276
31145
|
const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
|
|
@@ -30665,6 +31534,71 @@ class BacktestUtils {
|
|
|
30665
31534
|
}
|
|
30666
31535
|
return await bt.strategyCoreService.getPendingSignal(true, symbol, context);
|
|
30667
31536
|
};
|
|
31537
|
+
/**
|
|
31538
|
+
* Returns the percentage of the position currently held (not closed).
|
|
31539
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
31540
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31541
|
+
*
|
|
31542
|
+
* @param symbol - Trading pair symbol
|
|
31543
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31544
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
31545
|
+
*
|
|
31546
|
+
* @example
|
|
31547
|
+
* ```typescript
|
|
31548
|
+
* const heldPct = await Backtest.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31549
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
31550
|
+
* ```
|
|
31551
|
+
*/
|
|
31552
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
31553
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
31554
|
+
symbol,
|
|
31555
|
+
context,
|
|
31556
|
+
});
|
|
31557
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31558
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31559
|
+
{
|
|
31560
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31561
|
+
riskName &&
|
|
31562
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31563
|
+
riskList &&
|
|
31564
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31565
|
+
actions &&
|
|
31566
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31567
|
+
}
|
|
31568
|
+
return await bt.strategyCoreService.getTotalPercentClosed(true, symbol, context);
|
|
31569
|
+
};
|
|
31570
|
+
/**
|
|
31571
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
31572
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31573
|
+
*
|
|
31574
|
+
* @param symbol - Trading pair symbol
|
|
31575
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31576
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
31577
|
+
*
|
|
31578
|
+
* @example
|
|
31579
|
+
* ```typescript
|
|
31580
|
+
* const heldCost = await Backtest.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31581
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
31582
|
+
* ```
|
|
31583
|
+
*/
|
|
31584
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
31585
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
31586
|
+
symbol,
|
|
31587
|
+
context,
|
|
31588
|
+
});
|
|
31589
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31590
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31591
|
+
{
|
|
31592
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31593
|
+
riskName &&
|
|
31594
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31595
|
+
riskList &&
|
|
31596
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31597
|
+
actions &&
|
|
31598
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31599
|
+
}
|
|
31600
|
+
return await bt.strategyCoreService.getTotalCostClosed(true, symbol, context);
|
|
31601
|
+
};
|
|
30668
31602
|
/**
|
|
30669
31603
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
30670
31604
|
* If no scheduled signal exists, returns null.
|
|
@@ -30742,6 +31676,134 @@ class BacktestUtils {
|
|
|
30742
31676
|
}
|
|
30743
31677
|
return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
|
|
30744
31678
|
};
|
|
31679
|
+
this.getPositionAveragePrice = async (symbol, context) => {
|
|
31680
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
|
|
31681
|
+
symbol,
|
|
31682
|
+
context,
|
|
31683
|
+
});
|
|
31684
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31685
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31686
|
+
{
|
|
31687
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31688
|
+
riskName &&
|
|
31689
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31690
|
+
riskList &&
|
|
31691
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
31692
|
+
actions &&
|
|
31693
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
31694
|
+
}
|
|
31695
|
+
return await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
|
|
31696
|
+
};
|
|
31697
|
+
this.getPositionInvestedCount = async (symbol, context) => {
|
|
31698
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
|
|
31699
|
+
symbol,
|
|
31700
|
+
context,
|
|
31701
|
+
});
|
|
31702
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31703
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31704
|
+
{
|
|
31705
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31706
|
+
riskName &&
|
|
31707
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31708
|
+
riskList &&
|
|
31709
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
31710
|
+
actions &&
|
|
31711
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
31712
|
+
}
|
|
31713
|
+
return await bt.strategyCoreService.getPositionInvestedCount(true, symbol, context);
|
|
31714
|
+
};
|
|
31715
|
+
this.getPositionInvestedCost = async (symbol, context) => {
|
|
31716
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST, {
|
|
31717
|
+
symbol,
|
|
31718
|
+
context,
|
|
31719
|
+
});
|
|
31720
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31721
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31722
|
+
{
|
|
31723
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31724
|
+
riskName &&
|
|
31725
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31726
|
+
riskList &&
|
|
31727
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
31728
|
+
actions &&
|
|
31729
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
31730
|
+
}
|
|
31731
|
+
return await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
31732
|
+
};
|
|
31733
|
+
this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
|
|
31734
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
|
|
31735
|
+
symbol,
|
|
31736
|
+
currentPrice,
|
|
31737
|
+
context,
|
|
31738
|
+
});
|
|
31739
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31740
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31741
|
+
{
|
|
31742
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31743
|
+
riskName &&
|
|
31744
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31745
|
+
riskList &&
|
|
31746
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
31747
|
+
actions &&
|
|
31748
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
31749
|
+
}
|
|
31750
|
+
return await bt.strategyCoreService.getPositionPnlPercent(true, symbol, currentPrice, context);
|
|
31751
|
+
};
|
|
31752
|
+
this.getPositionPnlCost = async (symbol, currentPrice, context) => {
|
|
31753
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST, {
|
|
31754
|
+
symbol,
|
|
31755
|
+
currentPrice,
|
|
31756
|
+
context,
|
|
31757
|
+
});
|
|
31758
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31759
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31760
|
+
{
|
|
31761
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31762
|
+
riskName &&
|
|
31763
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31764
|
+
riskList &&
|
|
31765
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
31766
|
+
actions &&
|
|
31767
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
31768
|
+
}
|
|
31769
|
+
return await bt.strategyCoreService.getPositionPnlCost(true, symbol, currentPrice, context);
|
|
31770
|
+
};
|
|
31771
|
+
this.getPositionLevels = async (symbol, context) => {
|
|
31772
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_LEVELS, {
|
|
31773
|
+
symbol,
|
|
31774
|
+
context,
|
|
31775
|
+
});
|
|
31776
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31777
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31778
|
+
{
|
|
31779
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31780
|
+
riskName &&
|
|
31781
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31782
|
+
riskList &&
|
|
31783
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
|
|
31784
|
+
actions &&
|
|
31785
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
|
|
31786
|
+
}
|
|
31787
|
+
return await bt.strategyCoreService.getPositionLevels(true, symbol, context);
|
|
31788
|
+
};
|
|
31789
|
+
this.getPositionPartials = async (symbol, context) => {
|
|
31790
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS, {
|
|
31791
|
+
symbol,
|
|
31792
|
+
context,
|
|
31793
|
+
});
|
|
31794
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31795
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31796
|
+
{
|
|
31797
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31798
|
+
riskName &&
|
|
31799
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31800
|
+
riskList &&
|
|
31801
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
31802
|
+
actions &&
|
|
31803
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
31804
|
+
}
|
|
31805
|
+
return await bt.strategyCoreService.getPositionPartials(true, symbol, context);
|
|
31806
|
+
};
|
|
30745
31807
|
/**
|
|
30746
31808
|
* Stops the strategy from generating new signals.
|
|
30747
31809
|
*
|
|
@@ -30963,6 +32025,114 @@ class BacktestUtils {
|
|
|
30963
32025
|
}
|
|
30964
32026
|
return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
30965
32027
|
};
|
|
32028
|
+
/**
|
|
32029
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
32030
|
+
*
|
|
32031
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
32032
|
+
* to a percentage of the invested position cost automatically.
|
|
32033
|
+
* Price must be moving toward take profit (in profit direction).
|
|
32034
|
+
*
|
|
32035
|
+
* @param symbol - Trading pair symbol
|
|
32036
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
32037
|
+
* @param currentPrice - Current market price for this partial close
|
|
32038
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
32039
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
32040
|
+
*
|
|
32041
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
32042
|
+
* - LONG: currentPrice must be > priceOpen
|
|
32043
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
32044
|
+
*
|
|
32045
|
+
* @example
|
|
32046
|
+
* ```typescript
|
|
32047
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
32048
|
+
* const success = await Backtest.commitPartialProfitCost("BTCUSDT", 150, 45000, {
|
|
32049
|
+
* exchangeName: "binance",
|
|
32050
|
+
* frameName: "frame1",
|
|
32051
|
+
* strategyName: "my-strategy"
|
|
32052
|
+
* });
|
|
32053
|
+
* if (success) {
|
|
32054
|
+
* console.log('Partial profit executed');
|
|
32055
|
+
* }
|
|
32056
|
+
* ```
|
|
32057
|
+
*/
|
|
32058
|
+
this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
32059
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST, {
|
|
32060
|
+
symbol,
|
|
32061
|
+
dollarAmount,
|
|
32062
|
+
currentPrice,
|
|
32063
|
+
context,
|
|
32064
|
+
});
|
|
32065
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32066
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32067
|
+
{
|
|
32068
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32069
|
+
riskName &&
|
|
32070
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32071
|
+
riskList &&
|
|
32072
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
32073
|
+
actions &&
|
|
32074
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
32075
|
+
}
|
|
32076
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
32077
|
+
if (investedCost === null)
|
|
32078
|
+
return false;
|
|
32079
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
32080
|
+
return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
|
|
32081
|
+
};
|
|
32082
|
+
/**
|
|
32083
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
32084
|
+
*
|
|
32085
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
32086
|
+
* to a percentage of the invested position cost automatically.
|
|
32087
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
32088
|
+
*
|
|
32089
|
+
* @param symbol - Trading pair symbol
|
|
32090
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
32091
|
+
* @param currentPrice - Current market price for this partial close
|
|
32092
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
32093
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
32094
|
+
*
|
|
32095
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
32096
|
+
* - LONG: currentPrice must be < priceOpen
|
|
32097
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
32098
|
+
*
|
|
32099
|
+
* @example
|
|
32100
|
+
* ```typescript
|
|
32101
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
32102
|
+
* const success = await Backtest.commitPartialLossCost("BTCUSDT", 100, 38000, {
|
|
32103
|
+
* exchangeName: "binance",
|
|
32104
|
+
* frameName: "frame1",
|
|
32105
|
+
* strategyName: "my-strategy"
|
|
32106
|
+
* });
|
|
32107
|
+
* if (success) {
|
|
32108
|
+
* console.log('Partial loss executed');
|
|
32109
|
+
* }
|
|
32110
|
+
* ```
|
|
32111
|
+
*/
|
|
32112
|
+
this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
32113
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST, {
|
|
32114
|
+
symbol,
|
|
32115
|
+
dollarAmount,
|
|
32116
|
+
currentPrice,
|
|
32117
|
+
context,
|
|
32118
|
+
});
|
|
32119
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32120
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32121
|
+
{
|
|
32122
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32123
|
+
riskName &&
|
|
32124
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32125
|
+
riskList &&
|
|
32126
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
32127
|
+
actions &&
|
|
32128
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
32129
|
+
}
|
|
32130
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
32131
|
+
if (investedCost === null)
|
|
32132
|
+
return false;
|
|
32133
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
32134
|
+
return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
32135
|
+
};
|
|
30966
32136
|
/**
|
|
30967
32137
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
30968
32138
|
*
|
|
@@ -31380,13 +32550,24 @@ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
|
31380
32550
|
const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
|
|
31381
32551
|
const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
|
|
31382
32552
|
const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
|
|
32553
|
+
const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClosed";
|
|
32554
|
+
const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
|
|
31383
32555
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
31384
32556
|
const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
|
|
32557
|
+
const LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "LiveUtils.getPositionAveragePrice";
|
|
32558
|
+
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "LiveUtils.getPositionInvestedCount";
|
|
32559
|
+
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST = "LiveUtils.getPositionInvestedCost";
|
|
32560
|
+
const LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT = "LiveUtils.getPositionPnlPercent";
|
|
32561
|
+
const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
|
|
32562
|
+
const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
|
|
32563
|
+
const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
|
|
31385
32564
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
31386
32565
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
31387
32566
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
31388
32567
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
31389
32568
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
32569
|
+
const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
|
|
32570
|
+
const LIVE_METHOD_NAME_PARTIAL_LOSS_COST = "LiveUtils.commitPartialLossCost";
|
|
31390
32571
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
|
|
31391
32572
|
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
|
|
31392
32573
|
const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
|
|
@@ -31750,6 +32931,73 @@ class LiveUtils {
|
|
|
31750
32931
|
frameName: "",
|
|
31751
32932
|
});
|
|
31752
32933
|
};
|
|
32934
|
+
/**
|
|
32935
|
+
* Returns the percentage of the position currently held (not closed).
|
|
32936
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
32937
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32938
|
+
*
|
|
32939
|
+
* @param symbol - Trading pair symbol
|
|
32940
|
+
* @param context - Context with strategyName and exchangeName
|
|
32941
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
32942
|
+
*
|
|
32943
|
+
* @example
|
|
32944
|
+
* ```typescript
|
|
32945
|
+
* const heldPct = await Live.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32946
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
32947
|
+
* ```
|
|
32948
|
+
*/
|
|
32949
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
32950
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
32951
|
+
symbol,
|
|
32952
|
+
context,
|
|
32953
|
+
});
|
|
32954
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32955
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32956
|
+
{
|
|
32957
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32958
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32959
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32960
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32961
|
+
}
|
|
32962
|
+
return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
|
|
32963
|
+
strategyName: context.strategyName,
|
|
32964
|
+
exchangeName: context.exchangeName,
|
|
32965
|
+
frameName: "",
|
|
32966
|
+
});
|
|
32967
|
+
};
|
|
32968
|
+
/**
|
|
32969
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
32970
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32971
|
+
*
|
|
32972
|
+
* @param symbol - Trading pair symbol
|
|
32973
|
+
* @param context - Context with strategyName and exchangeName
|
|
32974
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
32975
|
+
*
|
|
32976
|
+
* @example
|
|
32977
|
+
* ```typescript
|
|
32978
|
+
* const heldCost = await Live.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32979
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
32980
|
+
* ```
|
|
32981
|
+
*/
|
|
32982
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
32983
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
32984
|
+
symbol,
|
|
32985
|
+
context,
|
|
32986
|
+
});
|
|
32987
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32988
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32989
|
+
{
|
|
32990
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32991
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32992
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
32993
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
32994
|
+
}
|
|
32995
|
+
return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
|
|
32996
|
+
strategyName: context.strategyName,
|
|
32997
|
+
exchangeName: context.exchangeName,
|
|
32998
|
+
frameName: "",
|
|
32999
|
+
});
|
|
33000
|
+
};
|
|
31753
33001
|
/**
|
|
31754
33002
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
31755
33003
|
* If no scheduled signal exists, returns null.
|
|
@@ -31828,6 +33076,118 @@ class LiveUtils {
|
|
|
31828
33076
|
frameName: "",
|
|
31829
33077
|
});
|
|
31830
33078
|
};
|
|
33079
|
+
this.getPositionAveragePrice = async (symbol, context) => {
|
|
33080
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, { symbol, context });
|
|
33081
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33082
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33083
|
+
{
|
|
33084
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33085
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33086
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
33087
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
33088
|
+
}
|
|
33089
|
+
return await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
|
|
33090
|
+
strategyName: context.strategyName,
|
|
33091
|
+
exchangeName: context.exchangeName,
|
|
33092
|
+
frameName: "",
|
|
33093
|
+
});
|
|
33094
|
+
};
|
|
33095
|
+
this.getPositionInvestedCount = async (symbol, context) => {
|
|
33096
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, { symbol, context });
|
|
33097
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33098
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33099
|
+
{
|
|
33100
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33101
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33102
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
33103
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
33104
|
+
}
|
|
33105
|
+
return await bt.strategyCoreService.getPositionInvestedCount(false, symbol, {
|
|
33106
|
+
strategyName: context.strategyName,
|
|
33107
|
+
exchangeName: context.exchangeName,
|
|
33108
|
+
frameName: "",
|
|
33109
|
+
});
|
|
33110
|
+
};
|
|
33111
|
+
this.getPositionInvestedCost = async (symbol, context) => {
|
|
33112
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, { symbol, context });
|
|
33113
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33114
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33115
|
+
{
|
|
33116
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33117
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33118
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
33119
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
33120
|
+
}
|
|
33121
|
+
return await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33122
|
+
strategyName: context.strategyName,
|
|
33123
|
+
exchangeName: context.exchangeName,
|
|
33124
|
+
frameName: "",
|
|
33125
|
+
});
|
|
33126
|
+
};
|
|
33127
|
+
this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
|
|
33128
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, { symbol, currentPrice, context });
|
|
33129
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33130
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33131
|
+
{
|
|
33132
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33133
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33134
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
33135
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
33136
|
+
}
|
|
33137
|
+
return await bt.strategyCoreService.getPositionPnlPercent(false, symbol, currentPrice, {
|
|
33138
|
+
strategyName: context.strategyName,
|
|
33139
|
+
exchangeName: context.exchangeName,
|
|
33140
|
+
frameName: "",
|
|
33141
|
+
});
|
|
33142
|
+
};
|
|
33143
|
+
this.getPositionPnlCost = async (symbol, currentPrice, context) => {
|
|
33144
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, { symbol, currentPrice, context });
|
|
33145
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33146
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33147
|
+
{
|
|
33148
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33149
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33150
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
33151
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
33152
|
+
}
|
|
33153
|
+
return await bt.strategyCoreService.getPositionPnlCost(false, symbol, currentPrice, {
|
|
33154
|
+
strategyName: context.strategyName,
|
|
33155
|
+
exchangeName: context.exchangeName,
|
|
33156
|
+
frameName: "",
|
|
33157
|
+
});
|
|
33158
|
+
};
|
|
33159
|
+
this.getPositionLevels = async (symbol, context) => {
|
|
33160
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, { symbol, context });
|
|
33161
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33162
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33163
|
+
{
|
|
33164
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33165
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33166
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
|
|
33167
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
|
|
33168
|
+
}
|
|
33169
|
+
return await bt.strategyCoreService.getPositionLevels(false, symbol, {
|
|
33170
|
+
strategyName: context.strategyName,
|
|
33171
|
+
exchangeName: context.exchangeName,
|
|
33172
|
+
frameName: "",
|
|
33173
|
+
});
|
|
33174
|
+
};
|
|
33175
|
+
this.getPositionPartials = async (symbol, context) => {
|
|
33176
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, { symbol, context });
|
|
33177
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33178
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33179
|
+
{
|
|
33180
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33181
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33182
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
33183
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
33184
|
+
}
|
|
33185
|
+
return await bt.strategyCoreService.getPositionPartials(false, symbol, {
|
|
33186
|
+
strategyName: context.strategyName,
|
|
33187
|
+
exchangeName: context.exchangeName,
|
|
33188
|
+
frameName: "",
|
|
33189
|
+
});
|
|
33190
|
+
};
|
|
31831
33191
|
/**
|
|
31832
33192
|
* Stops the strategy from generating new signals.
|
|
31833
33193
|
*
|
|
@@ -32046,6 +33406,112 @@ class LiveUtils {
|
|
|
32046
33406
|
frameName: "",
|
|
32047
33407
|
});
|
|
32048
33408
|
};
|
|
33409
|
+
/**
|
|
33410
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
33411
|
+
*
|
|
33412
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
33413
|
+
* to a percentage of the invested position cost automatically.
|
|
33414
|
+
* Price must be moving toward take profit (in profit direction).
|
|
33415
|
+
*
|
|
33416
|
+
* @param symbol - Trading pair symbol
|
|
33417
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
33418
|
+
* @param currentPrice - Current market price for this partial close
|
|
33419
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
33420
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
33421
|
+
*
|
|
33422
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
33423
|
+
* - LONG: currentPrice must be > priceOpen
|
|
33424
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
33425
|
+
*
|
|
33426
|
+
* @example
|
|
33427
|
+
* ```typescript
|
|
33428
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
33429
|
+
* const success = await Live.commitPartialProfitCost("BTCUSDT", 150, 45000, {
|
|
33430
|
+
* exchangeName: "binance",
|
|
33431
|
+
* strategyName: "my-strategy"
|
|
33432
|
+
* });
|
|
33433
|
+
* if (success) {
|
|
33434
|
+
* console.log('Partial profit executed');
|
|
33435
|
+
* }
|
|
33436
|
+
* ```
|
|
33437
|
+
*/
|
|
33438
|
+
this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
33439
|
+
bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, { symbol, dollarAmount, currentPrice, context });
|
|
33440
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33441
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33442
|
+
{
|
|
33443
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33444
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33445
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
33446
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
33447
|
+
}
|
|
33448
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33449
|
+
strategyName: context.strategyName,
|
|
33450
|
+
exchangeName: context.exchangeName,
|
|
33451
|
+
frameName: "",
|
|
33452
|
+
});
|
|
33453
|
+
if (investedCost === null)
|
|
33454
|
+
return false;
|
|
33455
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
33456
|
+
return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
|
|
33457
|
+
strategyName: context.strategyName,
|
|
33458
|
+
exchangeName: context.exchangeName,
|
|
33459
|
+
frameName: "",
|
|
33460
|
+
});
|
|
33461
|
+
};
|
|
33462
|
+
/**
|
|
33463
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
33464
|
+
*
|
|
33465
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
33466
|
+
* to a percentage of the invested position cost automatically.
|
|
33467
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
33468
|
+
*
|
|
33469
|
+
* @param symbol - Trading pair symbol
|
|
33470
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
33471
|
+
* @param currentPrice - Current market price for this partial close
|
|
33472
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
33473
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
33474
|
+
*
|
|
33475
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
33476
|
+
* - LONG: currentPrice must be < priceOpen
|
|
33477
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
33478
|
+
*
|
|
33479
|
+
* @example
|
|
33480
|
+
* ```typescript
|
|
33481
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
33482
|
+
* const success = await Live.commitPartialLossCost("BTCUSDT", 100, 38000, {
|
|
33483
|
+
* exchangeName: "binance",
|
|
33484
|
+
* strategyName: "my-strategy"
|
|
33485
|
+
* });
|
|
33486
|
+
* if (success) {
|
|
33487
|
+
* console.log('Partial loss executed');
|
|
33488
|
+
* }
|
|
33489
|
+
* ```
|
|
33490
|
+
*/
|
|
33491
|
+
this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
33492
|
+
bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, { symbol, dollarAmount, currentPrice, context });
|
|
33493
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33494
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33495
|
+
{
|
|
33496
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33497
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33498
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
33499
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
33500
|
+
}
|
|
33501
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33502
|
+
strategyName: context.strategyName,
|
|
33503
|
+
exchangeName: context.exchangeName,
|
|
33504
|
+
frameName: "",
|
|
33505
|
+
});
|
|
33506
|
+
if (investedCost === null)
|
|
33507
|
+
return false;
|
|
33508
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
33509
|
+
return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
|
|
33510
|
+
strategyName: context.strategyName,
|
|
33511
|
+
exchangeName: context.exchangeName,
|
|
33512
|
+
frameName: "",
|
|
33513
|
+
});
|
|
33514
|
+
};
|
|
32049
33515
|
/**
|
|
32050
33516
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
32051
33517
|
*
|
|
@@ -39566,4 +41032,25 @@ const set = (object, path, value) => {
|
|
|
39566
41032
|
}
|
|
39567
41033
|
};
|
|
39568
41034
|
|
|
39569
|
-
|
|
41035
|
+
/**
|
|
41036
|
+
* Calculate the percentage difference between two numbers.
|
|
41037
|
+
* @param {number} a - The first number.
|
|
41038
|
+
* @param {number} b - The second number.
|
|
41039
|
+
* @returns {number} The percentage difference between the two numbers.
|
|
41040
|
+
*/
|
|
41041
|
+
const percentDiff = (a = 1, b = 2) => {
|
|
41042
|
+
const result = 100 / (Math.min(a, b) / Math.max(a, b)) - 100;
|
|
41043
|
+
return Number.isFinite(result) ? result : 100;
|
|
41044
|
+
};
|
|
41045
|
+
|
|
41046
|
+
/**
|
|
41047
|
+
* Calculate the percentage change from yesterday's value to today's value.
|
|
41048
|
+
* @param {number} yesterdayValue - The value from yesterday.
|
|
41049
|
+
* @param {number} todayValue - The value from today.
|
|
41050
|
+
* @returns {number} The percentage change from yesterday to today.
|
|
41051
|
+
*/
|
|
41052
|
+
const percentValue = (yesterdayValue, todayValue) => {
|
|
41053
|
+
return yesterdayValue / todayValue - 1;
|
|
41054
|
+
};
|
|
41055
|
+
|
|
41056
|
+
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionAveragePrice, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentValue, roundTicks, set, setColumns, setConfig, setLogger, shutdown, stopStrategy, toProfitLossDto, validate, waitForCandle, warmCandles };
|