backtest-kit 3.6.0 → 3.7.1
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 +989 -184
- package/build/index.mjs +980 -185
- package/package.json +1 -1
- package/types.d.ts +508 -7
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
|
|
|
@@ -801,6 +809,11 @@ const PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA = "PersistLogUtils.writeLogData";
|
|
|
801
809
|
const PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON = "PersistLogUtils.useJson";
|
|
802
810
|
const PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY = "PersistLogUtils.useDummy";
|
|
803
811
|
const PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER = "PersistLogUtils.usePersistLogAdapter";
|
|
812
|
+
const PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA = "PersistMeasureUtils.readMeasureData";
|
|
813
|
+
const PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA = "PersistMeasureUtils.writeMeasureData";
|
|
814
|
+
const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON = "PersistMeasureUtils.useJson";
|
|
815
|
+
const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY = "PersistMeasureUtils.useDummy";
|
|
816
|
+
const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER = "PersistMeasureUtils.usePersistMeasureAdapter";
|
|
804
817
|
const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
|
|
805
818
|
const BASE_UNLINK_RETRY_COUNT = 5;
|
|
806
819
|
const BASE_UNLINK_RETRY_DELAY = 1000;
|
|
@@ -2071,6 +2084,92 @@ class PersistLogUtils {
|
|
|
2071
2084
|
* Used by LogPersistUtils for log entry persistence.
|
|
2072
2085
|
*/
|
|
2073
2086
|
const PersistLogAdapter = new PersistLogUtils();
|
|
2087
|
+
/**
|
|
2088
|
+
* Utility class for managing external API response cache persistence.
|
|
2089
|
+
*
|
|
2090
|
+
* Features:
|
|
2091
|
+
* - Memoized storage instances per cache bucket (aligned timestamp + symbol)
|
|
2092
|
+
* - Custom adapter support
|
|
2093
|
+
* - Atomic read/write operations
|
|
2094
|
+
* - Crash-safe cache state management
|
|
2095
|
+
*
|
|
2096
|
+
* Used by Cache.file for persistent caching of external API responses.
|
|
2097
|
+
*/
|
|
2098
|
+
class PersistMeasureUtils {
|
|
2099
|
+
constructor() {
|
|
2100
|
+
this.PersistMeasureFactory = PersistBase;
|
|
2101
|
+
this.getMeasureStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistMeasureFactory, [
|
|
2102
|
+
bucket,
|
|
2103
|
+
`./dump/data/measure/`,
|
|
2104
|
+
]));
|
|
2105
|
+
/**
|
|
2106
|
+
* Reads cached measure data for a given bucket and key.
|
|
2107
|
+
*
|
|
2108
|
+
* @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
|
|
2109
|
+
* @param key - Dynamic cache key within the bucket
|
|
2110
|
+
* @returns Promise resolving to cached value or null if not found
|
|
2111
|
+
*/
|
|
2112
|
+
this.readMeasureData = async (bucket, key) => {
|
|
2113
|
+
bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, {
|
|
2114
|
+
bucket,
|
|
2115
|
+
key,
|
|
2116
|
+
});
|
|
2117
|
+
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
2118
|
+
const stateStorage = this.getMeasureStorage(bucket);
|
|
2119
|
+
await stateStorage.waitForInit(isInitial);
|
|
2120
|
+
if (await stateStorage.hasValue(key)) {
|
|
2121
|
+
return await stateStorage.readValue(key);
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
};
|
|
2125
|
+
/**
|
|
2126
|
+
* Writes measure data to disk with atomic file writes.
|
|
2127
|
+
*
|
|
2128
|
+
* @param data - Data to cache
|
|
2129
|
+
* @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
|
|
2130
|
+
* @param key - Dynamic cache key within the bucket
|
|
2131
|
+
* @returns Promise that resolves when write is complete
|
|
2132
|
+
*/
|
|
2133
|
+
this.writeMeasureData = async (data, bucket, key) => {
|
|
2134
|
+
bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, {
|
|
2135
|
+
bucket,
|
|
2136
|
+
key,
|
|
2137
|
+
});
|
|
2138
|
+
const isInitial = !this.getMeasureStorage.has(bucket);
|
|
2139
|
+
const stateStorage = this.getMeasureStorage(bucket);
|
|
2140
|
+
await stateStorage.waitForInit(isInitial);
|
|
2141
|
+
await stateStorage.writeValue(key, data);
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Registers a custom persistence adapter.
|
|
2146
|
+
*
|
|
2147
|
+
* @param Ctor - Custom PersistBase constructor
|
|
2148
|
+
*/
|
|
2149
|
+
usePersistMeasureAdapter(Ctor) {
|
|
2150
|
+
bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
|
|
2151
|
+
this.PersistMeasureFactory = Ctor;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Switches to the default JSON persist adapter.
|
|
2155
|
+
*/
|
|
2156
|
+
useJson() {
|
|
2157
|
+
bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
|
|
2158
|
+
this.usePersistMeasureAdapter(PersistBase);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Switches to a dummy persist adapter that discards all writes.
|
|
2162
|
+
*/
|
|
2163
|
+
useDummy() {
|
|
2164
|
+
bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2165
|
+
this.usePersistMeasureAdapter(PersistDummy);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Global singleton instance of PersistMeasureUtils.
|
|
2170
|
+
* Used by Cache.file for persistent caching of external API responses.
|
|
2171
|
+
*/
|
|
2172
|
+
const PersistMeasureAdapter = new PersistMeasureUtils();
|
|
2074
2173
|
|
|
2075
2174
|
var _a$3, _b$3;
|
|
2076
2175
|
const BUSY_DELAY = 100;
|
|
@@ -3103,75 +3202,90 @@ class ExchangeConnectionService {
|
|
|
3103
3202
|
}
|
|
3104
3203
|
}
|
|
3105
3204
|
|
|
3205
|
+
const COST_BASIS_PER_ENTRY$3 = 100;
|
|
3106
3206
|
/**
|
|
3107
3207
|
* Returns the effective entry price for price calculations.
|
|
3108
3208
|
*
|
|
3109
|
-
*
|
|
3110
|
-
* the simple arithmetic mean of all entry prices (DCA average).
|
|
3111
|
-
* Otherwise returns the original signal.priceOpen.
|
|
3209
|
+
* Uses harmonic mean (correct for fixed-dollar DCA: $100 per entry).
|
|
3112
3210
|
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
* 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.
|
|
3116
3213
|
*
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
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
|
|
3119
3224
|
*/
|
|
3120
3225
|
const getEffectivePriceOpen = (signal) => {
|
|
3121
|
-
if (signal._entry
|
|
3122
|
-
return signal.
|
|
3123
|
-
|
|
3124
|
-
|
|
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);
|
|
3125
3264
|
};
|
|
3126
3265
|
|
|
3266
|
+
const COST_BASIS_PER_ENTRY$2 = 100;
|
|
3127
3267
|
/**
|
|
3128
3268
|
* Calculates profit/loss for a closed signal with slippage and fees.
|
|
3129
3269
|
*
|
|
3130
3270
|
* For signals with partial closes:
|
|
3131
|
-
* -
|
|
3132
|
-
*
|
|
3133
|
-
*
|
|
3134
|
-
*
|
|
3135
|
-
*
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
3141
|
-
*
|
|
3142
|
-
*
|
|
3143
|
-
*
|
|
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
|
|
3144
3285
|
*
|
|
3145
3286
|
* @param signal - Closed signal with position details and optional partial history
|
|
3146
3287
|
* @param priceClose - Actual close price at final exit
|
|
3147
3288
|
* @returns PNL data with percentage and prices
|
|
3148
|
-
*
|
|
3149
|
-
* @example
|
|
3150
|
-
* ```typescript
|
|
3151
|
-
* // Signal without partial closes
|
|
3152
|
-
* const pnl = toProfitLossDto(
|
|
3153
|
-
* {
|
|
3154
|
-
* position: "long",
|
|
3155
|
-
* priceOpen: 100,
|
|
3156
|
-
* },
|
|
3157
|
-
* 110 // close at +10%
|
|
3158
|
-
* );
|
|
3159
|
-
* console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
|
|
3160
|
-
*
|
|
3161
|
-
* // Signal with partial closes
|
|
3162
|
-
* const pnlPartial = toProfitLossDto(
|
|
3163
|
-
* {
|
|
3164
|
-
* position: "long",
|
|
3165
|
-
* priceOpen: 100,
|
|
3166
|
-
* _partial: [
|
|
3167
|
-
* { type: "profit", percent: 30, price: 120 }, // +20% on 30%
|
|
3168
|
-
* { type: "profit", percent: 40, price: 115 }, // +15% on 40%
|
|
3169
|
-
* ],
|
|
3170
|
-
* },
|
|
3171
|
-
* 105 // final close at +5% for remaining 30%
|
|
3172
|
-
* );
|
|
3173
|
-
* // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
|
|
3174
|
-
* ```
|
|
3175
3289
|
*/
|
|
3176
3290
|
const toProfitLossDto = (signal, priceClose) => {
|
|
3177
3291
|
const priceOpen = getEffectivePriceOpen(signal);
|
|
@@ -3180,47 +3294,65 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3180
3294
|
let totalWeightedPnl = 0;
|
|
3181
3295
|
// Open fee is paid once for the whole position
|
|
3182
3296
|
let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
|
|
3183
|
-
//
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3186
|
-
|
|
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;
|
|
3187
3302
|
// Calculate PNL for each partial close
|
|
3188
|
-
for (
|
|
3189
|
-
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);
|
|
3190
3320
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3191
3321
|
? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3192
3322
|
: partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3193
|
-
// Calculate PNL for this partial
|
|
3194
3323
|
const partialPnl = signal.position === "long"
|
|
3195
3324
|
? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
|
|
3196
3325
|
: ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
const
|
|
3209
|
-
|
|
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);
|
|
3210
3344
|
const priceCloseWithSlippage = signal.position === "long"
|
|
3211
3345
|
? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
|
|
3212
3346
|
: priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3213
|
-
// Calculate PNL for remaining
|
|
3214
3347
|
const remainingPnl = signal.position === "long"
|
|
3215
|
-
? ((priceCloseWithSlippage -
|
|
3216
|
-
: ((
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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);
|
|
3221
3355
|
}
|
|
3222
|
-
// Subtract total fees from weighted PNL
|
|
3223
|
-
// totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
|
|
3224
3356
|
const pnlPercentage = totalWeightedPnl - totalFees;
|
|
3225
3357
|
return {
|
|
3226
3358
|
pnlPercentage,
|
|
@@ -3232,33 +3364,24 @@ const toProfitLossDto = (signal, priceClose) => {
|
|
|
3232
3364
|
let priceOpenWithSlippage;
|
|
3233
3365
|
let priceCloseWithSlippage;
|
|
3234
3366
|
if (signal.position === "long") {
|
|
3235
|
-
// LONG: покупаем дороже, продаем дешевле
|
|
3236
3367
|
priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3237
3368
|
priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3238
3369
|
}
|
|
3239
3370
|
else {
|
|
3240
|
-
// SHORT: продаем дешевле, покупаем дороже
|
|
3241
3371
|
priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3242
3372
|
priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
|
|
3243
3373
|
}
|
|
3244
|
-
|
|
3245
|
-
|
|
3374
|
+
const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE *
|
|
3375
|
+
(1 + priceCloseWithSlippage / priceOpenWithSlippage);
|
|
3246
3376
|
let pnlPercentage;
|
|
3247
3377
|
if (signal.position === "long") {
|
|
3248
|
-
// LONG: прибыль при росте цены
|
|
3249
3378
|
pnlPercentage =
|
|
3250
|
-
((priceCloseWithSlippage - priceOpenWithSlippage) /
|
|
3251
|
-
priceOpenWithSlippage) *
|
|
3252
|
-
100;
|
|
3379
|
+
((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3253
3380
|
}
|
|
3254
3381
|
else {
|
|
3255
|
-
// SHORT: прибыль при падении цены
|
|
3256
3382
|
pnlPercentage =
|
|
3257
|
-
((priceOpenWithSlippage - priceCloseWithSlippage) /
|
|
3258
|
-
priceOpenWithSlippage) *
|
|
3259
|
-
100;
|
|
3383
|
+
((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
|
|
3260
3384
|
}
|
|
3261
|
-
// Вычитаем комиссии
|
|
3262
3385
|
pnlPercentage -= totalFee;
|
|
3263
3386
|
return {
|
|
3264
3387
|
pnlPercentage,
|
|
@@ -3314,6 +3437,54 @@ const toPlainString = (content) => {
|
|
|
3314
3437
|
return text.trim();
|
|
3315
3438
|
};
|
|
3316
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
|
+
|
|
3317
3488
|
/**
|
|
3318
3489
|
* Wraps a function to execute it outside of the current execution context if one exists.
|
|
3319
3490
|
*
|
|
@@ -3356,6 +3527,19 @@ const beginTime = (run) => (...args) => {
|
|
|
3356
3527
|
return fn();
|
|
3357
3528
|
};
|
|
3358
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
|
+
|
|
3359
3543
|
const INTERVAL_MINUTES$6 = {
|
|
3360
3544
|
"1m": 1,
|
|
3361
3545
|
"3m": 3,
|
|
@@ -3364,6 +3548,7 @@ const INTERVAL_MINUTES$6 = {
|
|
|
3364
3548
|
"30m": 30,
|
|
3365
3549
|
"1h": 60,
|
|
3366
3550
|
};
|
|
3551
|
+
const COST_BASIS_PER_ENTRY = 100;
|
|
3367
3552
|
/**
|
|
3368
3553
|
* Mock value for scheduled signal pendingAt timestamp.
|
|
3369
3554
|
* Used to indicate that the actual pendingAt will be set upon activation.
|
|
@@ -3965,7 +4150,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
3965
4150
|
scheduledAt: currentTime,
|
|
3966
4151
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
3967
4152
|
_isScheduled: false,
|
|
3968
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4153
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
3969
4154
|
};
|
|
3970
4155
|
// Валидируем сигнал перед возвратом
|
|
3971
4156
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -3987,7 +4172,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
3987
4172
|
scheduledAt: currentTime,
|
|
3988
4173
|
pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
|
|
3989
4174
|
_isScheduled: true,
|
|
3990
|
-
_entry: [{ price: signal.priceOpen }],
|
|
4175
|
+
_entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
|
|
3991
4176
|
};
|
|
3992
4177
|
// Валидируем сигнал перед возвратом
|
|
3993
4178
|
VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
|
|
@@ -4005,7 +4190,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
4005
4190
|
scheduledAt: currentTime,
|
|
4006
4191
|
pendingAt: currentTime, // Для immediate signal оба времени одинаковые
|
|
4007
4192
|
_isScheduled: false,
|
|
4008
|
-
_entry: [{ price: currentPrice }],
|
|
4193
|
+
_entry: [{ price: currentPrice, debugTimestamp: currentTime }],
|
|
4009
4194
|
};
|
|
4010
4195
|
// Валидируем сигнал перед возвратом
|
|
4011
4196
|
VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
|
|
@@ -4079,37 +4264,39 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4079
4264
|
// Initialize partial array if not present
|
|
4080
4265
|
if (!signal._partial)
|
|
4081
4266
|
signal._partial = [];
|
|
4082
|
-
//
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
const
|
|
4087
|
-
|
|
4088
|
-
.
|
|
4089
|
-
const totalClosed = tpClosed + slClosed;
|
|
4090
|
-
// Check if would exceed 100% total closed
|
|
4091
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4092
|
-
if (newTotalClosed > 100) {
|
|
4093
|
-
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", {
|
|
4094
4274
|
signalId: signal.id,
|
|
4095
|
-
|
|
4275
|
+
totalClosedPercent,
|
|
4276
|
+
remainingCostBasis,
|
|
4096
4277
|
percentToClose,
|
|
4097
|
-
|
|
4278
|
+
newPartialDollar,
|
|
4279
|
+
totalInvested,
|
|
4098
4280
|
});
|
|
4099
4281
|
return false;
|
|
4100
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;
|
|
4101
4286
|
// Add new partial close entry
|
|
4102
4287
|
signal._partial.push({
|
|
4103
4288
|
type: "profit",
|
|
4104
4289
|
percent: percentToClose,
|
|
4290
|
+
entryCountAtClose,
|
|
4105
4291
|
price: currentPrice,
|
|
4292
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4293
|
+
effectivePrice,
|
|
4106
4294
|
});
|
|
4107
4295
|
self.params.logger.info("PARTIAL_PROFIT_FN executed", {
|
|
4108
4296
|
signalId: signal.id,
|
|
4109
4297
|
percentClosed: percentToClose,
|
|
4110
|
-
|
|
4298
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4111
4299
|
currentPrice,
|
|
4112
|
-
tpClosed: tpClosed + percentToClose,
|
|
4113
4300
|
});
|
|
4114
4301
|
return true;
|
|
4115
4302
|
};
|
|
@@ -4117,37 +4304,39 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
4117
4304
|
// Initialize partial array if not present
|
|
4118
4305
|
if (!signal._partial)
|
|
4119
4306
|
signal._partial = [];
|
|
4120
|
-
//
|
|
4121
|
-
const
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4126
|
-
.
|
|
4127
|
-
const totalClosed = tpClosed + slClosed;
|
|
4128
|
-
// Check if would exceed 100% total closed
|
|
4129
|
-
const newTotalClosed = totalClosed + percentToClose;
|
|
4130
|
-
if (newTotalClosed > 100) {
|
|
4131
|
-
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", {
|
|
4132
4314
|
signalId: signal.id,
|
|
4133
|
-
|
|
4315
|
+
totalClosedPercent,
|
|
4316
|
+
remainingCostBasis,
|
|
4134
4317
|
percentToClose,
|
|
4135
|
-
|
|
4318
|
+
newPartialDollar,
|
|
4319
|
+
totalInvested,
|
|
4136
4320
|
});
|
|
4137
4321
|
return false;
|
|
4138
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;
|
|
4139
4326
|
// Add new partial close entry
|
|
4140
4327
|
signal._partial.push({
|
|
4141
4328
|
type: "loss",
|
|
4142
4329
|
percent: percentToClose,
|
|
4143
4330
|
price: currentPrice,
|
|
4331
|
+
entryCountAtClose,
|
|
4332
|
+
effectivePrice,
|
|
4333
|
+
debugTimestamp: getDebugTimestamp(),
|
|
4144
4334
|
});
|
|
4145
4335
|
self.params.logger.warn("PARTIAL_LOSS_FN executed", {
|
|
4146
4336
|
signalId: signal.id,
|
|
4147
4337
|
percentClosed: percentToClose,
|
|
4148
|
-
|
|
4338
|
+
totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
|
|
4149
4339
|
currentPrice,
|
|
4150
|
-
slClosed: slClosed + percentToClose,
|
|
4151
4340
|
});
|
|
4152
4341
|
return true;
|
|
4153
4342
|
};
|
|
@@ -4541,12 +4730,12 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
|
|
|
4541
4730
|
const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
4542
4731
|
// Ensure _entry is initialized (handles signals loaded from disk without _entry)
|
|
4543
4732
|
if (!signal._entry || signal._entry.length === 0) {
|
|
4544
|
-
signal._entry = [{ price: signal.priceOpen }];
|
|
4733
|
+
signal._entry = [{ price: signal.priceOpen, debugTimestamp: getDebugTimestamp() }];
|
|
4545
4734
|
}
|
|
4546
4735
|
const lastEntry = signal._entry[signal._entry.length - 1];
|
|
4547
4736
|
if (signal.position === "long") {
|
|
4548
4737
|
// LONG: averaging down = currentPrice must be strictly lower than last entry
|
|
4549
|
-
if (currentPrice >= lastEntry.price) {
|
|
4738
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= lastEntry.price) {
|
|
4550
4739
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
|
|
4551
4740
|
signalId: signal.id,
|
|
4552
4741
|
position: signal.position,
|
|
@@ -4559,7 +4748,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4559
4748
|
}
|
|
4560
4749
|
else {
|
|
4561
4750
|
// SHORT: averaging down = currentPrice must be strictly higher than last entry
|
|
4562
|
-
if (currentPrice <= lastEntry.price) {
|
|
4751
|
+
if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= lastEntry.price) {
|
|
4563
4752
|
self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
|
|
4564
4753
|
signalId: signal.id,
|
|
4565
4754
|
position: signal.position,
|
|
@@ -4570,7 +4759,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
|
|
|
4570
4759
|
return false;
|
|
4571
4760
|
}
|
|
4572
4761
|
}
|
|
4573
|
-
signal._entry.push({ price: currentPrice });
|
|
4762
|
+
signal._entry.push({ price: currentPrice, debugTimestamp: getDebugTimestamp() });
|
|
4574
4763
|
self.params.logger.info("AVERAGE_BUY_FN executed", {
|
|
4575
4764
|
signalId: signal.id,
|
|
4576
4765
|
position: signal.position,
|
|
@@ -6035,6 +6224,52 @@ class ClientStrategy {
|
|
|
6035
6224
|
});
|
|
6036
6225
|
return this._isStopped;
|
|
6037
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
|
+
}
|
|
6038
6273
|
/**
|
|
6039
6274
|
* Performs a single tick of strategy execution.
|
|
6040
6275
|
*
|
|
@@ -7495,14 +7730,6 @@ class ClientStrategy {
|
|
|
7495
7730
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
7496
7731
|
throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
7497
7732
|
}
|
|
7498
|
-
// Reject if any partial closes have already been executed
|
|
7499
|
-
if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
|
|
7500
|
-
this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
|
|
7501
|
-
symbol,
|
|
7502
|
-
partialCount: this._pendingSignal._partial.length,
|
|
7503
|
-
});
|
|
7504
|
-
return false;
|
|
7505
|
-
}
|
|
7506
7733
|
// Execute averaging logic
|
|
7507
7734
|
const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
|
|
7508
7735
|
if (!result) {
|
|
@@ -8181,6 +8408,43 @@ class StrategyConnectionService {
|
|
|
8181
8408
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8182
8409
|
return await strategy.getPendingSignal(symbol);
|
|
8183
8410
|
};
|
|
8411
|
+
/**
|
|
8412
|
+
* Returns the percentage of the position currently held (not closed).
|
|
8413
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
8414
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8415
|
+
*
|
|
8416
|
+
* @param backtest - Whether running in backtest mode
|
|
8417
|
+
* @param symbol - Trading pair symbol
|
|
8418
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8419
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
8420
|
+
*/
|
|
8421
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
8422
|
+
this.loggerService.log("strategyConnectionService getTotalPercentClosed", {
|
|
8423
|
+
symbol,
|
|
8424
|
+
context,
|
|
8425
|
+
backtest,
|
|
8426
|
+
});
|
|
8427
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8428
|
+
return await strategy.getTotalPercentClosed(symbol);
|
|
8429
|
+
};
|
|
8430
|
+
/**
|
|
8431
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
8432
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
8433
|
+
*
|
|
8434
|
+
* @param backtest - Whether running in backtest mode
|
|
8435
|
+
* @param symbol - Trading pair symbol
|
|
8436
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8437
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
8438
|
+
*/
|
|
8439
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
8440
|
+
this.loggerService.log("strategyConnectionService getTotalCostClosed", {
|
|
8441
|
+
symbol,
|
|
8442
|
+
context,
|
|
8443
|
+
backtest,
|
|
8444
|
+
});
|
|
8445
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8446
|
+
return await strategy.getTotalCostClosed(symbol);
|
|
8447
|
+
};
|
|
8184
8448
|
/**
|
|
8185
8449
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
8186
8450
|
* If no scheduled signal exists, returns null.
|
|
@@ -10371,7 +10635,7 @@ class ActionBase {
|
|
|
10371
10635
|
* @example
|
|
10372
10636
|
* ```typescript
|
|
10373
10637
|
* pingScheduled(event: SchedulePingContract) {
|
|
10374
|
-
* const waitTime =
|
|
10638
|
+
* const waitTime = getTimestamp() - event.data.timestampScheduled;
|
|
10375
10639
|
* const waitMinutes = Math.floor(waitTime / 60000);
|
|
10376
10640
|
* console.log(`Scheduled signal waiting ${waitMinutes} minutes`);
|
|
10377
10641
|
* }
|
|
@@ -10400,7 +10664,7 @@ class ActionBase {
|
|
|
10400
10664
|
* @example
|
|
10401
10665
|
* ```typescript
|
|
10402
10666
|
* pingActive(event: ActivePingContract) {
|
|
10403
|
-
* const holdTime =
|
|
10667
|
+
* const holdTime = getTimestamp() - event.data.pendingAt;
|
|
10404
10668
|
* const holdMinutes = Math.floor(holdTime / 60000);
|
|
10405
10669
|
* console.log(`Active signal holding ${holdMinutes} minutes`);
|
|
10406
10670
|
* }
|
|
@@ -11584,6 +11848,41 @@ class StrategyCoreService {
|
|
|
11584
11848
|
await this.validate(context);
|
|
11585
11849
|
return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
|
|
11586
11850
|
};
|
|
11851
|
+
/**
|
|
11852
|
+
* Returns the percentage of the position currently held (not closed).
|
|
11853
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
11854
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
11855
|
+
*
|
|
11856
|
+
* @param backtest - Whether running in backtest mode
|
|
11857
|
+
* @param symbol - Trading pair symbol
|
|
11858
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
11859
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
11860
|
+
*/
|
|
11861
|
+
this.getTotalPercentClosed = async (backtest, symbol, context) => {
|
|
11862
|
+
this.loggerService.log("strategyCoreService getTotalPercentClosed", {
|
|
11863
|
+
symbol,
|
|
11864
|
+
context,
|
|
11865
|
+
});
|
|
11866
|
+
await this.validate(context);
|
|
11867
|
+
return await this.strategyConnectionService.getTotalPercentClosed(backtest, symbol, context);
|
|
11868
|
+
};
|
|
11869
|
+
/**
|
|
11870
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
11871
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
11872
|
+
*
|
|
11873
|
+
* @param backtest - Whether running in backtest mode
|
|
11874
|
+
* @param symbol - Trading pair symbol
|
|
11875
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
11876
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
11877
|
+
*/
|
|
11878
|
+
this.getTotalCostClosed = async (backtest, symbol, context) => {
|
|
11879
|
+
this.loggerService.log("strategyCoreService getTotalCostClosed", {
|
|
11880
|
+
symbol,
|
|
11881
|
+
context,
|
|
11882
|
+
});
|
|
11883
|
+
await this.validate(context);
|
|
11884
|
+
return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
|
|
11885
|
+
};
|
|
11587
11886
|
/**
|
|
11588
11887
|
* Retrieves the currently active scheduled signal for the symbol.
|
|
11589
11888
|
* If no scheduled signal exists, returns null.
|
|
@@ -16102,6 +16401,20 @@ const COLUMN_CONFIG = {
|
|
|
16102
16401
|
*/
|
|
16103
16402
|
const DEFAULT_COLUMNS = Object.freeze({ ...COLUMN_CONFIG });
|
|
16104
16403
|
|
|
16404
|
+
/**
|
|
16405
|
+
* Retrieves the current timestamp based on the execution context.
|
|
16406
|
+
* 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.
|
|
16407
|
+
* If no execution context is active (e.g., during live operation), it returns the current real-world timestamp.
|
|
16408
|
+
* This function helps maintain accurate timing for logs, metrics, and other time-sensitive operations across both live and backtest modes.
|
|
16409
|
+
* @return {number} The current timestamp in milliseconds, either from the execution context or the real-world clock.
|
|
16410
|
+
*/
|
|
16411
|
+
const getContextTimestamp = () => {
|
|
16412
|
+
if (ExecutionContextService.hasContext()) {
|
|
16413
|
+
return bt.executionContextService.context.when.getTime();
|
|
16414
|
+
}
|
|
16415
|
+
return Date.now();
|
|
16416
|
+
};
|
|
16417
|
+
|
|
16105
16418
|
var _a$2, _b$2;
|
|
16106
16419
|
const MARKDOWN_METHOD_NAME_ENABLE = "MarkdownUtils.enable";
|
|
16107
16420
|
const MARKDOWN_METHOD_NAME_DISABLE = "MarkdownUtils.disable";
|
|
@@ -16235,7 +16548,7 @@ class MarkdownFileBase {
|
|
|
16235
16548
|
markdownName: this.markdownName,
|
|
16236
16549
|
data,
|
|
16237
16550
|
...searchFlags,
|
|
16238
|
-
timestamp:
|
|
16551
|
+
timestamp: getContextTimestamp(),
|
|
16239
16552
|
}) + "\n";
|
|
16240
16553
|
const status = await this[WRITE_SAFE_SYMBOL$2](line);
|
|
16241
16554
|
if (status === functoolsKit.TIMEOUT_SYMBOL) {
|
|
@@ -16779,7 +17092,7 @@ let ReportStorage$7 = class ReportStorage {
|
|
|
16779
17092
|
*/
|
|
16780
17093
|
async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
|
|
16781
17094
|
const markdown = await this.getReport(strategyName, columns);
|
|
16782
|
-
const timestamp =
|
|
17095
|
+
const timestamp = getContextTimestamp();
|
|
16783
17096
|
const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
16784
17097
|
await Markdown.writeData("backtest", markdown, {
|
|
16785
17098
|
path,
|
|
@@ -17114,7 +17427,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
17114
17427
|
*/
|
|
17115
17428
|
addIdleEvent(currentPrice) {
|
|
17116
17429
|
const newEvent = {
|
|
17117
|
-
timestamp:
|
|
17430
|
+
timestamp: getContextTimestamp(),
|
|
17118
17431
|
action: "idle",
|
|
17119
17432
|
currentPrice,
|
|
17120
17433
|
};
|
|
@@ -17170,7 +17483,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
17170
17483
|
*/
|
|
17171
17484
|
addActiveEvent(data) {
|
|
17172
17485
|
const newEvent = {
|
|
17173
|
-
timestamp:
|
|
17486
|
+
timestamp: getContextTimestamp(),
|
|
17174
17487
|
action: "active",
|
|
17175
17488
|
symbol: data.signal.symbol,
|
|
17176
17489
|
signalId: data.signal.id,
|
|
@@ -17272,7 +17585,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
17272
17585
|
*/
|
|
17273
17586
|
addWaitingEvent(data) {
|
|
17274
17587
|
const newEvent = {
|
|
17275
|
-
timestamp:
|
|
17588
|
+
timestamp: getContextTimestamp(),
|
|
17276
17589
|
action: "waiting",
|
|
17277
17590
|
symbol: data.signal.symbol,
|
|
17278
17591
|
signalId: data.signal.id,
|
|
@@ -17466,7 +17779,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
17466
17779
|
*/
|
|
17467
17780
|
async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
|
|
17468
17781
|
const markdown = await this.getReport(strategyName, columns);
|
|
17469
|
-
const timestamp =
|
|
17782
|
+
const timestamp = getContextTimestamp();
|
|
17470
17783
|
const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
17471
17784
|
await Markdown.writeData("live", markdown, {
|
|
17472
17785
|
path,
|
|
@@ -17998,7 +18311,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
17998
18311
|
*/
|
|
17999
18312
|
async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
|
|
18000
18313
|
const markdown = await this.getReport(strategyName, columns);
|
|
18001
|
-
const timestamp =
|
|
18314
|
+
const timestamp = getContextTimestamp();
|
|
18002
18315
|
const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
18003
18316
|
await Markdown.writeData("schedule", markdown, {
|
|
18004
18317
|
path,
|
|
@@ -18463,7 +18776,7 @@ class PerformanceStorage {
|
|
|
18463
18776
|
*/
|
|
18464
18777
|
async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
|
|
18465
18778
|
const markdown = await this.getReport(strategyName, columns);
|
|
18466
|
-
const timestamp =
|
|
18779
|
+
const timestamp = getContextTimestamp();
|
|
18467
18780
|
const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
18468
18781
|
await Markdown.writeData("performance", markdown, {
|
|
18469
18782
|
path,
|
|
@@ -18914,7 +19227,7 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
18914
19227
|
*/
|
|
18915
19228
|
async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
|
|
18916
19229
|
const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
|
|
18917
|
-
const timestamp =
|
|
19230
|
+
const timestamp = getContextTimestamp();
|
|
18918
19231
|
const filename = CREATE_FILE_NAME_FN$5(this.walkerName, timestamp);
|
|
18919
19232
|
await Markdown.writeData("walker", markdown, {
|
|
18920
19233
|
path,
|
|
@@ -19468,7 +19781,7 @@ class HeatmapStorage {
|
|
|
19468
19781
|
*/
|
|
19469
19782
|
async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
|
|
19470
19783
|
const markdown = await this.getReport(strategyName, columns);
|
|
19471
|
-
const timestamp =
|
|
19784
|
+
const timestamp = getContextTimestamp();
|
|
19472
19785
|
const filename = CREATE_FILE_NAME_FN$4(strategyName, this.exchangeName, this.frameName, timestamp);
|
|
19473
19786
|
await Markdown.writeData("heat", markdown, {
|
|
19474
19787
|
path,
|
|
@@ -21148,7 +21461,7 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
21148
21461
|
*/
|
|
21149
21462
|
async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
|
|
21150
21463
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
21151
|
-
const timestamp =
|
|
21464
|
+
const timestamp = getContextTimestamp();
|
|
21152
21465
|
const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
21153
21466
|
await Markdown.writeData("partial", markdown, {
|
|
21154
21467
|
path,
|
|
@@ -22241,7 +22554,7 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
22241
22554
|
*/
|
|
22242
22555
|
async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
|
|
22243
22556
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
22244
|
-
const timestamp =
|
|
22557
|
+
const timestamp = getContextTimestamp();
|
|
22245
22558
|
const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
22246
22559
|
await Markdown.writeData("breakeven", markdown, {
|
|
22247
22560
|
path,
|
|
@@ -22909,7 +23222,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
22909
23222
|
*/
|
|
22910
23223
|
async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
|
|
22911
23224
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
22912
|
-
const timestamp =
|
|
23225
|
+
const timestamp = getContextTimestamp();
|
|
22913
23226
|
const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
22914
23227
|
await Markdown.writeData("risk", markdown, {
|
|
22915
23228
|
path,
|
|
@@ -23393,7 +23706,7 @@ class ReportBase {
|
|
|
23393
23706
|
reportName: this.reportName,
|
|
23394
23707
|
data,
|
|
23395
23708
|
...searchFlags,
|
|
23396
|
-
timestamp:
|
|
23709
|
+
timestamp: getContextTimestamp(),
|
|
23397
23710
|
}) + "\n";
|
|
23398
23711
|
const status = await this[WRITE_SAFE_SYMBOL$1](line);
|
|
23399
23712
|
if (status === functoolsKit.TIMEOUT_SYMBOL) {
|
|
@@ -23732,7 +24045,7 @@ class BacktestReportService {
|
|
|
23732
24045
|
this.tick = async (data) => {
|
|
23733
24046
|
this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_TICK, { data });
|
|
23734
24047
|
const baseEvent = {
|
|
23735
|
-
timestamp:
|
|
24048
|
+
timestamp: getContextTimestamp(),
|
|
23736
24049
|
action: data.action,
|
|
23737
24050
|
symbol: data.symbol,
|
|
23738
24051
|
strategyName: data.strategyName,
|
|
@@ -23911,7 +24224,7 @@ class LiveReportService {
|
|
|
23911
24224
|
this.tick = async (data) => {
|
|
23912
24225
|
this.loggerService.log(LIVE_REPORT_METHOD_NAME_TICK, { data });
|
|
23913
24226
|
const baseEvent = {
|
|
23914
|
-
timestamp:
|
|
24227
|
+
timestamp: getContextTimestamp(),
|
|
23915
24228
|
action: data.action,
|
|
23916
24229
|
symbol: data.symbol,
|
|
23917
24230
|
strategyName: data.strategyName,
|
|
@@ -24434,7 +24747,7 @@ class WalkerReportService {
|
|
|
24434
24747
|
this.tick = async (data) => {
|
|
24435
24748
|
this.loggerService.log(WALKER_REPORT_METHOD_NAME_TICK, { data });
|
|
24436
24749
|
await Report.writeData("walker", {
|
|
24437
|
-
timestamp:
|
|
24750
|
+
timestamp: getContextTimestamp(),
|
|
24438
24751
|
walkerName: data.walkerName,
|
|
24439
24752
|
symbol: data.symbol,
|
|
24440
24753
|
exchangeName: data.exchangeName,
|
|
@@ -24561,7 +24874,7 @@ class HeatReportService {
|
|
|
24561
24874
|
return;
|
|
24562
24875
|
}
|
|
24563
24876
|
await Report.writeData("heat", {
|
|
24564
|
-
timestamp:
|
|
24877
|
+
timestamp: getContextTimestamp(),
|
|
24565
24878
|
action: data.action,
|
|
24566
24879
|
symbol: data.symbol,
|
|
24567
24880
|
strategyName: data.strategyName,
|
|
@@ -25883,7 +26196,7 @@ class ReportStorage {
|
|
|
25883
26196
|
*/
|
|
25884
26197
|
async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
|
|
25885
26198
|
const markdown = await this.getReport(symbol, strategyName, columns);
|
|
25886
|
-
const timestamp =
|
|
26199
|
+
const timestamp = getContextTimestamp();
|
|
25887
26200
|
const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
|
|
25888
26201
|
await Markdown.writeData("strategy", markdown, {
|
|
25889
26202
|
path,
|
|
@@ -28199,6 +28512,7 @@ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
|
28199
28512
|
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
28200
28513
|
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
28201
28514
|
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
28515
|
+
const GET_TIMESTAMP_METHOD_NAME = "exchange.getTimestamp";
|
|
28202
28516
|
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
28203
28517
|
const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
|
|
28204
28518
|
const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
@@ -28364,6 +28678,26 @@ async function getDate() {
|
|
|
28364
28678
|
const { when } = bt.executionContextService.context;
|
|
28365
28679
|
return new Date(when.getTime());
|
|
28366
28680
|
}
|
|
28681
|
+
/**
|
|
28682
|
+
* Gets the current timestamp from execution context.
|
|
28683
|
+
*
|
|
28684
|
+
* In backtest mode: returns the current timeframe timestamp being processed
|
|
28685
|
+
* In live mode: returns current real-time timestamp
|
|
28686
|
+
*
|
|
28687
|
+
* @returns Promise resolving to current execution context timestamp in milliseconds
|
|
28688
|
+
* @example
|
|
28689
|
+
* ```typescript
|
|
28690
|
+
* const timestamp = await getTimestamp();
|
|
28691
|
+
* console.log(timestamp); // 1700000000000
|
|
28692
|
+
* ```
|
|
28693
|
+
*/
|
|
28694
|
+
async function getTimestamp() {
|
|
28695
|
+
bt.loggerService.info(GET_TIMESTAMP_METHOD_NAME);
|
|
28696
|
+
if (!ExecutionContextService.hasContext()) {
|
|
28697
|
+
throw new Error("getTimestamp requires an execution context");
|
|
28698
|
+
}
|
|
28699
|
+
return getContextTimestamp();
|
|
28700
|
+
}
|
|
28367
28701
|
/**
|
|
28368
28702
|
* Gets the current execution mode.
|
|
28369
28703
|
*
|
|
@@ -28580,6 +28914,11 @@ const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
|
|
|
28580
28914
|
const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
|
|
28581
28915
|
const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
|
|
28582
28916
|
const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
|
|
28917
|
+
const GET_TOTAL_PERCENT_CLOSED_METHOD_NAME = "strategy.getTotalPercentClosed";
|
|
28918
|
+
const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
|
|
28919
|
+
const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
|
|
28920
|
+
const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
|
|
28921
|
+
const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
|
|
28583
28922
|
/**
|
|
28584
28923
|
* Cancels the scheduled signal without stopping the strategy.
|
|
28585
28924
|
*
|
|
@@ -28971,6 +29310,173 @@ async function commitAverageBuy(symbol) {
|
|
|
28971
29310
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
28972
29311
|
return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
28973
29312
|
}
|
|
29313
|
+
/**
|
|
29314
|
+
* Returns the percentage of the position currently held (not closed).
|
|
29315
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
29316
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29317
|
+
*
|
|
29318
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29319
|
+
*
|
|
29320
|
+
* @param symbol - Trading pair symbol
|
|
29321
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
29322
|
+
*
|
|
29323
|
+
* @example
|
|
29324
|
+
* ```typescript
|
|
29325
|
+
* import { getTotalPercentClosed } from "backtest-kit";
|
|
29326
|
+
*
|
|
29327
|
+
* const heldPct = await getTotalPercentClosed("BTCUSDT");
|
|
29328
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
29329
|
+
* ```
|
|
29330
|
+
*/
|
|
29331
|
+
async function getTotalPercentClosed(symbol) {
|
|
29332
|
+
bt.loggerService.info(GET_TOTAL_PERCENT_CLOSED_METHOD_NAME, {
|
|
29333
|
+
symbol,
|
|
29334
|
+
});
|
|
29335
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29336
|
+
throw new Error("getTotalPercentClosed requires an execution context");
|
|
29337
|
+
}
|
|
29338
|
+
if (!MethodContextService.hasContext()) {
|
|
29339
|
+
throw new Error("getTotalPercentClosed requires a method context");
|
|
29340
|
+
}
|
|
29341
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29342
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29343
|
+
return await bt.strategyCoreService.getTotalPercentClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29344
|
+
}
|
|
29345
|
+
/**
|
|
29346
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
29347
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
29348
|
+
*
|
|
29349
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29350
|
+
*
|
|
29351
|
+
* @param symbol - Trading pair symbol
|
|
29352
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
29353
|
+
*
|
|
29354
|
+
* @example
|
|
29355
|
+
* ```typescript
|
|
29356
|
+
* import { getTotalCostClosed } from "backtest-kit";
|
|
29357
|
+
*
|
|
29358
|
+
* const heldCost = await getTotalCostClosed("BTCUSDT");
|
|
29359
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
29360
|
+
* ```
|
|
29361
|
+
*/
|
|
29362
|
+
async function getTotalCostClosed(symbol) {
|
|
29363
|
+
bt.loggerService.info(GET_TOTAL_COST_CLOSED_METHOD_NAME, {
|
|
29364
|
+
symbol,
|
|
29365
|
+
});
|
|
29366
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29367
|
+
throw new Error("getTotalCostClosed requires an execution context");
|
|
29368
|
+
}
|
|
29369
|
+
if (!MethodContextService.hasContext()) {
|
|
29370
|
+
throw new Error("getTotalCostClosed requires a method context");
|
|
29371
|
+
}
|
|
29372
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29373
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29374
|
+
return await bt.strategyCoreService.getTotalCostClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29375
|
+
}
|
|
29376
|
+
/**
|
|
29377
|
+
* Returns the currently active pending signal for the strategy.
|
|
29378
|
+
* If no active signal exists, returns null.
|
|
29379
|
+
*
|
|
29380
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29381
|
+
*
|
|
29382
|
+
* @param symbol - Trading pair symbol
|
|
29383
|
+
* @returns Promise resolving to pending signal or null
|
|
29384
|
+
*
|
|
29385
|
+
* @example
|
|
29386
|
+
* ```typescript
|
|
29387
|
+
* import { getPendingSignal } from "backtest-kit";
|
|
29388
|
+
*
|
|
29389
|
+
* const pending = await getPendingSignal("BTCUSDT");
|
|
29390
|
+
* if (pending) {
|
|
29391
|
+
* console.log("Active signal:", pending.id);
|
|
29392
|
+
* }
|
|
29393
|
+
* ```
|
|
29394
|
+
*/
|
|
29395
|
+
async function getPendingSignal(symbol) {
|
|
29396
|
+
bt.loggerService.info(GET_PENDING_SIGNAL_METHOD_NAME, {
|
|
29397
|
+
symbol,
|
|
29398
|
+
});
|
|
29399
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29400
|
+
throw new Error("getPendingSignal requires an execution context");
|
|
29401
|
+
}
|
|
29402
|
+
if (!MethodContextService.hasContext()) {
|
|
29403
|
+
throw new Error("getPendingSignal requires a method context");
|
|
29404
|
+
}
|
|
29405
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29406
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29407
|
+
return await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29408
|
+
}
|
|
29409
|
+
/**
|
|
29410
|
+
* Returns the currently active scheduled signal for the strategy.
|
|
29411
|
+
* If no scheduled signal exists, returns null.
|
|
29412
|
+
*
|
|
29413
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29414
|
+
*
|
|
29415
|
+
* @param symbol - Trading pair symbol
|
|
29416
|
+
* @returns Promise resolving to scheduled signal or null
|
|
29417
|
+
*
|
|
29418
|
+
* @example
|
|
29419
|
+
* ```typescript
|
|
29420
|
+
* import { getScheduledSignal } from "backtest-kit";
|
|
29421
|
+
*
|
|
29422
|
+
* const scheduled = await getScheduledSignal("BTCUSDT");
|
|
29423
|
+
* if (scheduled) {
|
|
29424
|
+
* console.log("Scheduled signal:", scheduled.id);
|
|
29425
|
+
* }
|
|
29426
|
+
* ```
|
|
29427
|
+
*/
|
|
29428
|
+
async function getScheduledSignal(symbol) {
|
|
29429
|
+
bt.loggerService.info(GET_SCHEDULED_SIGNAL_METHOD_NAME, {
|
|
29430
|
+
symbol,
|
|
29431
|
+
});
|
|
29432
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29433
|
+
throw new Error("getScheduledSignal requires an execution context");
|
|
29434
|
+
}
|
|
29435
|
+
if (!MethodContextService.hasContext()) {
|
|
29436
|
+
throw new Error("getScheduledSignal requires a method context");
|
|
29437
|
+
}
|
|
29438
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29439
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29440
|
+
return await bt.strategyCoreService.getScheduledSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
29441
|
+
}
|
|
29442
|
+
/**
|
|
29443
|
+
* Checks if breakeven threshold has been reached for the current pending signal.
|
|
29444
|
+
*
|
|
29445
|
+
* Returns true if price has moved far enough in profit direction to cover
|
|
29446
|
+
* transaction costs. Threshold is calculated as: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2
|
|
29447
|
+
*
|
|
29448
|
+
* Automatically detects backtest/live mode from execution context.
|
|
29449
|
+
*
|
|
29450
|
+
* @param symbol - Trading pair symbol
|
|
29451
|
+
* @param currentPrice - Current market price to check against threshold
|
|
29452
|
+
* @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
|
|
29453
|
+
*
|
|
29454
|
+
* @example
|
|
29455
|
+
* ```typescript
|
|
29456
|
+
* import { getBreakeven, getAveragePrice } from "backtest-kit";
|
|
29457
|
+
*
|
|
29458
|
+
* const price = await getAveragePrice("BTCUSDT");
|
|
29459
|
+
* const canBreakeven = await getBreakeven("BTCUSDT", price);
|
|
29460
|
+
* if (canBreakeven) {
|
|
29461
|
+
* console.log("Breakeven available");
|
|
29462
|
+
* }
|
|
29463
|
+
* ```
|
|
29464
|
+
*/
|
|
29465
|
+
async function getBreakeven(symbol, currentPrice) {
|
|
29466
|
+
bt.loggerService.info(GET_BREAKEVEN_METHOD_NAME, {
|
|
29467
|
+
symbol,
|
|
29468
|
+
currentPrice,
|
|
29469
|
+
});
|
|
29470
|
+
if (!ExecutionContextService.hasContext()) {
|
|
29471
|
+
throw new Error("getBreakeven requires an execution context");
|
|
29472
|
+
}
|
|
29473
|
+
if (!MethodContextService.hasContext()) {
|
|
29474
|
+
throw new Error("getBreakeven requires a method context");
|
|
29475
|
+
}
|
|
29476
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
29477
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
29478
|
+
return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
29479
|
+
}
|
|
28974
29480
|
|
|
28975
29481
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
28976
29482
|
/**
|
|
@@ -30158,6 +30664,8 @@ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
|
|
|
30158
30664
|
const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
|
|
30159
30665
|
const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
|
|
30160
30666
|
const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
|
|
30667
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPercentClosed";
|
|
30668
|
+
const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
|
|
30161
30669
|
const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
|
|
30162
30670
|
const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
|
|
30163
30671
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
@@ -30559,6 +31067,71 @@ class BacktestUtils {
|
|
|
30559
31067
|
}
|
|
30560
31068
|
return await bt.strategyCoreService.getPendingSignal(true, symbol, context);
|
|
30561
31069
|
};
|
|
31070
|
+
/**
|
|
31071
|
+
* Returns the percentage of the position currently held (not closed).
|
|
31072
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
31073
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31074
|
+
*
|
|
31075
|
+
* @param symbol - Trading pair symbol
|
|
31076
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31077
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
31078
|
+
*
|
|
31079
|
+
* @example
|
|
31080
|
+
* ```typescript
|
|
31081
|
+
* const heldPct = await Backtest.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31082
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
31083
|
+
* ```
|
|
31084
|
+
*/
|
|
31085
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
31086
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
31087
|
+
symbol,
|
|
31088
|
+
context,
|
|
31089
|
+
});
|
|
31090
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31091
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31092
|
+
{
|
|
31093
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31094
|
+
riskName &&
|
|
31095
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
31096
|
+
riskList &&
|
|
31097
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31098
|
+
actions &&
|
|
31099
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
31100
|
+
}
|
|
31101
|
+
return await bt.strategyCoreService.getTotalPercentClosed(true, symbol, context);
|
|
31102
|
+
};
|
|
31103
|
+
/**
|
|
31104
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
31105
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
31106
|
+
*
|
|
31107
|
+
* @param symbol - Trading pair symbol
|
|
31108
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
31109
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
31110
|
+
*
|
|
31111
|
+
* @example
|
|
31112
|
+
* ```typescript
|
|
31113
|
+
* const heldCost = await Backtest.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName, frameName });
|
|
31114
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
31115
|
+
* ```
|
|
31116
|
+
*/
|
|
31117
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
31118
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
31119
|
+
symbol,
|
|
31120
|
+
context,
|
|
31121
|
+
});
|
|
31122
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31123
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31124
|
+
{
|
|
31125
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31126
|
+
riskName &&
|
|
31127
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
31128
|
+
riskList &&
|
|
31129
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31130
|
+
actions &&
|
|
31131
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
31132
|
+
}
|
|
31133
|
+
return await bt.strategyCoreService.getTotalCostClosed(true, symbol, context);
|
|
31134
|
+
};
|
|
30562
31135
|
/**
|
|
30563
31136
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
30564
31137
|
* If no scheduled signal exists, returns null.
|
|
@@ -31274,6 +31847,8 @@ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
|
|
|
31274
31847
|
const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
|
|
31275
31848
|
const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
|
|
31276
31849
|
const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
|
|
31850
|
+
const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClosed";
|
|
31851
|
+
const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
|
|
31277
31852
|
const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
|
|
31278
31853
|
const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
|
|
31279
31854
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
@@ -31644,6 +32219,73 @@ class LiveUtils {
|
|
|
31644
32219
|
frameName: "",
|
|
31645
32220
|
});
|
|
31646
32221
|
};
|
|
32222
|
+
/**
|
|
32223
|
+
* Returns the percentage of the position currently held (not closed).
|
|
32224
|
+
* 100 = nothing has been closed (full position), 0 = fully closed.
|
|
32225
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32226
|
+
*
|
|
32227
|
+
* @param symbol - Trading pair symbol
|
|
32228
|
+
* @param context - Context with strategyName and exchangeName
|
|
32229
|
+
* @returns Promise<number> - held percentage (0–100)
|
|
32230
|
+
*
|
|
32231
|
+
* @example
|
|
32232
|
+
* ```typescript
|
|
32233
|
+
* const heldPct = await Live.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32234
|
+
* console.log(`Holding ${heldPct}% of position`);
|
|
32235
|
+
* ```
|
|
32236
|
+
*/
|
|
32237
|
+
this.getTotalPercentClosed = async (symbol, context) => {
|
|
32238
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
|
|
32239
|
+
symbol,
|
|
32240
|
+
context,
|
|
32241
|
+
});
|
|
32242
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32243
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32244
|
+
{
|
|
32245
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32246
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
|
|
32247
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32248
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
|
|
32249
|
+
}
|
|
32250
|
+
return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
|
|
32251
|
+
strategyName: context.strategyName,
|
|
32252
|
+
exchangeName: context.exchangeName,
|
|
32253
|
+
frameName: "",
|
|
32254
|
+
});
|
|
32255
|
+
};
|
|
32256
|
+
/**
|
|
32257
|
+
* Returns the cost basis in dollars of the position currently held (not closed).
|
|
32258
|
+
* Correctly accounts for DCA entries between partial closes.
|
|
32259
|
+
*
|
|
32260
|
+
* @param symbol - Trading pair symbol
|
|
32261
|
+
* @param context - Context with strategyName and exchangeName
|
|
32262
|
+
* @returns Promise<number> - held cost basis in dollars
|
|
32263
|
+
*
|
|
32264
|
+
* @example
|
|
32265
|
+
* ```typescript
|
|
32266
|
+
* const heldCost = await Live.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName });
|
|
32267
|
+
* console.log(`Holding $${heldCost} of position`);
|
|
32268
|
+
* ```
|
|
32269
|
+
*/
|
|
32270
|
+
this.getTotalCostClosed = async (symbol, context) => {
|
|
32271
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
|
|
32272
|
+
symbol,
|
|
32273
|
+
context,
|
|
32274
|
+
});
|
|
32275
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32276
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32277
|
+
{
|
|
32278
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
32279
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
|
|
32280
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
32281
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
|
|
32282
|
+
}
|
|
32283
|
+
return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
|
|
32284
|
+
strategyName: context.strategyName,
|
|
32285
|
+
exchangeName: context.exchangeName,
|
|
32286
|
+
frameName: "",
|
|
32287
|
+
});
|
|
32288
|
+
};
|
|
31647
32289
|
/**
|
|
31648
32290
|
* Retrieves the currently active scheduled signal for the strategy.
|
|
31649
32291
|
* If no scheduled signal exists, returns null.
|
|
@@ -32561,7 +33203,6 @@ const ADD_ACTION_METHOD_NAME = "add.addActionSchema";
|
|
|
32561
33203
|
* priceTakeProfit: 51000,
|
|
32562
33204
|
* priceStopLoss: 49000,
|
|
32563
33205
|
* minuteEstimatedTime: 60,
|
|
32564
|
-
* timestamp: Date.now(),
|
|
32565
33206
|
* }),
|
|
32566
33207
|
* callbacks: {
|
|
32567
33208
|
* onOpen: (symbol, signal, currentPrice, backtest) => console.log("Signal opened"),
|
|
@@ -33488,8 +34129,8 @@ const WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
|
33488
34129
|
const WRITE_SAFE_SYMBOL = Symbol("write-safe");
|
|
33489
34130
|
/**
|
|
33490
34131
|
* Backtest execution time retrieval function.
|
|
33491
|
-
* Returns the 'when'
|
|
33492
|
-
* This allows log entries to be
|
|
34132
|
+
* Returns the 'when' priority from the execution context if available, otherwise returns the current time.
|
|
34133
|
+
* This allows log entries to be priorityed according to the backtest timeline rather than real-world time, improving log relevance and user experience during backtest analysis.
|
|
33493
34134
|
*/
|
|
33494
34135
|
const GET_DATE_FN = () => {
|
|
33495
34136
|
if (ExecutionContextService.hasContext()) {
|
|
@@ -33511,7 +34152,7 @@ const GET_METHOD_CONTEXT_FN = () => {
|
|
|
33511
34152
|
/**
|
|
33512
34153
|
* Execution context retrieval function.
|
|
33513
34154
|
* Returns the current execution context from ExecutionContextService if available, otherwise returns null.
|
|
33514
|
-
* This allows log entries to include contextual information about the symbol,
|
|
34155
|
+
* This allows log entries to include contextual information about the symbol, priority, and backtest mode associated with the log event, providing additional insights into the execution environment when analyzing logs.
|
|
33515
34156
|
*/
|
|
33516
34157
|
const GET_EXECUTION_CONTEXT_FN = () => {
|
|
33517
34158
|
if (ExecutionContextService.hasContext()) {
|
|
@@ -33541,7 +34182,7 @@ class LogPersistUtils {
|
|
|
33541
34182
|
this.waitForInit = functoolsKit.singleshot(async () => {
|
|
33542
34183
|
bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT);
|
|
33543
34184
|
const list = await PersistLogAdapter.readLogData();
|
|
33544
|
-
list.sort((a, b) => a.
|
|
34185
|
+
list.sort((a, b) => a.priority - b.priority);
|
|
33545
34186
|
this._entries = list.slice(-GLOBAL_CONFIG.CC_MAX_LOG_LINES);
|
|
33546
34187
|
});
|
|
33547
34188
|
/**
|
|
@@ -33557,7 +34198,8 @@ class LogPersistUtils {
|
|
|
33557
34198
|
this._entries.push({
|
|
33558
34199
|
id: functoolsKit.randomString(),
|
|
33559
34200
|
type: "log",
|
|
33560
|
-
|
|
34201
|
+
priority: Date.now(),
|
|
34202
|
+
timestamp: getContextTimestamp(),
|
|
33561
34203
|
createdAt: date.toISOString(),
|
|
33562
34204
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33563
34205
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33580,7 +34222,8 @@ class LogPersistUtils {
|
|
|
33580
34222
|
this._entries.push({
|
|
33581
34223
|
id: functoolsKit.randomString(),
|
|
33582
34224
|
type: "debug",
|
|
33583
|
-
|
|
34225
|
+
priority: Date.now(),
|
|
34226
|
+
timestamp: getContextTimestamp(),
|
|
33584
34227
|
createdAt: date.toISOString(),
|
|
33585
34228
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33586
34229
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33603,7 +34246,8 @@ class LogPersistUtils {
|
|
|
33603
34246
|
this._entries.push({
|
|
33604
34247
|
id: functoolsKit.randomString(),
|
|
33605
34248
|
type: "info",
|
|
33606
|
-
|
|
34249
|
+
priority: Date.now(),
|
|
34250
|
+
timestamp: getContextTimestamp(),
|
|
33607
34251
|
createdAt: date.toISOString(),
|
|
33608
34252
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33609
34253
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33626,7 +34270,8 @@ class LogPersistUtils {
|
|
|
33626
34270
|
this._entries.push({
|
|
33627
34271
|
id: functoolsKit.randomString(),
|
|
33628
34272
|
type: "warn",
|
|
33629
|
-
|
|
34273
|
+
priority: Date.now(),
|
|
34274
|
+
timestamp: getContextTimestamp(),
|
|
33630
34275
|
createdAt: date.toISOString(),
|
|
33631
34276
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33632
34277
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33682,7 +34327,8 @@ class LogMemoryUtils {
|
|
|
33682
34327
|
this._entries.push({
|
|
33683
34328
|
id: functoolsKit.randomString(),
|
|
33684
34329
|
type: "log",
|
|
33685
|
-
|
|
34330
|
+
priority: Date.now(),
|
|
34331
|
+
timestamp: getContextTimestamp(),
|
|
33686
34332
|
createdAt: date.toISOString(),
|
|
33687
34333
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33688
34334
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33703,7 +34349,8 @@ class LogMemoryUtils {
|
|
|
33703
34349
|
this._entries.push({
|
|
33704
34350
|
id: functoolsKit.randomString(),
|
|
33705
34351
|
type: "debug",
|
|
33706
|
-
|
|
34352
|
+
priority: Date.now(),
|
|
34353
|
+
timestamp: getContextTimestamp(),
|
|
33707
34354
|
createdAt: date.toISOString(),
|
|
33708
34355
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33709
34356
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33724,7 +34371,8 @@ class LogMemoryUtils {
|
|
|
33724
34371
|
this._entries.push({
|
|
33725
34372
|
id: functoolsKit.randomString(),
|
|
33726
34373
|
type: "info",
|
|
33727
|
-
|
|
34374
|
+
priority: Date.now(),
|
|
34375
|
+
timestamp: getContextTimestamp(),
|
|
33728
34376
|
createdAt: date.toISOString(),
|
|
33729
34377
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33730
34378
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33745,7 +34393,8 @@ class LogMemoryUtils {
|
|
|
33745
34393
|
this._entries.push({
|
|
33746
34394
|
id: functoolsKit.randomString(),
|
|
33747
34395
|
type: "warn",
|
|
33748
|
-
|
|
34396
|
+
priority: Date.now(),
|
|
34397
|
+
timestamp: getContextTimestamp(),
|
|
33749
34398
|
createdAt: date.toISOString(),
|
|
33750
34399
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33751
34400
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33859,7 +34508,8 @@ class LogJsonlUtils {
|
|
|
33859
34508
|
await this._append({
|
|
33860
34509
|
id: functoolsKit.randomString(),
|
|
33861
34510
|
type: "log",
|
|
33862
|
-
|
|
34511
|
+
priority: Date.now(),
|
|
34512
|
+
timestamp: getContextTimestamp(),
|
|
33863
34513
|
createdAt: date.toISOString(),
|
|
33864
34514
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33865
34515
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33878,7 +34528,8 @@ class LogJsonlUtils {
|
|
|
33878
34528
|
await this._append({
|
|
33879
34529
|
id: functoolsKit.randomString(),
|
|
33880
34530
|
type: "debug",
|
|
33881
|
-
|
|
34531
|
+
priority: Date.now(),
|
|
34532
|
+
timestamp: getContextTimestamp(),
|
|
33882
34533
|
createdAt: date.toISOString(),
|
|
33883
34534
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33884
34535
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33897,7 +34548,8 @@ class LogJsonlUtils {
|
|
|
33897
34548
|
await this._append({
|
|
33898
34549
|
id: functoolsKit.randomString(),
|
|
33899
34550
|
type: "info",
|
|
33900
|
-
|
|
34551
|
+
priority: Date.now(),
|
|
34552
|
+
timestamp: getContextTimestamp(),
|
|
33901
34553
|
createdAt: date.toISOString(),
|
|
33902
34554
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33903
34555
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -33916,7 +34568,8 @@ class LogJsonlUtils {
|
|
|
33916
34568
|
await this._append({
|
|
33917
34569
|
id: functoolsKit.randomString(),
|
|
33918
34570
|
type: "warn",
|
|
33919
|
-
|
|
34571
|
+
priority: Date.now(),
|
|
34572
|
+
timestamp: getContextTimestamp(),
|
|
33920
34573
|
createdAt: date.toISOString(),
|
|
33921
34574
|
methodContext: GET_METHOD_CONTEXT_FN(),
|
|
33922
34575
|
executionContext: GET_EXECUTION_CONTEXT_FN(),
|
|
@@ -38343,6 +38996,8 @@ const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
|
|
|
38343
38996
|
const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
|
|
38344
38997
|
const CACHE_METHOD_NAME_GC = "CacheInstance.gc";
|
|
38345
38998
|
const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
|
|
38999
|
+
const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
|
|
39000
|
+
const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
|
|
38346
39001
|
const MS_PER_MINUTE$1 = 60000;
|
|
38347
39002
|
const INTERVAL_MINUTES$1 = {
|
|
38348
39003
|
"1m": 1,
|
|
@@ -38564,6 +39219,95 @@ class CacheInstance {
|
|
|
38564
39219
|
};
|
|
38565
39220
|
}
|
|
38566
39221
|
}
|
|
39222
|
+
/**
|
|
39223
|
+
* Instance class for caching async function results in persistent file storage.
|
|
39224
|
+
*
|
|
39225
|
+
* Provides automatic cache invalidation based on candle intervals.
|
|
39226
|
+
* Cache key = `${alignedTimestamp}_${symbol}` (bucket) + dynamic key (file within bucket).
|
|
39227
|
+
* On cache hit reads from disk, on miss calls the function and writes the result.
|
|
39228
|
+
*
|
|
39229
|
+
* @template T - Async function type to cache
|
|
39230
|
+
* @template K - Dynamic key type
|
|
39231
|
+
*
|
|
39232
|
+
* @example
|
|
39233
|
+
* ```typescript
|
|
39234
|
+
* const instance = new CacheFileInstance(fetchFromApi, "1h");
|
|
39235
|
+
* const result = await instance.run("BTCUSDT", extraArg);
|
|
39236
|
+
* ```
|
|
39237
|
+
*/
|
|
39238
|
+
class CacheFileInstance {
|
|
39239
|
+
/**
|
|
39240
|
+
* Allocates a new unique index. Called once in the constructor to give each
|
|
39241
|
+
* CacheFileInstance its own namespace in the persistent key space.
|
|
39242
|
+
*/
|
|
39243
|
+
static createIndex() {
|
|
39244
|
+
return CacheFileInstance._indexCounter++;
|
|
39245
|
+
}
|
|
39246
|
+
/**
|
|
39247
|
+
* Creates a new CacheFileInstance.
|
|
39248
|
+
*
|
|
39249
|
+
* @param fn - Async function to cache
|
|
39250
|
+
* @param interval - Candle interval for cache invalidation
|
|
39251
|
+
* @param name - Human-readable bucket name used as the directory key (replaces symbol in bucket path)
|
|
39252
|
+
* @param key - Dynamic key generator; receives all args, must return a string.
|
|
39253
|
+
* Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
|
|
39254
|
+
*/
|
|
39255
|
+
constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
|
|
39256
|
+
this.fn = fn;
|
|
39257
|
+
this.interval = interval;
|
|
39258
|
+
this.name = name;
|
|
39259
|
+
this.key = key;
|
|
39260
|
+
/**
|
|
39261
|
+
* Execute async function with persistent file caching.
|
|
39262
|
+
*
|
|
39263
|
+
* Algorithm:
|
|
39264
|
+
* 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name
|
|
39265
|
+
* 2. Align execution context `when` to interval boundary → `alignedTs`
|
|
39266
|
+
* 3. Build entity key from the key generator (receives `[symbol, alignedTs, ...rest]`)
|
|
39267
|
+
* 4. Try to read from PersistMeasureAdapter using (bucket, entityKey)
|
|
39268
|
+
* 5. On hit — return cached value
|
|
39269
|
+
* 6. On miss — call fn, write result to disk, return result
|
|
39270
|
+
*
|
|
39271
|
+
* Cache invalidation happens through the entity key: the default key embeds `alignedTs`,
|
|
39272
|
+
* so each new candle interval produces a new file name while the bucket directory stays the same.
|
|
39273
|
+
*
|
|
39274
|
+
* Requires active execution context (symbol, when) and method context.
|
|
39275
|
+
*
|
|
39276
|
+
* @param args - Arguments forwarded to the wrapped function
|
|
39277
|
+
* @returns Cached or freshly computed result
|
|
39278
|
+
*/
|
|
39279
|
+
this.run = async (...args) => {
|
|
39280
|
+
bt.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
|
|
39281
|
+
const step = INTERVAL_MINUTES$1[this.interval];
|
|
39282
|
+
{
|
|
39283
|
+
if (!MethodContextService.hasContext()) {
|
|
39284
|
+
throw new Error("CacheFileInstance run requires method context");
|
|
39285
|
+
}
|
|
39286
|
+
if (!ExecutionContextService.hasContext()) {
|
|
39287
|
+
throw new Error("CacheFileInstance run requires execution context");
|
|
39288
|
+
}
|
|
39289
|
+
if (!step) {
|
|
39290
|
+
throw new Error(`CacheFileInstance unknown cache ttl interval=${this.interval}`);
|
|
39291
|
+
}
|
|
39292
|
+
}
|
|
39293
|
+
const [symbol, ...rest] = args;
|
|
39294
|
+
const { when } = bt.executionContextService.context;
|
|
39295
|
+
const alignedTs = align(when.getTime(), this.interval);
|
|
39296
|
+
const bucket = `${this.name}_${this.interval}_${this.index}`;
|
|
39297
|
+
const entityKey = this.key([symbol, alignedTs, ...rest]);
|
|
39298
|
+
const cached = await PersistMeasureAdapter.readMeasureData(bucket, entityKey);
|
|
39299
|
+
if (cached !== null) {
|
|
39300
|
+
return cached;
|
|
39301
|
+
}
|
|
39302
|
+
const result = await this.fn.call(null, ...args);
|
|
39303
|
+
await PersistMeasureAdapter.writeMeasureData(result, bucket, entityKey);
|
|
39304
|
+
return result;
|
|
39305
|
+
};
|
|
39306
|
+
this.index = CacheFileInstance.createIndex();
|
|
39307
|
+
}
|
|
39308
|
+
}
|
|
39309
|
+
/** Global counter — incremented once per CacheFileInstance construction */
|
|
39310
|
+
CacheFileInstance._indexCounter = 0;
|
|
38567
39311
|
/**
|
|
38568
39312
|
* Utility class for function caching with timeframe-based invalidation.
|
|
38569
39313
|
*
|
|
@@ -38585,7 +39329,12 @@ class CacheUtils {
|
|
|
38585
39329
|
* Memoized function to get or create CacheInstance for a function.
|
|
38586
39330
|
* Each function gets its own isolated cache instance.
|
|
38587
39331
|
*/
|
|
38588
|
-
this.
|
|
39332
|
+
this._getFnInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
|
|
39333
|
+
/**
|
|
39334
|
+
* Memoized function to get or create CacheFileInstance for an async function.
|
|
39335
|
+
* Each function gets its own isolated file-cache instance.
|
|
39336
|
+
*/
|
|
39337
|
+
this._getFileInstance = functoolsKit.memoize(([run]) => run, (run, interval, name, key) => new CacheFileInstance(run, interval, name, key));
|
|
38589
39338
|
/**
|
|
38590
39339
|
* Wrap a function with caching based on timeframe intervals.
|
|
38591
39340
|
*
|
|
@@ -38624,11 +39373,57 @@ class CacheUtils {
|
|
|
38624
39373
|
context,
|
|
38625
39374
|
});
|
|
38626
39375
|
const wrappedFn = (...args) => {
|
|
38627
|
-
const instance = this.
|
|
39376
|
+
const instance = this._getFnInstance(run, context.interval, context.key);
|
|
38628
39377
|
return instance.run(...args).value;
|
|
38629
39378
|
};
|
|
38630
39379
|
return wrappedFn;
|
|
38631
39380
|
};
|
|
39381
|
+
/**
|
|
39382
|
+
* Wrap an async function with persistent file-based caching.
|
|
39383
|
+
*
|
|
39384
|
+
* Returns a wrapped version of the function that reads from disk on cache hit
|
|
39385
|
+
* and writes the result to disk on cache miss. Files are stored under
|
|
39386
|
+
* `./dump/data/measure/{name}_{interval}_{index}/`.
|
|
39387
|
+
*
|
|
39388
|
+
* The `run` function reference is used as the memoization key for the underlying
|
|
39389
|
+
* `CacheFileInstance`, so each unique function reference gets its own isolated instance.
|
|
39390
|
+
* Pass the same function reference each time to reuse the same cache.
|
|
39391
|
+
*
|
|
39392
|
+
* @template T - Async function type to cache
|
|
39393
|
+
* @param run - Async function to wrap with file caching
|
|
39394
|
+
* @param context.interval - Candle interval for cache invalidation
|
|
39395
|
+
* @param context.name - Human-readable bucket name; becomes the directory prefix
|
|
39396
|
+
* @param context.key - Optional entity key generator. Receives `[symbol, alignMs, ...rest]`
|
|
39397
|
+
* where `alignMs` is the timestamp aligned to `interval`.
|
|
39398
|
+
* Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
|
|
39399
|
+
* @returns Wrapped async function with automatic persistent caching
|
|
39400
|
+
*
|
|
39401
|
+
* @example
|
|
39402
|
+
* ```typescript
|
|
39403
|
+
* const fetchData = async (symbol: string, period: number) => {
|
|
39404
|
+
* return await externalApi.fetch(symbol, period);
|
|
39405
|
+
* };
|
|
39406
|
+
*
|
|
39407
|
+
* // Default key — one cache file per symbol per aligned candle
|
|
39408
|
+
* const cachedFetch = Cache.file(fetchData, { interval: "1h", name: "fetchData" });
|
|
39409
|
+
*
|
|
39410
|
+
* // Custom key — one cache file per symbol + period combination
|
|
39411
|
+
* const cachedFetch = Cache.file(fetchData, {
|
|
39412
|
+
* interval: "1h",
|
|
39413
|
+
* name: "fetchData",
|
|
39414
|
+
* key: ([symbol, alignMs, period]) => `${symbol}_${alignMs}_${period}`,
|
|
39415
|
+
* });
|
|
39416
|
+
* const result = await cachedFetch("BTCUSDT", 14);
|
|
39417
|
+
* ```
|
|
39418
|
+
*/
|
|
39419
|
+
this.file = (run, context) => {
|
|
39420
|
+
bt.loggerService.info(CACHE_METHOD_NAME_FILE, { context });
|
|
39421
|
+
const wrappedFn = (...args) => {
|
|
39422
|
+
const instance = this._getFileInstance(run, context.interval, context.name, context.key);
|
|
39423
|
+
return instance.run(...args);
|
|
39424
|
+
};
|
|
39425
|
+
return wrappedFn;
|
|
39426
|
+
};
|
|
38632
39427
|
/**
|
|
38633
39428
|
* Flush (remove) cached CacheInstance for a specific function or all functions.
|
|
38634
39429
|
*
|
|
@@ -38662,7 +39457,7 @@ class CacheUtils {
|
|
|
38662
39457
|
bt.loggerService.info(CACHE_METHOD_NAME_FLUSH, {
|
|
38663
39458
|
run,
|
|
38664
39459
|
});
|
|
38665
|
-
this.
|
|
39460
|
+
this._getFnInstance.clear(run);
|
|
38666
39461
|
};
|
|
38667
39462
|
/**
|
|
38668
39463
|
* Clear cached value for current execution context of a specific function.
|
|
@@ -38705,7 +39500,7 @@ class CacheUtils {
|
|
|
38705
39500
|
console.warn(`${CACHE_METHOD_NAME_CLEAR} called without execution context, skipping clear`);
|
|
38706
39501
|
return;
|
|
38707
39502
|
}
|
|
38708
|
-
this.
|
|
39503
|
+
this._getFnInstance.get(run).clear();
|
|
38709
39504
|
};
|
|
38710
39505
|
/**
|
|
38711
39506
|
* Garbage collect expired cache entries for a specific function.
|
|
@@ -38737,7 +39532,7 @@ class CacheUtils {
|
|
|
38737
39532
|
console.warn(`${CACHE_METHOD_NAME_GC} called without execution context, skipping garbage collection`);
|
|
38738
39533
|
return;
|
|
38739
39534
|
}
|
|
38740
|
-
return this.
|
|
39535
|
+
return this._getFnInstance.get(run).gc();
|
|
38741
39536
|
};
|
|
38742
39537
|
}
|
|
38743
39538
|
}
|
|
@@ -39330,6 +40125,7 @@ exports.PersistBase = PersistBase;
|
|
|
39330
40125
|
exports.PersistBreakevenAdapter = PersistBreakevenAdapter;
|
|
39331
40126
|
exports.PersistCandleAdapter = PersistCandleAdapter;
|
|
39332
40127
|
exports.PersistLogAdapter = PersistLogAdapter;
|
|
40128
|
+
exports.PersistMeasureAdapter = PersistMeasureAdapter;
|
|
39333
40129
|
exports.PersistNotificationAdapter = PersistNotificationAdapter;
|
|
39334
40130
|
exports.PersistPartialAdapter = PersistPartialAdapter;
|
|
39335
40131
|
exports.PersistRiskAdapter = PersistRiskAdapter;
|
|
@@ -39373,6 +40169,7 @@ exports.getActionSchema = getActionSchema;
|
|
|
39373
40169
|
exports.getAggregatedTrades = getAggregatedTrades;
|
|
39374
40170
|
exports.getAveragePrice = getAveragePrice;
|
|
39375
40171
|
exports.getBacktestTimeframe = getBacktestTimeframe;
|
|
40172
|
+
exports.getBreakeven = getBreakeven;
|
|
39376
40173
|
exports.getCandles = getCandles;
|
|
39377
40174
|
exports.getColumns = getColumns;
|
|
39378
40175
|
exports.getConfig = getConfig;
|
|
@@ -39380,16 +40177,23 @@ exports.getContext = getContext;
|
|
|
39380
40177
|
exports.getDate = getDate;
|
|
39381
40178
|
exports.getDefaultColumns = getDefaultColumns;
|
|
39382
40179
|
exports.getDefaultConfig = getDefaultConfig;
|
|
40180
|
+
exports.getEffectivePriceOpen = getEffectivePriceOpen;
|
|
39383
40181
|
exports.getExchangeSchema = getExchangeSchema;
|
|
39384
40182
|
exports.getFrameSchema = getFrameSchema;
|
|
39385
40183
|
exports.getMode = getMode;
|
|
39386
40184
|
exports.getNextCandles = getNextCandles;
|
|
39387
40185
|
exports.getOrderBook = getOrderBook;
|
|
40186
|
+
exports.getPendingSignal = getPendingSignal;
|
|
39388
40187
|
exports.getRawCandles = getRawCandles;
|
|
39389
40188
|
exports.getRiskSchema = getRiskSchema;
|
|
40189
|
+
exports.getScheduledSignal = getScheduledSignal;
|
|
39390
40190
|
exports.getSizingSchema = getSizingSchema;
|
|
39391
40191
|
exports.getStrategySchema = getStrategySchema;
|
|
39392
40192
|
exports.getSymbol = getSymbol;
|
|
40193
|
+
exports.getTimestamp = getTimestamp;
|
|
40194
|
+
exports.getTotalClosed = getTotalClosed;
|
|
40195
|
+
exports.getTotalCostClosed = getTotalCostClosed;
|
|
40196
|
+
exports.getTotalPercentClosed = getTotalPercentClosed;
|
|
39393
40197
|
exports.getWalkerSchema = getWalkerSchema;
|
|
39394
40198
|
exports.hasTradeContext = hasTradeContext;
|
|
39395
40199
|
exports.lib = backtest;
|
|
@@ -39449,6 +40253,7 @@ exports.setConfig = setConfig;
|
|
|
39449
40253
|
exports.setLogger = setLogger;
|
|
39450
40254
|
exports.shutdown = shutdown;
|
|
39451
40255
|
exports.stopStrategy = stopStrategy;
|
|
40256
|
+
exports.toProfitLossDto = toProfitLossDto;
|
|
39452
40257
|
exports.validate = validate;
|
|
39453
40258
|
exports.waitForCandle = waitForCandle;
|
|
39454
40259
|
exports.warmCandles = warmCandles;
|