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.cjs
CHANGED
|
@@ -463,6 +463,14 @@ const GLOBAL_CONFIG = {
|
|
|
463
463
|
* Default: true (mutex locking enabled for candle fetching)
|
|
464
464
|
*/
|
|
465
465
|
CC_ENABLE_CANDLE_FETCH_MUTEX: true,
|
|
466
|
+
/**
|
|
467
|
+
* Enables DCA (Dollar-Cost Averaging) logic even if antirecord is not broken.
|
|
468
|
+
* Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
|
|
469
|
+
* 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.
|
|
470
|
+
*
|
|
471
|
+
* Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
|
|
472
|
+
*/
|
|
473
|
+
CC_ENABLE_DCA_EVERYWHERE: false,
|
|
466
474
|
};
|
|
467
475
|
const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
|
|
468
476
|
|
|
@@ -3194,75 +3202,90 @@ class ExchangeConnectionService {
|
|
|
3194
3202
|
}
|
|
3195
3203
|
}
|
|
3196
3204
|
|
|
3205
|
+
const COST_BASIS_PER_ENTRY$3 = 100;
|
|
3197
3206
|
/**
|
|
3198
3207
|
* Returns the effective entry price for price calculations.
|
|
3199
3208
|
*
|
|
3200
|
-
*
|
|
3201
|
-
* the simple arithmetic mean of all entry prices (DCA average).
|
|
3202
|
-
* Otherwise returns the original signal.priceOpen.
|
|
3209
|
+
* Uses harmonic mean (correct for fixed-dollar DCA: $100 per entry).
|
|
3203
3210
|
*
|
|
3204
|
-
*
|
|
3205
|
-
*
|
|
3206
|
-
* effective averaged price returned by this function.
|
|
3211
|
+
* When partial closes exist, replays the partial sequence to reconstruct
|
|
3212
|
+
* the running cost basis at each partial — no extra stored fields needed.
|
|
3207
3213
|
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3214
|
+
* Cost basis replay:
|
|
3215
|
+
* costBasis starts at 0
|
|
3216
|
+
* for each partial[i]:
|
|
3217
|
+
* newEntries = entryCountAtClose[i] - entryCountAtClose[i-1] (or entryCountAtClose[0] for i=0)
|
|
3218
|
+
* costBasis += newEntries × $100 ← add DCA entries up to this partial
|
|
3219
|
+
* positionCostBasisAtClose[i] = costBasis ← snapshot BEFORE close
|
|
3220
|
+
* costBasis × = (1 - percent[i] / 100) ← reduce after close
|
|
3221
|
+
*
|
|
3222
|
+
* @param signal - Signal row
|
|
3223
|
+
* @returns Effective entry price for PNL calculations
|
|
3210
3224
|
*/
|
|
3211
3225
|
const getEffectivePriceOpen = (signal) => {
|
|
3212
|
-
if (signal._entry
|
|
3213
|
-
return signal.
|
|
3214
|
-
|
|
3215
|
-
|
|
3226
|
+
if (!signal._entry || signal._entry.length === 0)
|
|
3227
|
+
return signal.priceOpen;
|
|
3228
|
+
const entries = signal._entry;
|
|
3229
|
+
const partials = signal._partial ?? [];
|
|
3230
|
+
// No partial exits — pure harmonic mean of all entries
|
|
3231
|
+
if (partials.length === 0) {
|
|
3232
|
+
return harmonicMean(entries.map((e) => e.price));
|
|
3233
|
+
}
|
|
3234
|
+
// Replay cost basis through all partials to get snapshot at the last one
|
|
3235
|
+
let costBasis = 0;
|
|
3236
|
+
for (let i = 0; i < partials.length; i++) {
|
|
3237
|
+
const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
|
|
3238
|
+
const newEntryCount = partials[i].entryCountAtClose - prevCount;
|
|
3239
|
+
costBasis += newEntryCount * COST_BASIS_PER_ENTRY$3;
|
|
3240
|
+
// costBasis is now positionCostBasisAtClose for partials[i]
|
|
3241
|
+
if (i < partials.length - 1) {
|
|
3242
|
+
costBasis *= 1 - partials[i].percent / 100;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
const lastPartial = partials[partials.length - 1];
|
|
3246
|
+
// Dollar cost basis remaining after the last partial close
|
|
3247
|
+
const remainingCostBasis = costBasis * (1 - lastPartial.percent / 100);
|
|
3248
|
+
// Coins remaining from the old position
|
|
3249
|
+
const oldCoins = remainingCostBasis / lastPartial.effectivePrice;
|
|
3250
|
+
// New DCA entries added AFTER the last partial close
|
|
3251
|
+
const newEntries = entries.slice(lastPartial.entryCountAtClose);
|
|
3252
|
+
// Coins from new DCA entries (each costs $100)
|
|
3253
|
+
const newCoins = newEntries.reduce((sum, e) => sum + 100 / e.price, 0);
|
|
3254
|
+
const totalCoins = oldCoins + newCoins;
|
|
3255
|
+
if (totalCoins === 0)
|
|
3256
|
+
return lastPartial.effectivePrice;
|
|
3257
|
+
const totalCost = remainingCostBasis + newEntries.length * 100;
|
|
3258
|
+
return totalCost / totalCoins;
|
|
3259
|
+
};
|
|
3260
|
+
const harmonicMean = (prices) => {
|
|
3261
|
+
if (prices.length === 0)
|
|
3262
|
+
return 0;
|
|
3263
|
+
return prices.length / prices.reduce((sum, p) => sum + 1 / p, 0);
|
|
3216
3264
|
};
|
|
3217
3265
|
|
|
3266
|
+
const COST_BASIS_PER_ENTRY$2 = 100;
|
|
3218
3267
|
/**
|
|
3219
3268
|
* Calculates profit/loss for a closed signal with slippage and fees.
|
|
3220
3269
|
*
|
|
3221
3270
|
* For signals with partial closes:
|
|
3222
|
-
* -
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
3228
|
-
*
|
|
3229
|
-
*
|
|
3230
|
-
*
|
|
3231
|
-
*
|
|
3232
|
-
*
|
|
3233
|
-
*
|
|
3234
|
-
*
|
|
3271
|
+
* - Weights are calculated by ACTUAL DOLLAR VALUE of each partial relative to total invested.
|
|
3272
|
+
* This correctly handles DCA entries that occur before or after partial closes.
|
|
3273
|
+
*
|
|
3274
|
+
* Cost basis is reconstructed by replaying the partial sequence via entryCountAtClose + percent:
|
|
3275
|
+
* costBasis = 0
|
|
3276
|
+
* for each partial[i]:
|
|
3277
|
+
* costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
|
|
3278
|
+
* partialDollarValue[i] = (percent[i] / 100) × costBasis
|
|
3279
|
+
* weight[i] = partialDollarValue[i] / totalInvested
|
|
3280
|
+
* costBasis *= (1 - percent[i] / 100)
|
|
3281
|
+
*
|
|
3282
|
+
* Fee structure:
|
|
3283
|
+
* - Open fee: CC_PERCENT_FEE (charged once)
|
|
3284
|
+
* - Close fee: CC_PERCENT_FEE × weight × (closeWithSlip / openWithSlip) per partial/remaining
|
|
3235
3285
|
*
|
|
3236
3286
|
* @param signal - Closed signal with position details and optional partial history
|
|
3237
3287
|
* @param priceClose - Actual close price at final exit
|
|
3238
3288
|
* @returns PNL data with percentage and prices
|
|
3239
|
-
*
|
|
3240
|
-
* @example
|
|
3241
|
-
* ```typescript
|
|
3242
|
-
* // Signal without partial closes
|
|
3243
|
-
* const pnl = toProfitLossDto(
|
|
3244
|
-
* {
|
|
3245
|
-
* position: "long",
|
|
3246
|
-
* priceOpen: 100,
|
|
3247
|
-
* },
|
|
3248
|
-
* 110 // close at +10%
|
|
3249
|
-
* );
|
|
3250
|
-
* console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
|
|
3251
|
-
*
|
|
3252
|
-
* // Signal with partial closes
|
|
3253
|
-
* const pnlPartial = toProfitLossDto(
|
|
3254
|
-
* {
|
|
3255
|
-
* position: "long",
|
|
3256
|
-
* priceOpen: 100,
|
|
3257
|
-
* _partial: [
|
|
3258
|
-
* { type: "profit", percent: 30, price: 120 }, // +20% on 30%
|
|
3259
|
-
* { type: "profit", percent: 40, price: 115 }, // +15% on 40%
|
|
3260
|
-
* ],
|
|
3261
|
-
* },
|
|
3262
|
-
* 105 // final close at +5% for remaining 30%
|
|
3263
|
-
* );
|
|
3264
|
-
* // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
|
|
3265
|
-
* ```
|
|
3266
3289
|
*/
|
|
3267
3290
|
const toProfitLossDto = (signal, priceClose) => {
|
|
3268
3291
|
const priceOpen = getEffectivePriceOpen(signal);
|
|
@@ -3271,47 +3294,65 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3271
3294
|
let totalWeightedPnl = 0;
|
|
3272
3295
|
// Open fee is paid once for the whole position
|
|
3273
3296
|
let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
|
|
3274
|
-
//
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3297
|
+
// Total invested capital = number of DCA entries × $100 per entry
|
|
3298
|
+
const totalInvested = signal._entry ? signal._entry.length * 100 : 100;
|
|
3299
|
+
let closedDollarValue = 0;
|
|
3300
|
+
// Running cost basis — replayed from entryCountAtClose + percent
|
|
3301
|
+
let costBasis = 0;
|
|
3278
3302
|
// Calculate PNL for each partial close
|
|
3279
|
-
for (
|
|
3280
|
-
const
|
|
3303
|
+
for (let i = 0; i < signal._partial.length; i++) {
|
|
3304
|
+
const partial = signal._partial[i];
|
|
3305
|
+
// Add DCA entries that existed at this partial but not at the previous one
|
|
3306
|
+
const prevCount = i === 0 ? 0 : signal._partial[i - 1].entryCountAtClose;
|
|
3307
|
+
const newEntryCount = partial.entryCountAtClose - prevCount;
|
|
3308
|
+
costBasis += newEntryCount * COST_BASIS_PER_ENTRY$2;
|
|
3309
|
+
// Real dollar value sold in this partial
|
|
3310
|
+
const partialDollarValue = (partial.percent / 100) * costBasis;
|
|
3311
|
+
// Weight relative to total invested capital
|
|
3312
|
+
const weight = partialDollarValue / totalInvested;
|
|
3313
|
+
closedDollarValue += partialDollarValue;
|
|
3314
|
+
// Reduce cost basis after close
|
|
3315
|
+
costBasis *= 1 - partial.percent / 100;
|
|
3316
|
+
// Use the effective entry price snapshot captured at the time of this partial close
|
|
3317
|
+
const priceOpenWithSlippage = signal.position === "long"
|
|
3318
|
+
? partial.effectivePrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3319
|
+
: partial.effectivePrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3281
3320
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3282
|
-
? partial.
|
|
3283
|
-
: partial.
|
|
3284
|
-
// Calculate PNL for this partial
|
|
3321
|
+
? partial.currentPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3322
|
+
: partial.currentPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3285
3323
|
const partialPnl = signal.position === "long"
|
|
3286
3324
|
? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
|
|
3287
3325
|
: ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
const
|
|
3300
|
-
|
|
3326
|
+
totalWeightedPnl += weight * partialPnl;
|
|
3327
|
+
// Close fee proportional to real dollar weight
|
|
3328
|
+
totalFees +=
|
|
3329
|
+
GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3330
|
+
weight *
|
|
3331
|
+
(priceCloseWithSlippage / priceOpenWithSlippage);
|
|
3332
|
+
}
|
|
3333
|
+
if (closedDollarValue > totalInvested + 0.001) {
|
|
3334
|
+
throw new Error(`Partial closes dollar value (${closedDollarValue.toFixed(4)}) exceeds total invested (${totalInvested}) — signal id: ${signal.id}`);
|
|
3335
|
+
}
|
|
3336
|
+
// Remaining position
|
|
3337
|
+
const remainingDollarValue = totalInvested - closedDollarValue;
|
|
3338
|
+
const remainingWeight = remainingDollarValue / totalInvested;
|
|
3339
|
+
if (remainingWeight > 0) {
|
|
3340
|
+
// Use current effective price — reflects all DCA including post-partial entries
|
|
3341
|
+
const remainingOpenWithSlippage = signal.position === "long"
|
|
3342
|
+
? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3343
|
+
: priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3301
3344
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3302
3345
|
? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3303
3346
|
: priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3304
|
-
// Calculate PNL for remaining
|
|
3305
3347
|
const remainingPnl = signal.position === "long"
|
|
3306
|
-
? ((priceCloseWithSlippage -
|
|
3307
|
-
: ((
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3348
|
+
? ((priceCloseWithSlippage - remainingOpenWithSlippage) / remainingOpenWithSlippage) * 100
|
|
3349
|
+
: ((remainingOpenWithSlippage - priceCloseWithSlippage) / remainingOpenWithSlippage) * 100;
|
|
3350
|
+
totalWeightedPnl += remainingWeight * remainingPnl;
|
|
3351
|
+
totalFees +=
|
|
3352
|
+
GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3353
|
+
remainingWeight *
|
|
3354
|
+
(priceCloseWithSlippage / remainingOpenWithSlippage);
|
|
3312
3355
|
}
|
|
3313
|
-
// Subtract total fees from weighted PNL
|
|
3314
|
-
// totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
|
|
3315
3356
|
const pnlPercentage = totalWeightedPnl - totalFees;
|
|
3316
3357
|
return {
|
|
3317
3358
|
pnlPercentage,
|
|
@@ -3323,33 +3364,24 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3323
3364
|
let priceOpenWithSlippage;
|
|
3324
3365
|
let priceCloseWithSlippage;
|
|
3325
3366
|
if (signal.position === "long") {
|
|
3326
|
-
// LONG: покупаем дороже, продаем дешевле
|
|
3327
3367
|
priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3328
3368
|
priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3329
3369
|
}
|
|
3330
3370
|
else {
|
|
3331
|
-
// SHORT: продаем дешевле, покупаем дороже
|
|
3332
3371
|
priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3333
3372
|
priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3334
3373
|
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3374
|
+
const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3375
|
+
(1 + priceCloseWithSlippage / priceOpenWithSlippage);
|
|
3337
3376
|
let pnlPercentage;
|
|
3338
3377
|
if (signal.position === "long") {
|
|
3339
|
-
// LONG: прибыль при росте цены
|
|
3340
3378
|
pnlPercentage =
|
|
3341
|
-
((priceCloseWithSlippage - priceOpenWithSlippage) /
|
|
3342
|
-
priceOpenWithSlippage) *
|
|
3343
|
-
100;
|
|
3379
|
+
((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3344
3380
|
}
|
|
3345
3381
|
else {
|
|
3346
|
-
// SHORT: прибыль при падении цены
|
|
3347
3382
|
pnlPercentage =
|
|
3348
|
-
((priceOpenWithSlippage - priceCloseWithSlippage) /
|
|
3349
|
-
priceOpenWithSlippage) *
|
|
3350
|
-
100;
|
|
3383
|
+
((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3351
3384
|
}
|
|
3352
|
-
// Вычитаем комиссии
|
|
3353
3385
|
pnlPercentage -= totalFee;
|
|
3354
3386
|
return {
|
|
3355
3387
|
pnlPercentage,
|
|
@@ -3405,6 +3437,54 @@ const toPlainString = (content) => {
|
|
|
3405
3437
|
return text.trim();
|
|
3406
3438
|
};
|
|
3407
3439
|
|
|
3440
|
+
const COST_BASIS_PER_ENTRY$1 = 100;
|
|
3441
|
+
/**
|
|
3442
|
+
* Returns the total closed state of a position using cost-basis replay.
|
|
3443
|
+
*
|
|
3444
|
+
* Correctly accounts for DCA entries added between partial closes via averageBuy().
|
|
3445
|
+
* Simple percent summation (sum of _partial[i].percent) is INCORRECT when averageBuy()
|
|
3446
|
+
* is called between partials — this function uses the same cost-basis replay as
|
|
3447
|
+
* toProfitLossDto to compute the true dollar-weighted closed fraction.
|
|
3448
|
+
*
|
|
3449
|
+
* Cost-basis replay:
|
|
3450
|
+
* costBasis = 0
|
|
3451
|
+
* for each partial[i]:
|
|
3452
|
+
* costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
|
|
3453
|
+
* closedDollar += (percent[i] / 100) × costBasis
|
|
3454
|
+
* costBasis ×= (1 - percent[i] / 100)
|
|
3455
|
+
* // then add entries added AFTER the last partial
|
|
3456
|
+
* costBasis += (currentEntryCount - lastPartialEntryCount) × $100
|
|
3457
|
+
*
|
|
3458
|
+
* @param signal - Signal row with _partial and _entry arrays
|
|
3459
|
+
* @returns Object with totalClosedPercent (0–100+) and remainingCostBasis (dollar value still open)
|
|
3460
|
+
*/
|
|
3461
|
+
const getTotalClosed = (signal) => {
|
|
3462
|
+
const partials = signal._partial ?? [];
|
|
3463
|
+
const currentEntryCount = signal._entry?.length ?? 1;
|
|
3464
|
+
const totalInvested = currentEntryCount * COST_BASIS_PER_ENTRY$1;
|
|
3465
|
+
if (partials.length === 0) {
|
|
3466
|
+
return {
|
|
3467
|
+
totalClosedPercent: 0,
|
|
3468
|
+
remainingCostBasis: totalInvested,
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
let costBasis = 0;
|
|
3472
|
+
let closedDollarValue = 0;
|
|
3473
|
+
for (let i = 0; i < partials.length; i++) {
|
|
3474
|
+
const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
|
|
3475
|
+
costBasis += (partials[i].entryCountAtClose - prevCount) * COST_BASIS_PER_ENTRY$1;
|
|
3476
|
+
closedDollarValue += (partials[i].percent / 100) * costBasis;
|
|
3477
|
+
costBasis *= 1 - partials[i].percent / 100;
|
|
3478
|
+
}
|
|
3479
|
+
// Add entries added AFTER the last partial (not yet accounted for in the loop)
|
|
3480
|
+
const lastEntryCount = partials[partials.length - 1].entryCountAtClose;
|
|
3481
|
+
costBasis += (currentEntryCount - lastEntryCount) * COST_BASIS_PER_ENTRY$1;
|
|
3482
|
+
return {
|
|
3483
|
+
totalClosedPercent: totalInvested > 0 ? (closedDollarValue / totalInvested) * 100 : 0,
|
|
3484
|
+
remainingCostBasis: costBasis,
|
|
3485
|
+
};
|
|
3486
|
+
};
|
|
3487
|
+
|
|
3408
3488
|
/**
|
|
3409
3489
|
* Wraps a function to execute it outside of the current execution context if one exists.
|
|
3410
3490
|
*
|
|
@@ -3447,6 +3527,19 @@ const beginTime = (run) => (...args) => {
|
|
|
3447
3527
|
return fn();
|
|
3448
3528
|
};
|
|
3449
3529
|
|
|
3530
|
+
/**
|
|
3531
|
+
* Retrieves the current timestamp for debugging purposes.
|
|
3532
|
+
* 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.
|
|
3533
|
+
* Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
|
|
3534
|
+
* @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
|
|
3535
|
+
*/
|
|
3536
|
+
const getDebugTimestamp = () => {
|
|
3537
|
+
if (ExecutionContextService.hasContext()) {
|
|
3538
|
+
return bt.executionContextService.context.when.getTime();
|
|
3539
|
+
}
|
|
3540
|
+
return undefined;
|
|
3541
|
+
};
|
|
3542
|
+
|
|
3450
3543
|
const INTERVAL_MINUTES$6 = {
|
|
3451
3544
|
"1m": 1,
|
|
3452
3545
|
"3m": 3,
|
|
@@ -3455,6 +3548,7 @@ const INTERVAL_MINUTES$6 = {
|
|
|
3455
3548
|
"30m": 30,
|
|
3456
3549
|
"1h": 60,
|
|
3457
3550
|
};
|
|
3551
|
+
const COST_BASIS_PER_ENTRY = 100;
|
|
3458
3552
|
/**
|
|
3459
3553
|
* Mock value for scheduled signal pendingAt timestamp.
|
|
3460
3554
|
* Used to indicate that the actual pendingAt will be set upon activation.
|
|
@@ -3710,8 +3804,8 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
|
|
|
3710
3804
|
const TO_PUBLIC_SIGNAL = (signal) => {
|
|
3711
3805
|
const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
|
|
3712
3806
|
const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
|
|
3713
|
-
const partialExecuted =
|
|
3714
|
-
? signal
|
|
3807
|
+
const partialExecuted = "_partial" in signal
|
|
3808
|
+
? getTotalClosed(signal).totalClosedPercent
|
|
3715
3809
|
: 0;
|
|
3716
3810
|
const totalEntries = ("_entry" in signal && Array.isArray(signal._entry))
|
|
3717
3811
|
? signal._entry.length
|
|
@@ -4056,7 +4150,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4056
4150
|
scheduledAt: currentTime,
|
|
4057
4151
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
4058
4152
|
_isScheduled: false,
|
|
4059
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4153
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
4060
4154
|
};
|
|
4061
4155
|
// Валидируем сигнал перед возвратом
|
|
4062
4156
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4078,7 +4172,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4078
4172
|
scheduledAt: currentTime,
|
|
4079
4173
|
pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
|
|
4080
4174
|
_isScheduled: true,
|
|
4081
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4175
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
4082
4176
|
};
|
|
4083
4177
|
// Валидируем сигнал перед возвратом
|
|
4084
4178
|
VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
|
|
@@ -4096,7 +4190,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4096
4190
|
scheduledAt: currentTime,
|
|
4097
4191
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
4098
4192
|
_isScheduled: false,
|
|
4099
|
-
_entry: [{ price: currentPrice }],
|
|
4193
|
+
_entry: [{ price: currentPrice, debugTimestamp: currentTime }],
|
|
4100
4194
|
};
|
|
4101
4195
|
// Валидируем сигнал перед возвратом
|
|
4102
4196
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4170,37 +4264,39 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4170
4264
|
// Initialize partial array if not present
|
|
4171
4265
|
if (!signal._partial)
|
|
4172
4266
|
signal._partial = [];
|
|
4173
|
-
//
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
.
|
|
4180
|
-
const totalClosed = tpClosed + slClosed;
|
|
4181
|
-
// Check if would exceed 100% total closed
|
|
4182
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4183
|
-
if (newTotalClosed > 100) {
|
|
4184
|
-
self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
|
|
4267
|
+
// Check if would exceed 100% total closed (dollar-basis, DCA-aware)
|
|
4268
|
+
const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
|
|
4269
|
+
const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
4270
|
+
const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
|
|
4271
|
+
const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
|
|
4272
|
+
if (newTotalClosedDollar > totalInvested) {
|
|
4273
|
+
self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed (dollar basis), skipping", {
|
|
4185
4274
|
signalId: signal.id,
|
|
4186
|
-
|
|
4275
|
+
totalClosedPercent,
|
|
4276
|
+
remainingCostBasis,
|
|
4187
4277
|
percentToClose,
|
|
4188
|
-
|
|
4278
|
+
newPartialDollar,
|
|
4279
|
+
totalInvested,
|
|
4189
4280
|
});
|
|
4190
4281
|
return false;
|
|
4191
4282
|
}
|
|
4283
|
+
// Capture effective entry price at the moment of partial close (for DCA-aware PNL)
|
|
4284
|
+
const effectivePrice = getEffectivePriceOpen(signal);
|
|
4285
|
+
const entryCountAtClose = signal._entry ? signal._entry.length : 1;
|
|
4192
4286
|
// Add new partial close entry
|
|
4193
4287
|
signal._partial.push({
|
|
4194
4288
|
type: "profit",
|
|
4195
4289
|
percent: percentToClose,
|
|
4196
|
-
|
|
4290
|
+
entryCountAtClose,
|
|
4291
|
+
currentPrice,
|
|
4292
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4293
|
+
effectivePrice,
|
|
4197
4294
|
});
|
|
4198
4295
|
self.params.logger.info("PARTIAL_PROFIT_FN executed", {
|
|
4199
4296
|
signalId: signal.id,
|
|
4200
4297
|
percentClosed: percentToClose,
|
|
4201
|
-
|
|
4298
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4202
4299
|
currentPrice,
|
|
4203
|
-
tpClosed: tpClosed + percentToClose,
|
|
4204
4300
|
});
|
|
4205
4301
|
return true;
|
|
4206
4302
|
};
|
|
@@ -4208,37 +4304,39 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4208
4304
|
// Initialize partial array if not present
|
|
4209
4305
|
if (!signal._partial)
|
|
4210
4306
|
signal._partial = [];
|
|
4211
|
-
//
|
|
4212
|
-
const
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
.
|
|
4218
|
-
const totalClosed = tpClosed + slClosed;
|
|
4219
|
-
// Check if would exceed 100% total closed
|
|
4220
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4221
|
-
if (newTotalClosed > 100) {
|
|
4222
|
-
self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
|
|
4307
|
+
// Check if would exceed 100% total closed (dollar-basis, DCA-aware)
|
|
4308
|
+
const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
|
|
4309
|
+
const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
4310
|
+
const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
|
|
4311
|
+
const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
|
|
4312
|
+
if (newTotalClosedDollar > totalInvested) {
|
|
4313
|
+
self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed (dollar basis), skipping", {
|
|
4223
4314
|
signalId: signal.id,
|
|
4224
|
-
|
|
4315
|
+
totalClosedPercent,
|
|
4316
|
+
remainingCostBasis,
|
|
4225
4317
|
percentToClose,
|
|
4226
|
-
|
|
4318
|
+
newPartialDollar,
|
|
4319
|
+
totalInvested,
|
|
4227
4320
|
});
|
|
4228
4321
|
return false;
|
|
4229
4322
|
}
|
|
4323
|
+
// Capture effective entry price at the moment of partial close (for DCA-aware PNL)
|
|
4324
|
+
const effectivePrice = getEffectivePriceOpen(signal);
|
|
4325
|
+
const entryCountAtClose = signal._entry ? signal._entry.length : 1;
|
|
4230
4326
|
// Add new partial close entry
|
|
4231
4327
|
signal._partial.push({
|
|
4232
4328
|
type: "loss",
|
|
4233
4329
|
percent: percentToClose,
|
|
4234
|
-
|
|
4330
|
+
currentPrice,
|
|
4331
|
+
entryCountAtClose,
|
|
4332
|
+
effectivePrice,
|
|
4333
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4235
4334
|
});
|
|
4236
4335
|
self.params.logger.warn("PARTIAL_LOSS_FN executed", {
|
|
4237
4336
|
signalId: signal.id,
|
|
4238
4337
|
percentClosed: percentToClose,
|
|
4239
|
-
|
|
4338
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4240
4339
|
currentPrice,
|
|
4241
|
-
slClosed: slClosed + percentToClose,
|
|
4242
4340
|
});
|
|
4243
4341
|
return true;
|
|
4244
4342
|
};
|
|
@@ -4632,12 +4730,12 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
|
|
|
4632
4730
|
const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
4633
4731
|
// Ensure _entry is initialized (handles signals loaded from disk without _entry)
|
|
4634
4732
|
if (!signal._entry || signal._entry.length === 0) {
|
|
4635
|
-
signal._entry = [{ price: signal.priceOpen }];
|
|
4733
|
+
signal._entry = [{ price: signal.priceOpen, debugTimestamp: getDebugTimestamp() }];
|
|
4636
4734
|
}
|
|
4637
4735
|
const lastEntry = signal._entry[signal._entry.length - 1];
|
|
4638
4736
|
if (signal.position === "long") {
|
|
4639
4737
|
// LONG: averaging down = currentPrice must be strictly lower than last entry
|
|
4640
|
-
if (currentPrice >= lastEntry.price) {
|
|
4738
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= lastEntry.price) {
|
|
4641
4739
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
|
|
4642
4740
|
signalId: signal.id,
|
|
4643
4741
|
position: signal.position,
|
|
@@ -4650,7 +4748,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4650
4748
|
}
|
|
4651
4749
|
else {
|
|
4652
4750
|
// SHORT: averaging down = currentPrice must be strictly higher than last entry
|
|
4653
|
-
if (currentPrice <= lastEntry.price) {
|
|
4751
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= lastEntry.price) {
|
|
4654
4752
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
|
|
4655
4753
|
signalId: signal.id,
|
|
4656
4754
|
position: signal.position,
|
|
@@ -4661,7 +4759,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4661
4759
|
return false;
|
|
4662
4760
|
}
|
|
4663
4761
|
}
|
|
4664
|
-
signal._entry.push({ price: currentPrice });
|
|
4762
|
+
signal._entry.push({ price: currentPrice, debugTimestamp: getDebugTimestamp() });
|
|
4665
4763
|
self.params.logger.info("AVERAGE_BUY_FN executed", {
|
|
4666
4764
|
signalId: signal.id,
|
|
4667
4765
|
position: signal.position,
|
|
@@ -6126,6 +6224,183 @@ class ClientStrategy {
|
|
|
6126
6224
|
});
|
|
6127
6225
|
return this._isStopped;
|
|
6128
6226
|
}
|
|
6227
|
+
/**
|
|
6228
|
+
* Returns how much of the position is still held, as a percentage of totalInvested.
|
|
6229
|
+
*
|
|
6230
|
+
* Uses dollar-basis cost-basis replay (DCA-aware).
|
|
6231
|
+
* 100% means nothing was closed yet. Decreases with each partial close.
|
|
6232
|
+
*
|
|
6233
|
+
* Example: 1 entry $100, partialProfit(30%) → returns 70
|
|
6234
|
+
* Example: 2 entries $200, partialProfit(50%) → returns 50
|
|
6235
|
+
*
|
|
6236
|
+
* Returns 100 if no pending signal or no partial closes.
|
|
6237
|
+
*
|
|
6238
|
+
* @param symbol - Trading pair symbol
|
|
6239
|
+
* @returns Promise resolving to held percentage (0–100)
|
|
6240
|
+
*/
|
|
6241
|
+
async getTotalPercentClosed(symbol) {
|
|
6242
|
+
this.params.logger.debug("ClientStrategy getTotalPercentClosed", { symbol });
|
|
6243
|
+
if (!this._pendingSignal) {
|
|
6244
|
+
return null;
|
|
6245
|
+
}
|
|
6246
|
+
const { totalClosedPercent } = getTotalClosed(this._pendingSignal);
|
|
6247
|
+
return 100 - totalClosedPercent;
|
|
6248
|
+
}
|
|
6249
|
+
/**
|
|
6250
|
+
* Returns how many dollars of cost basis are still held (not yet closed by partials).
|
|
6251
|
+
*
|
|
6252
|
+
* Equal to remainingCostBasis from getTotalClosed.
|
|
6253
|
+
* Full position open: equals totalInvested (entries × $100).
|
|
6254
|
+
* Decreases with each partial close, increases with each averageBuy().
|
|
6255
|
+
*
|
|
6256
|
+
* Example: 1 entry $100, no partials → returns 100
|
|
6257
|
+
* Example: 1 entry $100, partialProfit(30%) → returns 70
|
|
6258
|
+
* Example: 2 entries $200, partialProfit(50%) → returns 100
|
|
6259
|
+
*
|
|
6260
|
+
* Returns totalInvested if no pending signal or no partial closes.
|
|
6261
|
+
*
|
|
6262
|
+
* @param symbol - Trading pair symbol
|
|
6263
|
+
* @returns Promise resolving to held cost basis in dollars
|
|
6264
|
+
*/
|
|
6265
|
+
async getTotalCostClosed(symbol) {
|
|
6266
|
+
this.params.logger.debug("ClientStrategy getTotalCostClosed", { symbol });
|
|
6267
|
+
if (!this._pendingSignal) {
|
|
6268
|
+
return null;
|
|
6269
|
+
}
|
|
6270
|
+
const { remainingCostBasis } = getTotalClosed(this._pendingSignal);
|
|
6271
|
+
return remainingCostBasis;
|
|
6272
|
+
}
|
|
6273
|
+
/**
|
|
6274
|
+
* Returns the effective (DCA-averaged) entry price for the current pending signal.
|
|
6275
|
+
*
|
|
6276
|
+
* This is the harmonic mean of all _entry prices, which is the correct
|
|
6277
|
+
* cost-basis price used in all PNL calculations.
|
|
6278
|
+
* With no DCA entries, equals the original priceOpen.
|
|
6279
|
+
*
|
|
6280
|
+
* Returns null if no pending signal exists.
|
|
6281
|
+
*
|
|
6282
|
+
* @param symbol - Trading pair symbol
|
|
6283
|
+
* @returns Promise resolving to effective entry price or null
|
|
6284
|
+
*/
|
|
6285
|
+
async getPositionAveragePrice(symbol) {
|
|
6286
|
+
this.params.logger.debug("ClientStrategy getPositionAveragePrice", { symbol });
|
|
6287
|
+
if (!this._pendingSignal) {
|
|
6288
|
+
return null;
|
|
6289
|
+
}
|
|
6290
|
+
return getEffectivePriceOpen(this._pendingSignal);
|
|
6291
|
+
}
|
|
6292
|
+
/**
|
|
6293
|
+
* Returns the number of DCA entries made for the current pending signal.
|
|
6294
|
+
*
|
|
6295
|
+
* 1 = original entry only (no DCA).
|
|
6296
|
+
* Increases by 1 with each successful commitAverageBuy().
|
|
6297
|
+
*
|
|
6298
|
+
* Returns null if no pending signal exists.
|
|
6299
|
+
*
|
|
6300
|
+
* @param symbol - Trading pair symbol
|
|
6301
|
+
* @returns Promise resolving to entry count or null
|
|
6302
|
+
*/
|
|
6303
|
+
async getPositionInvestedCount(symbol) {
|
|
6304
|
+
this.params.logger.debug("ClientStrategy getPositionInvestedCount", { symbol });
|
|
6305
|
+
if (!this._pendingSignal) {
|
|
6306
|
+
return null;
|
|
6307
|
+
}
|
|
6308
|
+
return this._pendingSignal._entry?.length ?? 1;
|
|
6309
|
+
}
|
|
6310
|
+
/**
|
|
6311
|
+
* Returns the total invested cost basis in dollars for the current pending signal.
|
|
6312
|
+
*
|
|
6313
|
+
* Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
|
|
6314
|
+
* 1 entry = $100, 2 entries = $200, etc.
|
|
6315
|
+
*
|
|
6316
|
+
* Returns null if no pending signal exists.
|
|
6317
|
+
*
|
|
6318
|
+
* @param symbol - Trading pair symbol
|
|
6319
|
+
* @returns Promise resolving to total invested cost in dollars or null
|
|
6320
|
+
*/
|
|
6321
|
+
async getPositionInvestedCost(symbol) {
|
|
6322
|
+
this.params.logger.debug("ClientStrategy getPositionInvestedCost", { symbol });
|
|
6323
|
+
if (!this._pendingSignal) {
|
|
6324
|
+
return null;
|
|
6325
|
+
}
|
|
6326
|
+
return (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
6327
|
+
}
|
|
6328
|
+
/**
|
|
6329
|
+
* Returns the unrealized PNL percentage for the current pending signal at currentPrice.
|
|
6330
|
+
*
|
|
6331
|
+
* Accounts for partial closes, DCA entries, slippage and fees
|
|
6332
|
+
* (delegates to toProfitLossDto).
|
|
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 pnlPercentage or null
|
|
6339
|
+
*/
|
|
6340
|
+
async getPositionPnlPercent(symbol, currentPrice) {
|
|
6341
|
+
this.params.logger.debug("ClientStrategy getPositionPnlPercent", { symbol, currentPrice });
|
|
6342
|
+
if (!this._pendingSignal) {
|
|
6343
|
+
return null;
|
|
6344
|
+
}
|
|
6345
|
+
const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
|
|
6346
|
+
return pnl.pnlPercentage;
|
|
6347
|
+
}
|
|
6348
|
+
/**
|
|
6349
|
+
* Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
|
|
6350
|
+
*
|
|
6351
|
+
* Calculated as: pnlPercentage / 100 × totalInvestedCost
|
|
6352
|
+
* Accounts for partial closes, DCA entries, slippage and fees.
|
|
6353
|
+
*
|
|
6354
|
+
* Returns null if no pending signal exists.
|
|
6355
|
+
*
|
|
6356
|
+
* @param symbol - Trading pair symbol
|
|
6357
|
+
* @param currentPrice - Current market price
|
|
6358
|
+
* @returns Promise resolving to pnl in dollars or null
|
|
6359
|
+
*/
|
|
6360
|
+
async getPositionPnlCost(symbol, currentPrice) {
|
|
6361
|
+
this.params.logger.debug("ClientStrategy getPositionPnlCost", { symbol, currentPrice });
|
|
6362
|
+
if (!this._pendingSignal) {
|
|
6363
|
+
return null;
|
|
6364
|
+
}
|
|
6365
|
+
const totalInvested = (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
|
|
6366
|
+
const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
|
|
6367
|
+
return (pnl.pnlPercentage / 100) * totalInvested;
|
|
6368
|
+
}
|
|
6369
|
+
/**
|
|
6370
|
+
* Returns the list of DCA entry prices for the current pending signal.
|
|
6371
|
+
*
|
|
6372
|
+
* The first element is always the original priceOpen (initial entry).
|
|
6373
|
+
* Each subsequent element is a price added by commitAverageBuy().
|
|
6374
|
+
*
|
|
6375
|
+
* Returns null if no pending signal exists.
|
|
6376
|
+
* Returns a single-element array [priceOpen] if no DCA entries were made.
|
|
6377
|
+
*
|
|
6378
|
+
* @param symbol - Trading pair symbol
|
|
6379
|
+
* @returns Promise resolving to array of entry prices or null
|
|
6380
|
+
*
|
|
6381
|
+
* @example
|
|
6382
|
+
* // No DCA: [43000]
|
|
6383
|
+
* // One DCA: [43000, 42000]
|
|
6384
|
+
* // Two DCA: [43000, 42000, 41500]
|
|
6385
|
+
*/
|
|
6386
|
+
async getPositionLevels(symbol) {
|
|
6387
|
+
this.params.logger.debug("ClientStrategy getPositionLevels", { symbol });
|
|
6388
|
+
if (!this._pendingSignal) {
|
|
6389
|
+
return null;
|
|
6390
|
+
}
|
|
6391
|
+
const entries = this._pendingSignal._entry;
|
|
6392
|
+
if (!entries || entries.length === 0) {
|
|
6393
|
+
return [this._pendingSignal.priceOpen];
|
|
6394
|
+
}
|
|
6395
|
+
return entries.map((e) => e.price);
|
|
6396
|
+
}
|
|
6397
|
+
async getPositionPartials(symbol) {
|
|
6398
|
+
this.params.logger.debug("ClientStrategy getPositionPartials", { symbol });
|
|
6399
|
+
if (!this._pendingSignal) {
|
|
6400
|
+
return null;
|
|
6401
|
+
}
|
|
6402
|
+
return this._pendingSignal._partial ?? [];
|
|
6403
|
+
}
|
|
6129
6404
|
/**
|
|
6130
6405
|
* Performs a single tick of strategy execution.
|
|
6131
6406
|
*
|
|
@@ -7586,14 +7861,6 @@ class ClientStrategy {
|
|
|
7586
7861
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
7587
7862
|
throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
7588
7863
|
}
|
|
7589
|
-
// Reject if any partial closes have already been executed
|
|
7590
|
-
if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
|
|
7591
|
-
this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
|
|
7592
|
-
symbol,
|
|
7593
|
-
partialCount: this._pendingSignal._partial.length,
|
|
7594
|
-
});
|
|
7595
|
-
return false;
|
|
7596
|
-
}
|
|
7597
7864
|
// Execute averaging logic
|
|
7598
7865
|
const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
|
|
7599
7866
|
if (!result) {
|
|
@@ -8272,6 +8539,108 @@ class StrategyConnectionService {
|
|
|
8272
8539
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8273
8540
|
return await strategy.getPendingSignal(symbol);
|
|
8274
8541
|
};
|
|
8542
|
+
/**
|
|
8543
|
+
* Returns the percentage of the position currently held (not closed).
|
|
8544
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
8545
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8546
|
+
*
|
|
8547
|
+
* @param backtest - Whether running in backtest mode
|
|
8548
|
+
* @param symbol - Trading pair symbol
|
|
8549
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8550
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
8551
|
+
*/
|
|
8552
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
8553
|
+
this.loggerService.log("strategyConnectionService getTotalPercentClosed", {
|
|
8554
|
+
symbol,
|
|
8555
|
+
context,
|
|
8556
|
+
backtest,
|
|
8557
|
+
});
|
|
8558
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8559
|
+
return await strategy.getTotalPercentClosed(symbol);
|
|
8560
|
+
};
|
|
8561
|
+
/**
|
|
8562
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
8563
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8564
|
+
*
|
|
8565
|
+
* @param backtest - Whether running in backtest mode
|
|
8566
|
+
* @param symbol - Trading pair symbol
|
|
8567
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8568
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
8569
|
+
*/
|
|
8570
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
8571
|
+
this.loggerService.log("strategyConnectionService getTotalCostClosed", {
|
|
8572
|
+
symbol,
|
|
8573
|
+
context,
|
|
8574
|
+
backtest,
|
|
8575
|
+
});
|
|
8576
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8577
|
+
return await strategy.getTotalCostClosed(symbol);
|
|
8578
|
+
};
|
|
8579
|
+
this.getPositionAveragePrice = async (backtest, symbol, context) => {
|
|
8580
|
+
this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
|
|
8581
|
+
symbol,
|
|
8582
|
+
context,
|
|
8583
|
+
backtest,
|
|
8584
|
+
});
|
|
8585
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8586
|
+
return await strategy.getPositionAveragePrice(symbol);
|
|
8587
|
+
};
|
|
8588
|
+
this.getPositionInvestedCount = async (backtest, symbol, context) => {
|
|
8589
|
+
this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
|
|
8590
|
+
symbol,
|
|
8591
|
+
context,
|
|
8592
|
+
backtest,
|
|
8593
|
+
});
|
|
8594
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8595
|
+
return await strategy.getPositionInvestedCount(symbol);
|
|
8596
|
+
};
|
|
8597
|
+
this.getPositionInvestedCost = async (backtest, symbol, context) => {
|
|
8598
|
+
this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
|
|
8599
|
+
symbol,
|
|
8600
|
+
context,
|
|
8601
|
+
backtest,
|
|
8602
|
+
});
|
|
8603
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8604
|
+
return await strategy.getPositionInvestedCost(symbol);
|
|
8605
|
+
};
|
|
8606
|
+
this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
|
|
8607
|
+
this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
|
|
8608
|
+
symbol,
|
|
8609
|
+
currentPrice,
|
|
8610
|
+
context,
|
|
8611
|
+
backtest,
|
|
8612
|
+
});
|
|
8613
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8614
|
+
return await strategy.getPositionPnlPercent(symbol, currentPrice);
|
|
8615
|
+
};
|
|
8616
|
+
this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
|
|
8617
|
+
this.loggerService.log("strategyConnectionService getPositionPnlCost", {
|
|
8618
|
+
symbol,
|
|
8619
|
+
currentPrice,
|
|
8620
|
+
context,
|
|
8621
|
+
backtest,
|
|
8622
|
+
});
|
|
8623
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8624
|
+
return await strategy.getPositionPnlCost(symbol, currentPrice);
|
|
8625
|
+
};
|
|
8626
|
+
this.getPositionLevels = async (backtest, symbol, context) => {
|
|
8627
|
+
this.loggerService.log("strategyConnectionService getPositionLevels", {
|
|
8628
|
+
symbol,
|
|
8629
|
+
context,
|
|
8630
|
+
backtest,
|
|
8631
|
+
});
|
|
8632
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8633
|
+
return await strategy.getPositionLevels(symbol);
|
|
8634
|
+
};
|
|
8635
|
+
this.getPositionPartials = async (backtest, symbol, context) => {
|
|
8636
|
+
this.loggerService.log("strategyConnectionService getPositionPartials", {
|
|
8637
|
+
symbol,
|
|
8638
|
+
context,
|
|
8639
|
+
backtest,
|
|
8640
|
+
});
|
|
8641
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8642
|
+
return await strategy.getPositionPartials(symbol);
|
|
8643
|
+
};
|
|
8275
8644
|
/**
|
|
8276
8645
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
8277
8646
|
* If no scheduled signal exists, returns null.
|
|
@@ -11675,6 +12044,99 @@ class StrategyCoreService {
|
|
|
11675
12044
|
await this.validate(context);
|
|
11676
12045
|
return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
|
|
11677
12046
|
};
|
|
12047
|
+
/**
|
|
12048
|
+
* Returns the percentage of the position currently held (not closed).
|
|
12049
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
12050
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
12051
|
+
*
|
|
12052
|
+
* @param backtest - Whether running in backtest mode
|
|
12053
|
+
* @param symbol - Trading pair symbol
|
|
12054
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
12055
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
12056
|
+
*/
|
|
12057
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
12058
|
+
this.loggerService.log("strategyCoreService getTotalPercentClosed", {
|
|
12059
|
+
symbol,
|
|
12060
|
+
context,
|
|
12061
|
+
});
|
|
12062
|
+
await this.validate(context);
|
|
12063
|
+
return await this.strategyConnectionService.getTotalPercentClosed(backtest, symbol, context);
|
|
12064
|
+
};
|
|
12065
|
+
/**
|
|
12066
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
12067
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
12068
|
+
*
|
|
12069
|
+
* @param backtest - Whether running in backtest mode
|
|
12070
|
+
* @param symbol - Trading pair symbol
|
|
12071
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
12072
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
12073
|
+
*/
|
|
12074
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
12075
|
+
this.loggerService.log("strategyCoreService getTotalCostClosed", {
|
|
12076
|
+
symbol,
|
|
12077
|
+
context,
|
|
12078
|
+
});
|
|
12079
|
+
await this.validate(context);
|
|
12080
|
+
return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
|
|
12081
|
+
};
|
|
12082
|
+
this.getPositionAveragePrice = async (backtest, symbol, context) => {
|
|
12083
|
+
this.loggerService.log("strategyCoreService getPositionAveragePrice", {
|
|
12084
|
+
symbol,
|
|
12085
|
+
context,
|
|
12086
|
+
});
|
|
12087
|
+
await this.validate(context);
|
|
12088
|
+
return await this.strategyConnectionService.getPositionAveragePrice(backtest, symbol, context);
|
|
12089
|
+
};
|
|
12090
|
+
this.getPositionInvestedCount = async (backtest, symbol, context) => {
|
|
12091
|
+
this.loggerService.log("strategyCoreService getPositionInvestedCount", {
|
|
12092
|
+
symbol,
|
|
12093
|
+
context,
|
|
12094
|
+
});
|
|
12095
|
+
await this.validate(context);
|
|
12096
|
+
return await this.strategyConnectionService.getPositionInvestedCount(backtest, symbol, context);
|
|
12097
|
+
};
|
|
12098
|
+
this.getPositionInvestedCost = async (backtest, symbol, context) => {
|
|
12099
|
+
this.loggerService.log("strategyCoreService getPositionInvestedCost", {
|
|
12100
|
+
symbol,
|
|
12101
|
+
context,
|
|
12102
|
+
});
|
|
12103
|
+
await this.validate(context);
|
|
12104
|
+
return await this.strategyConnectionService.getPositionInvestedCost(backtest, symbol, context);
|
|
12105
|
+
};
|
|
12106
|
+
this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
|
|
12107
|
+
this.loggerService.log("strategyCoreService getPositionPnlPercent", {
|
|
12108
|
+
symbol,
|
|
12109
|
+
currentPrice,
|
|
12110
|
+
context,
|
|
12111
|
+
});
|
|
12112
|
+
await this.validate(context);
|
|
12113
|
+
return await this.strategyConnectionService.getPositionPnlPercent(backtest, symbol, currentPrice, context);
|
|
12114
|
+
};
|
|
12115
|
+
this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
|
|
12116
|
+
this.loggerService.log("strategyCoreService getPositionPnlCost", {
|
|
12117
|
+
symbol,
|
|
12118
|
+
currentPrice,
|
|
12119
|
+
context,
|
|
12120
|
+
});
|
|
12121
|
+
await this.validate(context);
|
|
12122
|
+
return await this.strategyConnectionService.getPositionPnlCost(backtest, symbol, currentPrice, context);
|
|
12123
|
+
};
|
|
12124
|
+
this.getPositionLevels = async (backtest, symbol, context) => {
|
|
12125
|
+
this.loggerService.log("strategyCoreService getPositionLevels", {
|
|
12126
|
+
symbol,
|
|
12127
|
+
context,
|
|
12128
|
+
});
|
|
12129
|
+
await this.validate(context);
|
|
12130
|
+
return await this.strategyConnectionService.getPositionLevels(backtest, symbol, context);
|
|
12131
|
+
};
|
|
12132
|
+
this.getPositionPartials = async (backtest, symbol, context) => {
|
|
12133
|
+
this.loggerService.log("strategyCoreService getPositionPartials", {
|
|
12134
|
+
symbol,
|
|
12135
|
+
context,
|
|
12136
|
+
});
|
|
12137
|
+
await this.validate(context);
|
|
12138
|
+
return await this.strategyConnectionService.getPositionPartials(backtest, symbol, context);
|
|
12139
|
+
};
|
|
11678
12140
|
/**
|
|
11679
12141
|
* Retrieves the currently active scheduled signal for the symbol.
|
|
11680
12142
|
* If no scheduled signal exists, returns null.
|
|
@@ -28697,15 +29159,45 @@ async function getAggregatedTrades(symbol, limit) {
|
|
|
28697
29159
|
return await bt.exchangeConnectionService.getAggregatedTrades(symbol, limit);
|
|
28698
29160
|
}
|
|
28699
29161
|
|
|
29162
|
+
/**
|
|
29163
|
+
* Convert an absolute dollar amount to a percentage of the invested position cost.
|
|
29164
|
+
* Use the result as the `percent` argument to `commitPartialProfit` / `commitPartialLoss`.
|
|
29165
|
+
*
|
|
29166
|
+
* @param dollarAmount - Dollar value to close (e.g. 150)
|
|
29167
|
+
* @param investedCost - Total invested cost from `getPositionInvestedCost` (e.g. 300)
|
|
29168
|
+
* @returns Percentage of the position to close (0–100)
|
|
29169
|
+
*
|
|
29170
|
+
* @example
|
|
29171
|
+
* const percent = investedCostToPercent(150, 300); // 50
|
|
29172
|
+
* await commitPartialProfit("BTCUSDT", percent);
|
|
29173
|
+
*/
|
|
29174
|
+
const investedCostToPercent = (dollarAmount, investedCost) => {
|
|
29175
|
+
return (dollarAmount / investedCost) * 100;
|
|
29176
|
+
};
|
|
29177
|
+
|
|
28700
29178
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
28701
29179
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
28702
29180
|
const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
|
|
28703
29181
|
const PARTIAL_LOSS_METHOD_NAME = "strategy.commitPartialLoss";
|
|
29182
|
+
const PARTIAL_PROFIT_COST_METHOD_NAME = "strategy.commitPartialProfitCost";
|
|
29183
|
+
const PARTIAL_LOSS_COST_METHOD_NAME = "strategy.commitPartialLossCost";
|
|
28704
29184
|
const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
|
|
28705
29185
|
const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
|
|
28706
29186
|
const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
|
|
28707
29187
|
const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
|
|
28708
29188
|
const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
|
|
29189
|
+
const GET_TOTAL_PERCENT_CLOSED_METHOD_NAME = "strategy.getTotalPercentClosed";
|
|
29190
|
+
const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
|
|
29191
|
+
const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
|
|
29192
|
+
const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
|
|
29193
|
+
const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
|
|
29194
|
+
const GET_POSITION_AVERAGE_PRICE_METHOD_NAME = "strategy.getPositionAveragePrice";
|
|
29195
|
+
const GET_POSITION_INVESTED_COUNT_METHOD_NAME = "strategy.getPositionInvestedCount";
|
|
29196
|
+
const GET_POSITION_INVESTED_COST_METHOD_NAME = "strategy.getPositionInvestedCost";
|
|
29197
|
+
const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
|
|
29198
|
+
const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
|
|
29199
|
+
const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
|
|
29200
|
+
const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
|
|
28709
29201
|
/**
|
|
28710
29202
|
* Cancels the scheduled signal without stopping the strategy.
|
|
28711
29203
|
*
|
|
@@ -29097,6 +29589,372 @@ async function commitAverageBuy(symbol) {
|
|
|
29097
29589
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29098
29590
|
return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29099
29591
|
}
|
|
29592
|
+
/**
|
|
29593
|
+
* Returns the percentage of the position currently held (not closed).
|
|
29594
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
29595
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29596
|
+
*
|
|
29597
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29598
|
+
*
|
|
29599
|
+
* @param symbol - Trading pair symbol
|
|
29600
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
29601
|
+
*
|
|
29602
|
+
* @example
|
|
29603
|
+
* ```typescript
|
|
29604
|
+
* import { getTotalPercentClosed } from "backtest-kit";
|
|
29605
|
+
*
|
|
29606
|
+
* const heldPct = await getTotalPercentClosed("BTCUSDT");
|
|
29607
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
29608
|
+
* ```
|
|
29609
|
+
*/
|
|
29610
|
+
async function getTotalPercentClosed(symbol) {
|
|
29611
|
+
bt.loggerService.info(GET_TOTAL_PERCENT_CLOSED_METHOD_NAME, {
|
|
29612
|
+
symbol,
|
|
29613
|
+
});
|
|
29614
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29615
|
+
throw new Error("getTotalPercentClosed requires an execution context");
|
|
29616
|
+
}
|
|
29617
|
+
if (!MethodContextService.hasContext()) {
|
|
29618
|
+
throw new Error("getTotalPercentClosed requires a method context");
|
|
29619
|
+
}
|
|
29620
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29621
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29622
|
+
return await bt.strategyCoreService.getTotalPercentClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29623
|
+
}
|
|
29624
|
+
/**
|
|
29625
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
29626
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29627
|
+
*
|
|
29628
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29629
|
+
*
|
|
29630
|
+
* @param symbol - Trading pair symbol
|
|
29631
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
29632
|
+
*
|
|
29633
|
+
* @example
|
|
29634
|
+
* ```typescript
|
|
29635
|
+
* import { getTotalCostClosed } from "backtest-kit";
|
|
29636
|
+
*
|
|
29637
|
+
* const heldCost = await getTotalCostClosed("BTCUSDT");
|
|
29638
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
29639
|
+
* ```
|
|
29640
|
+
*/
|
|
29641
|
+
async function getTotalCostClosed(symbol) {
|
|
29642
|
+
bt.loggerService.info(GET_TOTAL_COST_CLOSED_METHOD_NAME, {
|
|
29643
|
+
symbol,
|
|
29644
|
+
});
|
|
29645
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29646
|
+
throw new Error("getTotalCostClosed requires an execution context");
|
|
29647
|
+
}
|
|
29648
|
+
if (!MethodContextService.hasContext()) {
|
|
29649
|
+
throw new Error("getTotalCostClosed requires a method context");
|
|
29650
|
+
}
|
|
29651
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29652
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29653
|
+
return await bt.strategyCoreService.getTotalCostClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29654
|
+
}
|
|
29655
|
+
/**
|
|
29656
|
+
* Returns the currently active pending signal for the strategy.
|
|
29657
|
+
* If no active signal exists, returns null.
|
|
29658
|
+
*
|
|
29659
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29660
|
+
*
|
|
29661
|
+
* @param symbol - Trading pair symbol
|
|
29662
|
+
* @returns Promise resolving to pending signal or null
|
|
29663
|
+
*
|
|
29664
|
+
* @example
|
|
29665
|
+
* ```typescript
|
|
29666
|
+
* import { getPendingSignal } from "backtest-kit";
|
|
29667
|
+
*
|
|
29668
|
+
* const pending = await getPendingSignal("BTCUSDT");
|
|
29669
|
+
* if (pending) {
|
|
29670
|
+
* console.log("Active signal:", pending.id);
|
|
29671
|
+
* }
|
|
29672
|
+
* ```
|
|
29673
|
+
*/
|
|
29674
|
+
async function getPendingSignal(symbol) {
|
|
29675
|
+
bt.loggerService.info(GET_PENDING_SIGNAL_METHOD_NAME, {
|
|
29676
|
+
symbol,
|
|
29677
|
+
});
|
|
29678
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29679
|
+
throw new Error("getPendingSignal requires an execution context");
|
|
29680
|
+
}
|
|
29681
|
+
if (!MethodContextService.hasContext()) {
|
|
29682
|
+
throw new Error("getPendingSignal requires a method context");
|
|
29683
|
+
}
|
|
29684
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29685
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29686
|
+
return await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29687
|
+
}
|
|
29688
|
+
/**
|
|
29689
|
+
* Returns the currently active scheduled signal for the strategy.
|
|
29690
|
+
* If no scheduled signal exists, returns null.
|
|
29691
|
+
*
|
|
29692
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29693
|
+
*
|
|
29694
|
+
* @param symbol - Trading pair symbol
|
|
29695
|
+
* @returns Promise resolving to scheduled signal or null
|
|
29696
|
+
*
|
|
29697
|
+
* @example
|
|
29698
|
+
* ```typescript
|
|
29699
|
+
* import { getScheduledSignal } from "backtest-kit";
|
|
29700
|
+
*
|
|
29701
|
+
* const scheduled = await getScheduledSignal("BTCUSDT");
|
|
29702
|
+
* if (scheduled) {
|
|
29703
|
+
* console.log("Scheduled signal:", scheduled.id);
|
|
29704
|
+
* }
|
|
29705
|
+
* ```
|
|
29706
|
+
*/
|
|
29707
|
+
async function getScheduledSignal(symbol) {
|
|
29708
|
+
bt.loggerService.info(GET_SCHEDULED_SIGNAL_METHOD_NAME, {
|
|
29709
|
+
symbol,
|
|
29710
|
+
});
|
|
29711
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29712
|
+
throw new Error("getScheduledSignal requires an execution context");
|
|
29713
|
+
}
|
|
29714
|
+
if (!MethodContextService.hasContext()) {
|
|
29715
|
+
throw new Error("getScheduledSignal requires a method context");
|
|
29716
|
+
}
|
|
29717
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29718
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29719
|
+
return await bt.strategyCoreService.getScheduledSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29720
|
+
}
|
|
29721
|
+
/**
|
|
29722
|
+
* Checks if breakeven threshold has been reached for the current pending signal.
|
|
29723
|
+
*
|
|
29724
|
+
* Returns true if price has moved far enough in profit direction to cover
|
|
29725
|
+
* transaction costs. Threshold is calculated as: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2
|
|
29726
|
+
*
|
|
29727
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29728
|
+
*
|
|
29729
|
+
* @param symbol - Trading pair symbol
|
|
29730
|
+
* @param currentPrice - Current market price to check against threshold
|
|
29731
|
+
* @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
|
|
29732
|
+
*
|
|
29733
|
+
* @example
|
|
29734
|
+
* ```typescript
|
|
29735
|
+
* import { getBreakeven, getAveragePrice } from "backtest-kit";
|
|
29736
|
+
*
|
|
29737
|
+
* const price = await getAveragePrice("BTCUSDT");
|
|
29738
|
+
* const canBreakeven = await getBreakeven("BTCUSDT", price);
|
|
29739
|
+
* if (canBreakeven) {
|
|
29740
|
+
* console.log("Breakeven available");
|
|
29741
|
+
* }
|
|
29742
|
+
* ```
|
|
29743
|
+
*/
|
|
29744
|
+
async function getBreakeven(symbol, currentPrice) {
|
|
29745
|
+
bt.loggerService.info(GET_BREAKEVEN_METHOD_NAME, {
|
|
29746
|
+
symbol,
|
|
29747
|
+
currentPrice,
|
|
29748
|
+
});
|
|
29749
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29750
|
+
throw new Error("getBreakeven requires an execution context");
|
|
29751
|
+
}
|
|
29752
|
+
if (!MethodContextService.hasContext()) {
|
|
29753
|
+
throw new Error("getBreakeven requires a method context");
|
|
29754
|
+
}
|
|
29755
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29756
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29757
|
+
return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29758
|
+
}
|
|
29759
|
+
async function getPositionAveragePrice(symbol) {
|
|
29760
|
+
bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, { symbol });
|
|
29761
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29762
|
+
throw new Error("getPositionAveragePrice requires an execution context");
|
|
29763
|
+
}
|
|
29764
|
+
if (!MethodContextService.hasContext()) {
|
|
29765
|
+
throw new Error("getPositionAveragePrice requires a method context");
|
|
29766
|
+
}
|
|
29767
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29768
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29769
|
+
return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29770
|
+
}
|
|
29771
|
+
async function getPositionInvestedCount(symbol) {
|
|
29772
|
+
bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, { symbol });
|
|
29773
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29774
|
+
throw new Error("getPositionInvestedCount requires an execution context");
|
|
29775
|
+
}
|
|
29776
|
+
if (!MethodContextService.hasContext()) {
|
|
29777
|
+
throw new Error("getPositionInvestedCount requires a method context");
|
|
29778
|
+
}
|
|
29779
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29780
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29781
|
+
return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29782
|
+
}
|
|
29783
|
+
async function getPositionInvestedCost(symbol) {
|
|
29784
|
+
bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, { symbol });
|
|
29785
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29786
|
+
throw new Error("getPositionInvestedCost requires an execution context");
|
|
29787
|
+
}
|
|
29788
|
+
if (!MethodContextService.hasContext()) {
|
|
29789
|
+
throw new Error("getPositionInvestedCost requires a method context");
|
|
29790
|
+
}
|
|
29791
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29792
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29793
|
+
return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29794
|
+
}
|
|
29795
|
+
async function getPositionPnlPercent(symbol) {
|
|
29796
|
+
bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
|
|
29797
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29798
|
+
throw new Error("getPositionPnlPercent requires an execution context");
|
|
29799
|
+
}
|
|
29800
|
+
if (!MethodContextService.hasContext()) {
|
|
29801
|
+
throw new Error("getPositionPnlPercent requires a method context");
|
|
29802
|
+
}
|
|
29803
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29804
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29805
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29806
|
+
return await bt.strategyCoreService.getPositionPnlPercent(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29807
|
+
}
|
|
29808
|
+
/**
|
|
29809
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
29810
|
+
*
|
|
29811
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
29812
|
+
* to a percentage of the invested position cost automatically.
|
|
29813
|
+
* Price must be moving toward take profit (in profit direction).
|
|
29814
|
+
*
|
|
29815
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29816
|
+
* Automatically fetches current price via getAveragePrice.
|
|
29817
|
+
*
|
|
29818
|
+
* @param symbol - Trading pair symbol
|
|
29819
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
29820
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
29821
|
+
*
|
|
29822
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
29823
|
+
* - LONG: currentPrice must be > priceOpen
|
|
29824
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
29825
|
+
*
|
|
29826
|
+
* @example
|
|
29827
|
+
* ```typescript
|
|
29828
|
+
* import { commitPartialProfitCost } from "backtest-kit";
|
|
29829
|
+
*
|
|
29830
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
29831
|
+
* const success = await commitPartialProfitCost("BTCUSDT", 150);
|
|
29832
|
+
* if (success) {
|
|
29833
|
+
* console.log('Partial profit executed');
|
|
29834
|
+
* }
|
|
29835
|
+
* ```
|
|
29836
|
+
*/
|
|
29837
|
+
async function commitPartialProfitCost(symbol, dollarAmount) {
|
|
29838
|
+
bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, { symbol, dollarAmount });
|
|
29839
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29840
|
+
throw new Error("commitPartialProfitCost requires an execution context");
|
|
29841
|
+
}
|
|
29842
|
+
if (!MethodContextService.hasContext()) {
|
|
29843
|
+
throw new Error("commitPartialProfitCost requires a method context");
|
|
29844
|
+
}
|
|
29845
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29846
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29847
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29848
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29849
|
+
if (investedCost === null)
|
|
29850
|
+
return false;
|
|
29851
|
+
const percentToClose = investedCostToPercent(dollarAmount, investedCost);
|
|
29852
|
+
return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
29853
|
+
}
|
|
29854
|
+
/**
|
|
29855
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
29856
|
+
*
|
|
29857
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
29858
|
+
* to a percentage of the invested position cost automatically.
|
|
29859
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
29860
|
+
*
|
|
29861
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29862
|
+
* Automatically fetches current price via getAveragePrice.
|
|
29863
|
+
*
|
|
29864
|
+
* @param symbol - Trading pair symbol
|
|
29865
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
29866
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
29867
|
+
*
|
|
29868
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
29869
|
+
* - LONG: currentPrice must be < priceOpen
|
|
29870
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
29871
|
+
*
|
|
29872
|
+
* @example
|
|
29873
|
+
* ```typescript
|
|
29874
|
+
* import { commitPartialLossCost } from "backtest-kit";
|
|
29875
|
+
*
|
|
29876
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
29877
|
+
* const success = await commitPartialLossCost("BTCUSDT", 100);
|
|
29878
|
+
* if (success) {
|
|
29879
|
+
* console.log('Partial loss executed');
|
|
29880
|
+
* }
|
|
29881
|
+
* ```
|
|
29882
|
+
*/
|
|
29883
|
+
async function commitPartialLossCost(symbol, dollarAmount) {
|
|
29884
|
+
bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, { symbol, dollarAmount });
|
|
29885
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29886
|
+
throw new Error("commitPartialLossCost requires an execution context");
|
|
29887
|
+
}
|
|
29888
|
+
if (!MethodContextService.hasContext()) {
|
|
29889
|
+
throw new Error("commitPartialLossCost requires a method context");
|
|
29890
|
+
}
|
|
29891
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29892
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29893
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29894
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29895
|
+
if (investedCost === null)
|
|
29896
|
+
return false;
|
|
29897
|
+
const percentToClose = investedCostToPercent(dollarAmount, investedCost);
|
|
29898
|
+
return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
|
|
29899
|
+
}
|
|
29900
|
+
async function getPositionPnlCost(symbol) {
|
|
29901
|
+
bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
|
|
29902
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29903
|
+
throw new Error("getPositionPnlCost requires an execution context");
|
|
29904
|
+
}
|
|
29905
|
+
if (!MethodContextService.hasContext()) {
|
|
29906
|
+
throw new Error("getPositionPnlCost requires a method context");
|
|
29907
|
+
}
|
|
29908
|
+
const currentPrice = await getAveragePrice(symbol);
|
|
29909
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29910
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29911
|
+
return await bt.strategyCoreService.getPositionPnlCost(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29912
|
+
}
|
|
29913
|
+
/**
|
|
29914
|
+
* Returns the list of DCA entry prices for the current pending signal.
|
|
29915
|
+
*
|
|
29916
|
+
* The first element is always the original priceOpen (initial entry).
|
|
29917
|
+
* Each subsequent element is a price added by commitAverageBuy().
|
|
29918
|
+
*
|
|
29919
|
+
* Returns null if no pending signal exists.
|
|
29920
|
+
* Returns a single-element array [priceOpen] if no DCA entries were made.
|
|
29921
|
+
*
|
|
29922
|
+
* @param symbol - Trading pair symbol
|
|
29923
|
+
* @returns Promise resolving to array of entry prices or null
|
|
29924
|
+
*
|
|
29925
|
+
* @example
|
|
29926
|
+
* ```typescript
|
|
29927
|
+
* import { getPositionLevels } from "backtest-kit";
|
|
29928
|
+
*
|
|
29929
|
+
* const levels = await getPositionLevels("BTCUSDT");
|
|
29930
|
+
* // No DCA: [43000]
|
|
29931
|
+
* // One DCA: [43000, 42000]
|
|
29932
|
+
* ```
|
|
29933
|
+
*/
|
|
29934
|
+
async function getPositionLevels(symbol) {
|
|
29935
|
+
bt.loggerService.info(GET_POSITION_LEVELS_METHOD_NAME, { symbol });
|
|
29936
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29937
|
+
throw new Error("getPositionLevels requires an execution context");
|
|
29938
|
+
}
|
|
29939
|
+
if (!MethodContextService.hasContext()) {
|
|
29940
|
+
throw new Error("getPositionLevels requires a method context");
|
|
29941
|
+
}
|
|
29942
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29943
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29944
|
+
return await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29945
|
+
}
|
|
29946
|
+
async function getPositionPartials(symbol) {
|
|
29947
|
+
bt.loggerService.info(GET_POSITION_PARTIALS_METHOD_NAME, { symbol });
|
|
29948
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29949
|
+
throw new Error("getPositionPartials requires an execution context");
|
|
29950
|
+
}
|
|
29951
|
+
if (!MethodContextService.hasContext()) {
|
|
29952
|
+
throw new Error("getPositionPartials requires a method context");
|
|
29953
|
+
}
|
|
29954
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29955
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29956
|
+
return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29957
|
+
}
|
|
29100
29958
|
|
|
29101
29959
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
29102
29960
|
/**
|
|
@@ -30284,13 +31142,24 @@ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
|
30284
31142
|
const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
|
|
30285
31143
|
const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
|
|
30286
31144
|
const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
|
|
31145
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPercentClosed";
|
|
31146
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
|
|
30287
31147
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
30288
31148
|
const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
|
|
31149
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "BacktestUtils.getPositionAveragePrice";
|
|
31150
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "BacktestUtils.getPositionInvestedCount";
|
|
31151
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST = "BacktestUtils.getPositionInvestedCost";
|
|
31152
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT = "BacktestUtils.getPositionPnlPercent";
|
|
31153
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnlCost";
|
|
31154
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
|
|
31155
|
+
const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
|
|
30289
31156
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
30290
31157
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
30291
31158
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
30292
31159
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
30293
31160
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
31161
|
+
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
|
|
31162
|
+
const BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST = "BacktestUtils.commitPartialLossCost";
|
|
30294
31163
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
|
|
30295
31164
|
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
|
|
30296
31165
|
const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
|
|
@@ -30685,6 +31554,71 @@ class BacktestUtils {
|
|
|
30685
31554
|
}
|
|
30686
31555
|
return await bt.strategyCoreService.getPendingSignal(true, symbol, context);
|
|
30687
31556
|
};
|
|
31557
|
+
/**
|
|
31558
|
+
* Returns the percentage of the position currently held (not closed).
|
|
31559
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
31560
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31561
|
+
*
|
|
31562
|
+
* @param symbol - Trading pair symbol
|
|
31563
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31564
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
31565
|
+
*
|
|
31566
|
+
* @example
|
|
31567
|
+
* ```typescript
|
|
31568
|
+
* const heldPct = await Backtest.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31569
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
31570
|
+
* ```
|
|
31571
|
+
*/
|
|
31572
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
31573
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
31574
|
+
symbol,
|
|
31575
|
+
context,
|
|
31576
|
+
});
|
|
31577
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31578
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31579
|
+
{
|
|
31580
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31581
|
+
riskName &&
|
|
31582
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31583
|
+
riskList &&
|
|
31584
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31585
|
+
actions &&
|
|
31586
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31587
|
+
}
|
|
31588
|
+
return await bt.strategyCoreService.getTotalPercentClosed(true, symbol, context);
|
|
31589
|
+
};
|
|
31590
|
+
/**
|
|
31591
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
31592
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31593
|
+
*
|
|
31594
|
+
* @param symbol - Trading pair symbol
|
|
31595
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31596
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
31597
|
+
*
|
|
31598
|
+
* @example
|
|
31599
|
+
* ```typescript
|
|
31600
|
+
* const heldCost = await Backtest.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31601
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
31602
|
+
* ```
|
|
31603
|
+
*/
|
|
31604
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
31605
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
31606
|
+
symbol,
|
|
31607
|
+
context,
|
|
31608
|
+
});
|
|
31609
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31610
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31611
|
+
{
|
|
31612
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31613
|
+
riskName &&
|
|
31614
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31615
|
+
riskList &&
|
|
31616
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31617
|
+
actions &&
|
|
31618
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31619
|
+
}
|
|
31620
|
+
return await bt.strategyCoreService.getTotalCostClosed(true, symbol, context);
|
|
31621
|
+
};
|
|
30688
31622
|
/**
|
|
30689
31623
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
30690
31624
|
* If no scheduled signal exists, returns null.
|
|
@@ -30762,6 +31696,134 @@ class BacktestUtils {
|
|
|
30762
31696
|
}
|
|
30763
31697
|
return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
|
|
30764
31698
|
};
|
|
31699
|
+
this.getPositionAveragePrice = async (symbol, context) => {
|
|
31700
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
|
|
31701
|
+
symbol,
|
|
31702
|
+
context,
|
|
31703
|
+
});
|
|
31704
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31705
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31706
|
+
{
|
|
31707
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31708
|
+
riskName &&
|
|
31709
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
31710
|
+
riskList &&
|
|
31711
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
31712
|
+
actions &&
|
|
31713
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
31714
|
+
}
|
|
31715
|
+
return await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
|
|
31716
|
+
};
|
|
31717
|
+
this.getPositionInvestedCount = async (symbol, context) => {
|
|
31718
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
|
|
31719
|
+
symbol,
|
|
31720
|
+
context,
|
|
31721
|
+
});
|
|
31722
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31723
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31724
|
+
{
|
|
31725
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31726
|
+
riskName &&
|
|
31727
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
31728
|
+
riskList &&
|
|
31729
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
31730
|
+
actions &&
|
|
31731
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
31732
|
+
}
|
|
31733
|
+
return await bt.strategyCoreService.getPositionInvestedCount(true, symbol, context);
|
|
31734
|
+
};
|
|
31735
|
+
this.getPositionInvestedCost = async (symbol, context) => {
|
|
31736
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST, {
|
|
31737
|
+
symbol,
|
|
31738
|
+
context,
|
|
31739
|
+
});
|
|
31740
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31741
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31742
|
+
{
|
|
31743
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31744
|
+
riskName &&
|
|
31745
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
31746
|
+
riskList &&
|
|
31747
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
31748
|
+
actions &&
|
|
31749
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
31750
|
+
}
|
|
31751
|
+
return await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
31752
|
+
};
|
|
31753
|
+
this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
|
|
31754
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
|
|
31755
|
+
symbol,
|
|
31756
|
+
currentPrice,
|
|
31757
|
+
context,
|
|
31758
|
+
});
|
|
31759
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31760
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31761
|
+
{
|
|
31762
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31763
|
+
riskName &&
|
|
31764
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
31765
|
+
riskList &&
|
|
31766
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
31767
|
+
actions &&
|
|
31768
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
31769
|
+
}
|
|
31770
|
+
return await bt.strategyCoreService.getPositionPnlPercent(true, symbol, currentPrice, context);
|
|
31771
|
+
};
|
|
31772
|
+
this.getPositionPnlCost = async (symbol, currentPrice, context) => {
|
|
31773
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST, {
|
|
31774
|
+
symbol,
|
|
31775
|
+
currentPrice,
|
|
31776
|
+
context,
|
|
31777
|
+
});
|
|
31778
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31779
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31780
|
+
{
|
|
31781
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31782
|
+
riskName &&
|
|
31783
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
31784
|
+
riskList &&
|
|
31785
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
31786
|
+
actions &&
|
|
31787
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
31788
|
+
}
|
|
31789
|
+
return await bt.strategyCoreService.getPositionPnlCost(true, symbol, currentPrice, context);
|
|
31790
|
+
};
|
|
31791
|
+
this.getPositionLevels = async (symbol, context) => {
|
|
31792
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_LEVELS, {
|
|
31793
|
+
symbol,
|
|
31794
|
+
context,
|
|
31795
|
+
});
|
|
31796
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31797
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31798
|
+
{
|
|
31799
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31800
|
+
riskName &&
|
|
31801
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
|
|
31802
|
+
riskList &&
|
|
31803
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
|
|
31804
|
+
actions &&
|
|
31805
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
|
|
31806
|
+
}
|
|
31807
|
+
return await bt.strategyCoreService.getPositionLevels(true, symbol, context);
|
|
31808
|
+
};
|
|
31809
|
+
this.getPositionPartials = async (symbol, context) => {
|
|
31810
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS, {
|
|
31811
|
+
symbol,
|
|
31812
|
+
context,
|
|
31813
|
+
});
|
|
31814
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31815
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31816
|
+
{
|
|
31817
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31818
|
+
riskName &&
|
|
31819
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
31820
|
+
riskList &&
|
|
31821
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
31822
|
+
actions &&
|
|
31823
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
31824
|
+
}
|
|
31825
|
+
return await bt.strategyCoreService.getPositionPartials(true, symbol, context);
|
|
31826
|
+
};
|
|
30765
31827
|
/**
|
|
30766
31828
|
* Stops the strategy from generating new signals.
|
|
30767
31829
|
*
|
|
@@ -30983,6 +32045,114 @@ class BacktestUtils {
|
|
|
30983
32045
|
}
|
|
30984
32046
|
return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
30985
32047
|
};
|
|
32048
|
+
/**
|
|
32049
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
32050
|
+
*
|
|
32051
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
32052
|
+
* to a percentage of the invested position cost automatically.
|
|
32053
|
+
* Price must be moving toward take profit (in profit direction).
|
|
32054
|
+
*
|
|
32055
|
+
* @param symbol - Trading pair symbol
|
|
32056
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
32057
|
+
* @param currentPrice - Current market price for this partial close
|
|
32058
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
32059
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
32060
|
+
*
|
|
32061
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
32062
|
+
* - LONG: currentPrice must be > priceOpen
|
|
32063
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
32064
|
+
*
|
|
32065
|
+
* @example
|
|
32066
|
+
* ```typescript
|
|
32067
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
32068
|
+
* const success = await Backtest.commitPartialProfitCost("BTCUSDT", 150, 45000, {
|
|
32069
|
+
* exchangeName: "binance",
|
|
32070
|
+
* frameName: "frame1",
|
|
32071
|
+
* strategyName: "my-strategy"
|
|
32072
|
+
* });
|
|
32073
|
+
* if (success) {
|
|
32074
|
+
* console.log('Partial profit executed');
|
|
32075
|
+
* }
|
|
32076
|
+
* ```
|
|
32077
|
+
*/
|
|
32078
|
+
this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
32079
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST, {
|
|
32080
|
+
symbol,
|
|
32081
|
+
dollarAmount,
|
|
32082
|
+
currentPrice,
|
|
32083
|
+
context,
|
|
32084
|
+
});
|
|
32085
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32086
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32087
|
+
{
|
|
32088
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32089
|
+
riskName &&
|
|
32090
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
32091
|
+
riskList &&
|
|
32092
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
32093
|
+
actions &&
|
|
32094
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
32095
|
+
}
|
|
32096
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
32097
|
+
if (investedCost === null)
|
|
32098
|
+
return false;
|
|
32099
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
32100
|
+
return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
|
|
32101
|
+
};
|
|
32102
|
+
/**
|
|
32103
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
32104
|
+
*
|
|
32105
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
32106
|
+
* to a percentage of the invested position cost automatically.
|
|
32107
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
32108
|
+
*
|
|
32109
|
+
* @param symbol - Trading pair symbol
|
|
32110
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
32111
|
+
* @param currentPrice - Current market price for this partial close
|
|
32112
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
32113
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
32114
|
+
*
|
|
32115
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
32116
|
+
* - LONG: currentPrice must be < priceOpen
|
|
32117
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
32118
|
+
*
|
|
32119
|
+
* @example
|
|
32120
|
+
* ```typescript
|
|
32121
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
32122
|
+
* const success = await Backtest.commitPartialLossCost("BTCUSDT", 100, 38000, {
|
|
32123
|
+
* exchangeName: "binance",
|
|
32124
|
+
* frameName: "frame1",
|
|
32125
|
+
* strategyName: "my-strategy"
|
|
32126
|
+
* });
|
|
32127
|
+
* if (success) {
|
|
32128
|
+
* console.log('Partial loss executed');
|
|
32129
|
+
* }
|
|
32130
|
+
* ```
|
|
32131
|
+
*/
|
|
32132
|
+
this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
32133
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST, {
|
|
32134
|
+
symbol,
|
|
32135
|
+
dollarAmount,
|
|
32136
|
+
currentPrice,
|
|
32137
|
+
context,
|
|
32138
|
+
});
|
|
32139
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32140
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32141
|
+
{
|
|
32142
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32143
|
+
riskName &&
|
|
32144
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
32145
|
+
riskList &&
|
|
32146
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
32147
|
+
actions &&
|
|
32148
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
32149
|
+
}
|
|
32150
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
|
|
32151
|
+
if (investedCost === null)
|
|
32152
|
+
return false;
|
|
32153
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
32154
|
+
return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
|
|
32155
|
+
};
|
|
30986
32156
|
/**
|
|
30987
32157
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
30988
32158
|
*
|
|
@@ -31400,13 +32570,24 @@ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
|
31400
32570
|
const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
|
|
31401
32571
|
const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
|
|
31402
32572
|
const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
|
|
32573
|
+
const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClosed";
|
|
32574
|
+
const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
|
|
31403
32575
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
31404
32576
|
const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
|
|
32577
|
+
const LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "LiveUtils.getPositionAveragePrice";
|
|
32578
|
+
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "LiveUtils.getPositionInvestedCount";
|
|
32579
|
+
const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST = "LiveUtils.getPositionInvestedCost";
|
|
32580
|
+
const LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT = "LiveUtils.getPositionPnlPercent";
|
|
32581
|
+
const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
|
|
32582
|
+
const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
|
|
32583
|
+
const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
|
|
31405
32584
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
31406
32585
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
31407
32586
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
31408
32587
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
31409
32588
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
32589
|
+
const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
|
|
32590
|
+
const LIVE_METHOD_NAME_PARTIAL_LOSS_COST = "LiveUtils.commitPartialLossCost";
|
|
31410
32591
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
|
|
31411
32592
|
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
|
|
31412
32593
|
const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
|
|
@@ -31770,6 +32951,73 @@ class LiveUtils {
|
|
|
31770
32951
|
frameName: "",
|
|
31771
32952
|
});
|
|
31772
32953
|
};
|
|
32954
|
+
/**
|
|
32955
|
+
* Returns the percentage of the position currently held (not closed).
|
|
32956
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
32957
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32958
|
+
*
|
|
32959
|
+
* @param symbol - Trading pair symbol
|
|
32960
|
+
* @param context - Context with strategyName and exchangeName
|
|
32961
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
32962
|
+
*
|
|
32963
|
+
* @example
|
|
32964
|
+
* ```typescript
|
|
32965
|
+
* const heldPct = await Live.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32966
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
32967
|
+
* ```
|
|
32968
|
+
*/
|
|
32969
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
32970
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
32971
|
+
symbol,
|
|
32972
|
+
context,
|
|
32973
|
+
});
|
|
32974
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32975
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32976
|
+
{
|
|
32977
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32978
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32979
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32980
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32981
|
+
}
|
|
32982
|
+
return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
|
|
32983
|
+
strategyName: context.strategyName,
|
|
32984
|
+
exchangeName: context.exchangeName,
|
|
32985
|
+
frameName: "",
|
|
32986
|
+
});
|
|
32987
|
+
};
|
|
32988
|
+
/**
|
|
32989
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
32990
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32991
|
+
*
|
|
32992
|
+
* @param symbol - Trading pair symbol
|
|
32993
|
+
* @param context - Context with strategyName and exchangeName
|
|
32994
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
32995
|
+
*
|
|
32996
|
+
* @example
|
|
32997
|
+
* ```typescript
|
|
32998
|
+
* const heldCost = await Live.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32999
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
33000
|
+
* ```
|
|
33001
|
+
*/
|
|
33002
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
33003
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
33004
|
+
symbol,
|
|
33005
|
+
context,
|
|
33006
|
+
});
|
|
33007
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
33008
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
33009
|
+
{
|
|
33010
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33011
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
33012
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
33013
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
33014
|
+
}
|
|
33015
|
+
return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
|
|
33016
|
+
strategyName: context.strategyName,
|
|
33017
|
+
exchangeName: context.exchangeName,
|
|
33018
|
+
frameName: "",
|
|
33019
|
+
});
|
|
33020
|
+
};
|
|
31773
33021
|
/**
|
|
31774
33022
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
31775
33023
|
* If no scheduled signal exists, returns null.
|
|
@@ -31848,6 +33096,118 @@ class LiveUtils {
|
|
|
31848
33096
|
frameName: "",
|
|
31849
33097
|
});
|
|
31850
33098
|
};
|
|
33099
|
+
this.getPositionAveragePrice = async (symbol, context) => {
|
|
33100
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, { symbol, context });
|
|
33101
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33102
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33103
|
+
{
|
|
33104
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33105
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
|
|
33106
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
33107
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
|
|
33108
|
+
}
|
|
33109
|
+
return await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
|
|
33110
|
+
strategyName: context.strategyName,
|
|
33111
|
+
exchangeName: context.exchangeName,
|
|
33112
|
+
frameName: "",
|
|
33113
|
+
});
|
|
33114
|
+
};
|
|
33115
|
+
this.getPositionInvestedCount = async (symbol, context) => {
|
|
33116
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, { symbol, context });
|
|
33117
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33118
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33119
|
+
{
|
|
33120
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33121
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
|
|
33122
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
33123
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
|
|
33124
|
+
}
|
|
33125
|
+
return await bt.strategyCoreService.getPositionInvestedCount(false, symbol, {
|
|
33126
|
+
strategyName: context.strategyName,
|
|
33127
|
+
exchangeName: context.exchangeName,
|
|
33128
|
+
frameName: "",
|
|
33129
|
+
});
|
|
33130
|
+
};
|
|
33131
|
+
this.getPositionInvestedCost = async (symbol, context) => {
|
|
33132
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, { symbol, context });
|
|
33133
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33134
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33135
|
+
{
|
|
33136
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33137
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
|
|
33138
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
33139
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
|
|
33140
|
+
}
|
|
33141
|
+
return await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33142
|
+
strategyName: context.strategyName,
|
|
33143
|
+
exchangeName: context.exchangeName,
|
|
33144
|
+
frameName: "",
|
|
33145
|
+
});
|
|
33146
|
+
};
|
|
33147
|
+
this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
|
|
33148
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, { symbol, currentPrice, context });
|
|
33149
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33150
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33151
|
+
{
|
|
33152
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33153
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
|
|
33154
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
33155
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
|
|
33156
|
+
}
|
|
33157
|
+
return await bt.strategyCoreService.getPositionPnlPercent(false, symbol, currentPrice, {
|
|
33158
|
+
strategyName: context.strategyName,
|
|
33159
|
+
exchangeName: context.exchangeName,
|
|
33160
|
+
frameName: "",
|
|
33161
|
+
});
|
|
33162
|
+
};
|
|
33163
|
+
this.getPositionPnlCost = async (symbol, currentPrice, context) => {
|
|
33164
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, { symbol, currentPrice, context });
|
|
33165
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33166
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33167
|
+
{
|
|
33168
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33169
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
|
|
33170
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
33171
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
|
|
33172
|
+
}
|
|
33173
|
+
return await bt.strategyCoreService.getPositionPnlCost(false, symbol, currentPrice, {
|
|
33174
|
+
strategyName: context.strategyName,
|
|
33175
|
+
exchangeName: context.exchangeName,
|
|
33176
|
+
frameName: "",
|
|
33177
|
+
});
|
|
33178
|
+
};
|
|
33179
|
+
this.getPositionLevels = async (symbol, context) => {
|
|
33180
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, { symbol, context });
|
|
33181
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33182
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33183
|
+
{
|
|
33184
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33185
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
|
|
33186
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
|
|
33187
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
|
|
33188
|
+
}
|
|
33189
|
+
return await bt.strategyCoreService.getPositionLevels(false, symbol, {
|
|
33190
|
+
strategyName: context.strategyName,
|
|
33191
|
+
exchangeName: context.exchangeName,
|
|
33192
|
+
frameName: "",
|
|
33193
|
+
});
|
|
33194
|
+
};
|
|
33195
|
+
this.getPositionPartials = async (symbol, context) => {
|
|
33196
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, { symbol, context });
|
|
33197
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33198
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33199
|
+
{
|
|
33200
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33201
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
|
|
33202
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
33203
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
|
|
33204
|
+
}
|
|
33205
|
+
return await bt.strategyCoreService.getPositionPartials(false, symbol, {
|
|
33206
|
+
strategyName: context.strategyName,
|
|
33207
|
+
exchangeName: context.exchangeName,
|
|
33208
|
+
frameName: "",
|
|
33209
|
+
});
|
|
33210
|
+
};
|
|
31851
33211
|
/**
|
|
31852
33212
|
* Stops the strategy from generating new signals.
|
|
31853
33213
|
*
|
|
@@ -32066,6 +33426,112 @@ class LiveUtils {
|
|
|
32066
33426
|
frameName: "",
|
|
32067
33427
|
});
|
|
32068
33428
|
};
|
|
33429
|
+
/**
|
|
33430
|
+
* Executes partial close at profit level by absolute dollar amount (moving toward TP).
|
|
33431
|
+
*
|
|
33432
|
+
* Convenience wrapper around commitPartialProfit that converts a dollar amount
|
|
33433
|
+
* to a percentage of the invested position cost automatically.
|
|
33434
|
+
* Price must be moving toward take profit (in profit direction).
|
|
33435
|
+
*
|
|
33436
|
+
* @param symbol - Trading pair symbol
|
|
33437
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
|
|
33438
|
+
* @param currentPrice - Current market price for this partial close
|
|
33439
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
33440
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
33441
|
+
*
|
|
33442
|
+
* @throws Error if currentPrice is not in profit direction:
|
|
33443
|
+
* - LONG: currentPrice must be > priceOpen
|
|
33444
|
+
* - SHORT: currentPrice must be < priceOpen
|
|
33445
|
+
*
|
|
33446
|
+
* @example
|
|
33447
|
+
* ```typescript
|
|
33448
|
+
* // Close $150 of a $300 position (50%) at profit
|
|
33449
|
+
* const success = await Live.commitPartialProfitCost("BTCUSDT", 150, 45000, {
|
|
33450
|
+
* exchangeName: "binance",
|
|
33451
|
+
* strategyName: "my-strategy"
|
|
33452
|
+
* });
|
|
33453
|
+
* if (success) {
|
|
33454
|
+
* console.log('Partial profit executed');
|
|
33455
|
+
* }
|
|
33456
|
+
* ```
|
|
33457
|
+
*/
|
|
33458
|
+
this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
33459
|
+
bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, { symbol, dollarAmount, currentPrice, context });
|
|
33460
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33461
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33462
|
+
{
|
|
33463
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33464
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
|
|
33465
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
33466
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
|
|
33467
|
+
}
|
|
33468
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33469
|
+
strategyName: context.strategyName,
|
|
33470
|
+
exchangeName: context.exchangeName,
|
|
33471
|
+
frameName: "",
|
|
33472
|
+
});
|
|
33473
|
+
if (investedCost === null)
|
|
33474
|
+
return false;
|
|
33475
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
33476
|
+
return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
|
|
33477
|
+
strategyName: context.strategyName,
|
|
33478
|
+
exchangeName: context.exchangeName,
|
|
33479
|
+
frameName: "",
|
|
33480
|
+
});
|
|
33481
|
+
};
|
|
33482
|
+
/**
|
|
33483
|
+
* Executes partial close at loss level by absolute dollar amount (moving toward SL).
|
|
33484
|
+
*
|
|
33485
|
+
* Convenience wrapper around commitPartialLoss that converts a dollar amount
|
|
33486
|
+
* to a percentage of the invested position cost automatically.
|
|
33487
|
+
* Price must be moving toward stop loss (in loss direction).
|
|
33488
|
+
*
|
|
33489
|
+
* @param symbol - Trading pair symbol
|
|
33490
|
+
* @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
|
|
33491
|
+
* @param currentPrice - Current market price for this partial close
|
|
33492
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
33493
|
+
* @returns Promise<boolean> - true if partial close executed, false if skipped or no position
|
|
33494
|
+
*
|
|
33495
|
+
* @throws Error if currentPrice is not in loss direction:
|
|
33496
|
+
* - LONG: currentPrice must be < priceOpen
|
|
33497
|
+
* - SHORT: currentPrice must be > priceOpen
|
|
33498
|
+
*
|
|
33499
|
+
* @example
|
|
33500
|
+
* ```typescript
|
|
33501
|
+
* // Close $100 of a $300 position (~33%) at loss
|
|
33502
|
+
* const success = await Live.commitPartialLossCost("BTCUSDT", 100, 38000, {
|
|
33503
|
+
* exchangeName: "binance",
|
|
33504
|
+
* strategyName: "my-strategy"
|
|
33505
|
+
* });
|
|
33506
|
+
* if (success) {
|
|
33507
|
+
* console.log('Partial loss executed');
|
|
33508
|
+
* }
|
|
33509
|
+
* ```
|
|
33510
|
+
*/
|
|
33511
|
+
this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
|
|
33512
|
+
bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, { symbol, dollarAmount, currentPrice, context });
|
|
33513
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33514
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33515
|
+
{
|
|
33516
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
33517
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
|
|
33518
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
33519
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
|
|
33520
|
+
}
|
|
33521
|
+
const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
|
|
33522
|
+
strategyName: context.strategyName,
|
|
33523
|
+
exchangeName: context.exchangeName,
|
|
33524
|
+
frameName: "",
|
|
33525
|
+
});
|
|
33526
|
+
if (investedCost === null)
|
|
33527
|
+
return false;
|
|
33528
|
+
const percentToClose = (dollarAmount / investedCost) * 100;
|
|
33529
|
+
return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
|
|
33530
|
+
strategyName: context.strategyName,
|
|
33531
|
+
exchangeName: context.exchangeName,
|
|
33532
|
+
frameName: "",
|
|
33533
|
+
});
|
|
33534
|
+
};
|
|
32069
33535
|
/**
|
|
32070
33536
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
32071
33537
|
*
|
|
@@ -39586,6 +41052,27 @@ const set = (object, path, value) => {
|
|
|
39586
41052
|
}
|
|
39587
41053
|
};
|
|
39588
41054
|
|
|
41055
|
+
/**
|
|
41056
|
+
* Calculate the percentage difference between two numbers.
|
|
41057
|
+
* @param {number} a - The first number.
|
|
41058
|
+
* @param {number} b - The second number.
|
|
41059
|
+
* @returns {number} The percentage difference between the two numbers.
|
|
41060
|
+
*/
|
|
41061
|
+
const percentDiff = (a = 1, b = 2) => {
|
|
41062
|
+
const result = 100 / (Math.min(a, b) / Math.max(a, b)) - 100;
|
|
41063
|
+
return Number.isFinite(result) ? result : 100;
|
|
41064
|
+
};
|
|
41065
|
+
|
|
41066
|
+
/**
|
|
41067
|
+
* Calculate the percentage change from yesterday's value to today's value.
|
|
41068
|
+
* @param {number} yesterdayValue - The value from yesterday.
|
|
41069
|
+
* @param {number} todayValue - The value from today.
|
|
41070
|
+
* @returns {number} The percentage change from yesterday to today.
|
|
41071
|
+
*/
|
|
41072
|
+
const percentValue = (yesterdayValue, todayValue) => {
|
|
41073
|
+
return yesterdayValue / todayValue - 1;
|
|
41074
|
+
};
|
|
41075
|
+
|
|
39589
41076
|
exports.ActionBase = ActionBase;
|
|
39590
41077
|
exports.Backtest = Backtest;
|
|
39591
41078
|
exports.Breakeven = Breakeven;
|
|
@@ -39641,7 +41128,9 @@ exports.commitBreakeven = commitBreakeven;
|
|
|
39641
41128
|
exports.commitCancelScheduled = commitCancelScheduled;
|
|
39642
41129
|
exports.commitClosePending = commitClosePending;
|
|
39643
41130
|
exports.commitPartialLoss = commitPartialLoss;
|
|
41131
|
+
exports.commitPartialLossCost = commitPartialLossCost;
|
|
39644
41132
|
exports.commitPartialProfit = commitPartialProfit;
|
|
41133
|
+
exports.commitPartialProfitCost = commitPartialProfitCost;
|
|
39645
41134
|
exports.commitTrailingStop = commitTrailingStop;
|
|
39646
41135
|
exports.commitTrailingTake = commitTrailingTake;
|
|
39647
41136
|
exports.dumpMessages = dumpMessages;
|
|
@@ -39653,6 +41142,7 @@ exports.getActionSchema = getActionSchema;
|
|
|
39653
41142
|
exports.getAggregatedTrades = getAggregatedTrades;
|
|
39654
41143
|
exports.getAveragePrice = getAveragePrice;
|
|
39655
41144
|
exports.getBacktestTimeframe = getBacktestTimeframe;
|
|
41145
|
+
exports.getBreakeven = getBreakeven;
|
|
39656
41146
|
exports.getCandles = getCandles;
|
|
39657
41147
|
exports.getColumns = getColumns;
|
|
39658
41148
|
exports.getConfig = getConfig;
|
|
@@ -39660,19 +41150,33 @@ exports.getContext = getContext;
|
|
|
39660
41150
|
exports.getDate = getDate;
|
|
39661
41151
|
exports.getDefaultColumns = getDefaultColumns;
|
|
39662
41152
|
exports.getDefaultConfig = getDefaultConfig;
|
|
41153
|
+
exports.getEffectivePriceOpen = getEffectivePriceOpen;
|
|
39663
41154
|
exports.getExchangeSchema = getExchangeSchema;
|
|
39664
41155
|
exports.getFrameSchema = getFrameSchema;
|
|
39665
41156
|
exports.getMode = getMode;
|
|
39666
41157
|
exports.getNextCandles = getNextCandles;
|
|
39667
41158
|
exports.getOrderBook = getOrderBook;
|
|
41159
|
+
exports.getPendingSignal = getPendingSignal;
|
|
41160
|
+
exports.getPositionAveragePrice = getPositionAveragePrice;
|
|
41161
|
+
exports.getPositionInvestedCost = getPositionInvestedCost;
|
|
41162
|
+
exports.getPositionInvestedCount = getPositionInvestedCount;
|
|
41163
|
+
exports.getPositionLevels = getPositionLevels;
|
|
41164
|
+
exports.getPositionPartials = getPositionPartials;
|
|
41165
|
+
exports.getPositionPnlCost = getPositionPnlCost;
|
|
41166
|
+
exports.getPositionPnlPercent = getPositionPnlPercent;
|
|
39668
41167
|
exports.getRawCandles = getRawCandles;
|
|
39669
41168
|
exports.getRiskSchema = getRiskSchema;
|
|
41169
|
+
exports.getScheduledSignal = getScheduledSignal;
|
|
39670
41170
|
exports.getSizingSchema = getSizingSchema;
|
|
39671
41171
|
exports.getStrategySchema = getStrategySchema;
|
|
39672
41172
|
exports.getSymbol = getSymbol;
|
|
39673
41173
|
exports.getTimestamp = getTimestamp;
|
|
41174
|
+
exports.getTotalClosed = getTotalClosed;
|
|
41175
|
+
exports.getTotalCostClosed = getTotalCostClosed;
|
|
41176
|
+
exports.getTotalPercentClosed = getTotalPercentClosed;
|
|
39674
41177
|
exports.getWalkerSchema = getWalkerSchema;
|
|
39675
41178
|
exports.hasTradeContext = hasTradeContext;
|
|
41179
|
+
exports.investedCostToPercent = investedCostToPercent;
|
|
39676
41180
|
exports.lib = backtest;
|
|
39677
41181
|
exports.listExchangeSchema = listExchangeSchema;
|
|
39678
41182
|
exports.listFrameSchema = listFrameSchema;
|
|
@@ -39723,6 +41227,8 @@ exports.overrideSizingSchema = overrideSizingSchema;
|
|
|
39723
41227
|
exports.overrideStrategySchema = overrideStrategySchema;
|
|
39724
41228
|
exports.overrideWalkerSchema = overrideWalkerSchema;
|
|
39725
41229
|
exports.parseArgs = parseArgs;
|
|
41230
|
+
exports.percentDiff = percentDiff;
|
|
41231
|
+
exports.percentValue = percentValue;
|
|
39726
41232
|
exports.roundTicks = roundTicks;
|
|
39727
41233
|
exports.set = set;
|
|
39728
41234
|
exports.setColumns = setColumns;
|
|
@@ -39730,6 +41236,7 @@ exports.setConfig = setConfig;
|
|
|
39730
41236
|
exports.setLogger = setLogger;
|
|
39731
41237
|
exports.shutdown = shutdown;
|
|
39732
41238
|
exports.stopStrategy = stopStrategy;
|
|
41239
|
+
exports.toProfitLossDto = toProfitLossDto;
|
|
39733
41240
|
exports.validate = validate;
|
|
39734
41241
|
exports.waitForCandle = waitForCandle;
|
|
39735
41242
|
exports.warmCandles = warmCandles;
|