backtest-kit 3.0.10 → 3.0.12
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 +24 -0
- package/build/index.cjs +1465 -1331
- package/build/index.mjs +1465 -1333
- package/package.json +1 -1
- package/types.d.ts +154 -109
package/build/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createActivator } from 'di-kit';
|
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
3
|
import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
|
-
import fs__default from 'fs/promises';
|
|
5
|
+
import fs__default, { stat, opendir, readFile } from 'fs/promises';
|
|
6
6
|
import path, { join, dirname } from 'path';
|
|
7
7
|
import crypto from 'crypto';
|
|
8
8
|
import os from 'os';
|
|
@@ -695,7 +695,7 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
695
695
|
var _a$2;
|
|
696
696
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
697
697
|
// Calculate step in milliseconds for candle close time validation
|
|
698
|
-
const INTERVAL_MINUTES$
|
|
698
|
+
const INTERVAL_MINUTES$6 = {
|
|
699
699
|
"1m": 1,
|
|
700
700
|
"3m": 3,
|
|
701
701
|
"5m": 5,
|
|
@@ -707,7 +707,7 @@ const INTERVAL_MINUTES$5 = {
|
|
|
707
707
|
"6h": 360,
|
|
708
708
|
"8h": 480,
|
|
709
709
|
};
|
|
710
|
-
const MS_PER_MINUTE$
|
|
710
|
+
const MS_PER_MINUTE$4 = 60000;
|
|
711
711
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
712
712
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
713
713
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
@@ -1605,7 +1605,7 @@ class PersistCandleUtils {
|
|
|
1605
1605
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1606
1606
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1607
1607
|
await stateStorage.waitForInit(isInitial);
|
|
1608
|
-
const stepMs = INTERVAL_MINUTES$
|
|
1608
|
+
const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
|
|
1609
1609
|
// Calculate expected timestamps and fetch each candle directly
|
|
1610
1610
|
const cachedCandles = [];
|
|
1611
1611
|
for (let i = 0; i < limit; i++) {
|
|
@@ -1661,7 +1661,7 @@ class PersistCandleUtils {
|
|
|
1661
1661
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1662
1662
|
await stateStorage.waitForInit(isInitial);
|
|
1663
1663
|
// Calculate step in milliseconds to determine candle close time
|
|
1664
|
-
const stepMs = INTERVAL_MINUTES$
|
|
1664
|
+
const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
|
|
1665
1665
|
const now = Date.now();
|
|
1666
1666
|
// Write each candle as a separate file, skipping incomplete candles
|
|
1667
1667
|
for (const candle of candles) {
|
|
@@ -1918,8 +1918,8 @@ class PersistNotificationUtils {
|
|
|
1918
1918
|
*/
|
|
1919
1919
|
const PersistNotificationAdapter = new PersistNotificationUtils();
|
|
1920
1920
|
|
|
1921
|
-
const MS_PER_MINUTE$
|
|
1922
|
-
const INTERVAL_MINUTES$
|
|
1921
|
+
const MS_PER_MINUTE$3 = 60000;
|
|
1922
|
+
const INTERVAL_MINUTES$5 = {
|
|
1923
1923
|
"1m": 1,
|
|
1924
1924
|
"3m": 3,
|
|
1925
1925
|
"5m": 5,
|
|
@@ -1948,8 +1948,8 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1948
1948
|
* @param intervalMinutes - Interval in minutes
|
|
1949
1949
|
* @returns Aligned timestamp rounded down to interval boundary
|
|
1950
1950
|
*/
|
|
1951
|
-
const ALIGN_TO_INTERVAL_FN$
|
|
1952
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
1951
|
+
const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
|
|
1952
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
|
|
1953
1953
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1954
1954
|
};
|
|
1955
1955
|
/**
|
|
@@ -2094,9 +2094,9 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
|
|
|
2094
2094
|
* @returns Promise resolving to array of candle data
|
|
2095
2095
|
*/
|
|
2096
2096
|
const GET_CANDLES_FN = async (dto, since, self) => {
|
|
2097
|
-
const step = INTERVAL_MINUTES$
|
|
2097
|
+
const step = INTERVAL_MINUTES$5[dto.interval];
|
|
2098
2098
|
const sinceTimestamp = since.getTime();
|
|
2099
|
-
const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$
|
|
2099
|
+
const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$3;
|
|
2100
2100
|
// Try to read from cache first
|
|
2101
2101
|
const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
|
|
2102
2102
|
if (cachedCandles !== null) {
|
|
@@ -2204,14 +2204,14 @@ class ClientExchange {
|
|
|
2204
2204
|
interval,
|
|
2205
2205
|
limit,
|
|
2206
2206
|
});
|
|
2207
|
-
const step = INTERVAL_MINUTES$
|
|
2207
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2208
2208
|
if (!step) {
|
|
2209
2209
|
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2210
2210
|
}
|
|
2211
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2211
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2212
2212
|
// Align when down to interval boundary
|
|
2213
2213
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2214
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2214
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2215
2215
|
// Calculate since: go back limit candles from aligned when
|
|
2216
2216
|
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2217
2217
|
const since = new Date(sinceTimestamp);
|
|
@@ -2285,15 +2285,15 @@ class ClientExchange {
|
|
|
2285
2285
|
if (!this.params.execution.context.backtest) {
|
|
2286
2286
|
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2287
2287
|
}
|
|
2288
|
-
const step = INTERVAL_MINUTES$
|
|
2288
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2289
2289
|
if (!step) {
|
|
2290
2290
|
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2291
2291
|
}
|
|
2292
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2292
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2293
2293
|
const now = Date.now();
|
|
2294
2294
|
// Align when down to interval boundary
|
|
2295
2295
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2296
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2296
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2297
2297
|
// since = alignedWhen (start from aligned when, going forward)
|
|
2298
2298
|
const sinceTimestamp = alignedWhen;
|
|
2299
2299
|
const since = new Date(sinceTimestamp);
|
|
@@ -2453,13 +2453,13 @@ class ClientExchange {
|
|
|
2453
2453
|
sDate,
|
|
2454
2454
|
eDate,
|
|
2455
2455
|
});
|
|
2456
|
-
const step = INTERVAL_MINUTES$
|
|
2456
|
+
const step = INTERVAL_MINUTES$5[interval];
|
|
2457
2457
|
if (!step) {
|
|
2458
2458
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2459
2459
|
}
|
|
2460
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
2460
|
+
const stepMs = step * MS_PER_MINUTE$3;
|
|
2461
2461
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2462
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN$
|
|
2462
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
|
|
2463
2463
|
let sinceTimestamp;
|
|
2464
2464
|
let calculatedLimit;
|
|
2465
2465
|
// Case 1: all three parameters provided
|
|
@@ -2471,7 +2471,7 @@ class ClientExchange {
|
|
|
2471
2471
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2472
2472
|
}
|
|
2473
2473
|
// Align sDate down to interval boundary
|
|
2474
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2474
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2475
2475
|
calculatedLimit = limit;
|
|
2476
2476
|
}
|
|
2477
2477
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2483,8 +2483,8 @@ class ClientExchange {
|
|
|
2483
2483
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2484
2484
|
}
|
|
2485
2485
|
// Align sDate down to interval boundary
|
|
2486
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2487
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN$
|
|
2486
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2487
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
|
|
2488
2488
|
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2489
2489
|
if (calculatedLimit <= 0) {
|
|
2490
2490
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
@@ -2496,14 +2496,14 @@ class ClientExchange {
|
|
|
2496
2496
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2497
2497
|
}
|
|
2498
2498
|
// Align eDate down and calculate sinceTimestamp
|
|
2499
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN$
|
|
2499
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
|
|
2500
2500
|
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2501
2501
|
calculatedLimit = limit;
|
|
2502
2502
|
}
|
|
2503
2503
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2504
2504
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2505
2505
|
// Align sDate down to interval boundary
|
|
2506
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN$
|
|
2506
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
|
|
2507
2507
|
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2508
2508
|
if (endTimestamp > whenTimestamp) {
|
|
2509
2509
|
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
@@ -2583,10 +2583,10 @@ class ClientExchange {
|
|
|
2583
2583
|
depth,
|
|
2584
2584
|
});
|
|
2585
2585
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2586
|
-
const alignedTo = ALIGN_TO_INTERVAL_FN$
|
|
2586
|
+
const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
2587
2587
|
const to = new Date(alignedTo);
|
|
2588
2588
|
const from = new Date(alignedTo -
|
|
2589
|
-
GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$
|
|
2589
|
+
GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
|
|
2590
2590
|
return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
|
|
2591
2591
|
}
|
|
2592
2592
|
}
|
|
@@ -3051,7 +3051,7 @@ const beginTime = (run) => (...args) => {
|
|
|
3051
3051
|
return fn();
|
|
3052
3052
|
};
|
|
3053
3053
|
|
|
3054
|
-
const INTERVAL_MINUTES$
|
|
3054
|
+
const INTERVAL_MINUTES$4 = {
|
|
3055
3055
|
"1m": 1,
|
|
3056
3056
|
"3m": 3,
|
|
3057
3057
|
"5m": 5,
|
|
@@ -3564,7 +3564,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
|
|
|
3564
3564
|
}
|
|
3565
3565
|
const currentTime = self.params.execution.context.when.getTime();
|
|
3566
3566
|
{
|
|
3567
|
-
const intervalMinutes = INTERVAL_MINUTES$
|
|
3567
|
+
const intervalMinutes = INTERVAL_MINUTES$4[self.params.interval];
|
|
3568
3568
|
const intervalMs = intervalMinutes * 60 * 1000;
|
|
3569
3569
|
// Проверяем что прошел нужный интервал с последнего getSignal
|
|
3570
3570
|
if (self._lastSignalTimestamp !== null &&
|
|
@@ -8175,7 +8175,7 @@ class StrategyConnectionService {
|
|
|
8175
8175
|
* Maps FrameInterval to minutes for timestamp calculation.
|
|
8176
8176
|
* Used to generate timeframe arrays with proper spacing.
|
|
8177
8177
|
*/
|
|
8178
|
-
const INTERVAL_MINUTES$
|
|
8178
|
+
const INTERVAL_MINUTES$3 = {
|
|
8179
8179
|
"1m": 1,
|
|
8180
8180
|
"3m": 3,
|
|
8181
8181
|
"5m": 5,
|
|
@@ -8230,7 +8230,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
|
8230
8230
|
symbol,
|
|
8231
8231
|
});
|
|
8232
8232
|
const { interval, startDate, endDate } = self.params;
|
|
8233
|
-
const intervalMinutes = INTERVAL_MINUTES$
|
|
8233
|
+
const intervalMinutes = INTERVAL_MINUTES$3[interval];
|
|
8234
8234
|
if (!intervalMinutes) {
|
|
8235
8235
|
throw new Error(`ClientFrame unknown interval: ${interval}`);
|
|
8236
8236
|
}
|
|
@@ -26020,402 +26020,1262 @@ async function getBacktestTimeframe(symbol) {
|
|
|
26020
26020
|
return await bt.frameCoreService.getTimeframe(symbol, bt.methodContextService.context.frameName);
|
|
26021
26021
|
}
|
|
26022
26022
|
|
|
26023
|
-
const
|
|
26023
|
+
const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
|
|
26024
|
+
const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
|
|
26025
|
+
const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
|
|
26026
|
+
const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
26027
|
+
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
26028
|
+
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
26029
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
26024
26030
|
/**
|
|
26025
|
-
*
|
|
26026
|
-
*
|
|
26027
|
-
* @returns Map of exchange names
|
|
26031
|
+
* Gets current timestamp from execution context if available.
|
|
26032
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
26028
26033
|
*/
|
|
26029
|
-
const
|
|
26030
|
-
|
|
26031
|
-
|
|
26032
|
-
Object.assign(exchangeMap, { [exchangeName]: exchangeName });
|
|
26034
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
26035
|
+
if (ExecutionContextService.hasContext()) {
|
|
26036
|
+
return new Date(bt.executionContextService.context.when);
|
|
26033
26037
|
}
|
|
26034
|
-
return
|
|
26038
|
+
return new Date();
|
|
26035
26039
|
};
|
|
26036
26040
|
/**
|
|
26037
|
-
*
|
|
26038
|
-
*
|
|
26039
|
-
* @returns Map of frame names
|
|
26041
|
+
* Gets backtest mode flag from execution context if available.
|
|
26042
|
+
* Returns false if no execution context exists (live mode).
|
|
26040
26043
|
*/
|
|
26041
|
-
const
|
|
26042
|
-
|
|
26043
|
-
|
|
26044
|
-
Object.assign(frameMap, { [frameName]: frameName });
|
|
26044
|
+
const GET_BACKTEST_FN = async () => {
|
|
26045
|
+
if (ExecutionContextService.hasContext()) {
|
|
26046
|
+
return bt.executionContextService.context.backtest;
|
|
26045
26047
|
}
|
|
26046
|
-
return
|
|
26048
|
+
return false;
|
|
26047
26049
|
};
|
|
26048
26050
|
/**
|
|
26049
|
-
*
|
|
26050
|
-
*
|
|
26051
|
-
* @returns Map of strategy names
|
|
26051
|
+
* Default implementation for getCandles.
|
|
26052
|
+
* Throws an error indicating the method is not implemented.
|
|
26052
26053
|
*/
|
|
26053
|
-
const
|
|
26054
|
-
|
|
26055
|
-
for (const { strategyName } of await bt.strategyValidationService.list()) {
|
|
26056
|
-
Object.assign(strategyMap, { [strategyName]: strategyName });
|
|
26057
|
-
}
|
|
26058
|
-
return strategyMap;
|
|
26054
|
+
const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
|
|
26055
|
+
throw new Error(`getCandles is not implemented for this exchange`);
|
|
26059
26056
|
};
|
|
26060
26057
|
/**
|
|
26061
|
-
*
|
|
26062
|
-
*
|
|
26063
|
-
* @returns Map of risk names
|
|
26058
|
+
* Default implementation for formatQuantity.
|
|
26059
|
+
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
26064
26060
|
*/
|
|
26065
|
-
const
|
|
26066
|
-
|
|
26067
|
-
for (const { riskName } of await bt.riskValidationService.list()) {
|
|
26068
|
-
Object.assign(riskMap, { [riskName]: riskName });
|
|
26069
|
-
}
|
|
26070
|
-
return riskMap;
|
|
26061
|
+
const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
|
|
26062
|
+
return quantity.toFixed(8);
|
|
26071
26063
|
};
|
|
26072
26064
|
/**
|
|
26073
|
-
*
|
|
26074
|
-
*
|
|
26075
|
-
* @returns Map of action names
|
|
26065
|
+
* Default implementation for formatPrice.
|
|
26066
|
+
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
26076
26067
|
*/
|
|
26077
|
-
const
|
|
26078
|
-
|
|
26079
|
-
for (const { actionName } of await bt.actionValidationService.list()) {
|
|
26080
|
-
Object.assign(actionMap, { [actionName]: actionName });
|
|
26081
|
-
}
|
|
26082
|
-
return actionMap;
|
|
26068
|
+
const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
|
|
26069
|
+
return price.toFixed(2);
|
|
26083
26070
|
};
|
|
26084
26071
|
/**
|
|
26085
|
-
*
|
|
26086
|
-
*
|
|
26087
|
-
*
|
|
26072
|
+
* Default implementation for getOrderBook.
|
|
26073
|
+
* Throws an error indicating the method is not implemented.
|
|
26074
|
+
*
|
|
26075
|
+
* @param _symbol - Trading pair symbol (unused)
|
|
26076
|
+
* @param _depth - Maximum depth levels (unused)
|
|
26077
|
+
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
26078
|
+
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
26079
|
+
* @param _backtest - Whether running in backtest mode (unused)
|
|
26088
26080
|
*/
|
|
26089
|
-
const
|
|
26090
|
-
|
|
26091
|
-
for (const { sizingName } of await bt.sizingValidationService.list()) {
|
|
26092
|
-
Object.assign(sizingMap, { [sizingName]: sizingName });
|
|
26093
|
-
}
|
|
26094
|
-
return sizingMap;
|
|
26081
|
+
const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
|
|
26082
|
+
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
26095
26083
|
};
|
|
26096
|
-
|
|
26097
|
-
|
|
26098
|
-
|
|
26099
|
-
|
|
26100
|
-
|
|
26101
|
-
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26106
|
-
|
|
26084
|
+
const INTERVAL_MINUTES$2 = {
|
|
26085
|
+
"1m": 1,
|
|
26086
|
+
"3m": 3,
|
|
26087
|
+
"5m": 5,
|
|
26088
|
+
"15m": 15,
|
|
26089
|
+
"30m": 30,
|
|
26090
|
+
"1h": 60,
|
|
26091
|
+
"2h": 120,
|
|
26092
|
+
"4h": 240,
|
|
26093
|
+
"6h": 360,
|
|
26094
|
+
"8h": 480,
|
|
26107
26095
|
};
|
|
26108
26096
|
/**
|
|
26109
|
-
*
|
|
26097
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
26098
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
26110
26099
|
*
|
|
26111
|
-
*
|
|
26112
|
-
*
|
|
26100
|
+
* Candle timestamp convention:
|
|
26101
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
26102
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
26113
26103
|
*
|
|
26114
|
-
*
|
|
26115
|
-
*
|
|
26104
|
+
* Adapter contract:
|
|
26105
|
+
* - Adapter must return candles with timestamp = openTime
|
|
26106
|
+
* - First returned candle.timestamp must equal aligned since
|
|
26107
|
+
* - Adapter must return exactly `limit` candles
|
|
26116
26108
|
*
|
|
26117
|
-
* @
|
|
26118
|
-
* @param
|
|
26119
|
-
* @
|
|
26109
|
+
* @param timestamp - Timestamp in milliseconds
|
|
26110
|
+
* @param intervalMinutes - Interval in minutes
|
|
26111
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
26120
26112
|
*/
|
|
26121
|
-
const
|
|
26122
|
-
const
|
|
26123
|
-
|
|
26124
|
-
bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
|
|
26125
|
-
}
|
|
26126
|
-
for (const frameName of Object.values(FrameName)) {
|
|
26127
|
-
bt.frameValidationService.validate(frameName, METHOD_NAME);
|
|
26128
|
-
}
|
|
26129
|
-
for (const strategyName of Object.values(StrategyName)) {
|
|
26130
|
-
bt.strategyValidationService.validate(strategyName, METHOD_NAME);
|
|
26131
|
-
}
|
|
26132
|
-
for (const riskName of Object.values(RiskName)) {
|
|
26133
|
-
bt.riskValidationService.validate(riskName, METHOD_NAME);
|
|
26134
|
-
}
|
|
26135
|
-
for (const actionName of Object.values(ActionName)) {
|
|
26136
|
-
bt.actionValidationService.validate(actionName, METHOD_NAME);
|
|
26137
|
-
}
|
|
26138
|
-
for (const sizingName of Object.values(SizingName)) {
|
|
26139
|
-
bt.sizingValidationService.validate(sizingName, METHOD_NAME);
|
|
26140
|
-
}
|
|
26141
|
-
for (const walkerName of Object.values(WalkerName)) {
|
|
26142
|
-
bt.walkerValidationService.validate(walkerName, METHOD_NAME);
|
|
26143
|
-
}
|
|
26113
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
26114
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
|
|
26115
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
26144
26116
|
};
|
|
26145
26117
|
/**
|
|
26146
|
-
*
|
|
26147
|
-
*
|
|
26148
|
-
* This function accepts enum objects for various entity types (exchanges, frames,
|
|
26149
|
-
* strategies, risks, sizings, walkers) and validates that each entity
|
|
26150
|
-
* name exists in its respective registry. Validation results are memoized for performance.
|
|
26151
|
-
*
|
|
26152
|
-
* If no arguments are provided (or specific entity types are omitted), the function
|
|
26153
|
-
* automatically fetches and validates ALL registered entities from their respective
|
|
26154
|
-
* validation services. This is useful for comprehensive validation of the entire setup.
|
|
26155
|
-
*
|
|
26156
|
-
* Use this before running backtests or optimizations to ensure all referenced
|
|
26157
|
-
* entities are properly registered and configured.
|
|
26158
|
-
*
|
|
26159
|
-
* @public
|
|
26160
|
-
* @param args - Partial validation arguments containing entity name enums to validate.
|
|
26161
|
-
* If empty or omitted, validates all registered entities.
|
|
26162
|
-
* @throws {Error} If any entity name is not found in its validation service
|
|
26163
|
-
*
|
|
26164
|
-
* @example
|
|
26165
|
-
* ```typescript
|
|
26166
|
-
* // Validate ALL registered entities (exchanges, frames, strategies, etc.)
|
|
26167
|
-
* await validate({});
|
|
26168
|
-
* ```
|
|
26169
|
-
*
|
|
26170
|
-
* @example
|
|
26171
|
-
* ```typescript
|
|
26172
|
-
* // Define your entity name enums
|
|
26173
|
-
* enum ExchangeName {
|
|
26174
|
-
* BINANCE = "binance",
|
|
26175
|
-
* BYBIT = "bybit"
|
|
26176
|
-
* }
|
|
26177
|
-
*
|
|
26178
|
-
* enum StrategyName {
|
|
26179
|
-
* MOMENTUM_BTC = "momentum-btc"
|
|
26180
|
-
* }
|
|
26181
|
-
*
|
|
26182
|
-
* // Validate specific entities before running backtest
|
|
26183
|
-
* await validate({
|
|
26184
|
-
* ExchangeName,
|
|
26185
|
-
* StrategyName,
|
|
26186
|
-
* });
|
|
26187
|
-
* ```
|
|
26188
|
-
*
|
|
26189
|
-
* @example
|
|
26190
|
-
* ```typescript
|
|
26191
|
-
* // Validate specific entity types
|
|
26192
|
-
* await validate({
|
|
26193
|
-
* RiskName: { CONSERVATIVE: "conservative" },
|
|
26194
|
-
* SizingName: { FIXED_1000: "fixed-1000" },
|
|
26195
|
-
* });
|
|
26196
|
-
* ```
|
|
26197
|
-
*/
|
|
26198
|
-
async function validate(args = {}) {
|
|
26199
|
-
bt.loggerService.log(METHOD_NAME);
|
|
26200
|
-
return await validateInternal(args);
|
|
26201
|
-
}
|
|
26202
|
-
|
|
26203
|
-
const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
|
|
26204
|
-
const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
|
|
26205
|
-
const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
|
|
26206
|
-
const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
|
|
26207
|
-
const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
|
|
26208
|
-
const GET_RISK_METHOD_NAME = "get.getRiskSchema";
|
|
26209
|
-
const GET_ACTION_METHOD_NAME = "get.getActionSchema";
|
|
26210
|
-
/**
|
|
26211
|
-
* Retrieves a registered strategy schema by name.
|
|
26212
|
-
*
|
|
26213
|
-
* @param strategyName - Unique strategy identifier
|
|
26214
|
-
* @returns The strategy schema configuration object
|
|
26215
|
-
* @throws Error if strategy is not registered
|
|
26216
|
-
*
|
|
26217
|
-
* @example
|
|
26218
|
-
* ```typescript
|
|
26219
|
-
* const strategy = getStrategy("my-strategy");
|
|
26220
|
-
* console.log(strategy.interval); // "5m"
|
|
26221
|
-
* console.log(strategy.getSignal); // async function
|
|
26222
|
-
* ```
|
|
26223
|
-
*/
|
|
26224
|
-
function getStrategySchema(strategyName) {
|
|
26225
|
-
bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
|
|
26226
|
-
strategyName,
|
|
26227
|
-
});
|
|
26228
|
-
bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
|
|
26229
|
-
return bt.strategySchemaService.get(strategyName);
|
|
26230
|
-
}
|
|
26231
|
-
/**
|
|
26232
|
-
* Retrieves a registered exchange schema by name.
|
|
26233
|
-
*
|
|
26234
|
-
* @param exchangeName - Unique exchange identifier
|
|
26235
|
-
* @returns The exchange schema configuration object
|
|
26236
|
-
* @throws Error if exchange is not registered
|
|
26237
|
-
*
|
|
26238
|
-
* @example
|
|
26239
|
-
* ```typescript
|
|
26240
|
-
* const exchange = getExchange("binance");
|
|
26241
|
-
* console.log(exchange.getCandles); // async function
|
|
26242
|
-
* console.log(exchange.formatPrice); // async function
|
|
26243
|
-
* ```
|
|
26244
|
-
*/
|
|
26245
|
-
function getExchangeSchema(exchangeName) {
|
|
26246
|
-
bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
|
|
26247
|
-
exchangeName,
|
|
26248
|
-
});
|
|
26249
|
-
bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
|
|
26250
|
-
return bt.exchangeSchemaService.get(exchangeName);
|
|
26251
|
-
}
|
|
26252
|
-
/**
|
|
26253
|
-
* Retrieves a registered frame schema by name.
|
|
26254
|
-
*
|
|
26255
|
-
* @param frameName - Unique frame identifier
|
|
26256
|
-
* @returns The frame schema configuration object
|
|
26257
|
-
* @throws Error if frame is not registered
|
|
26258
|
-
*
|
|
26259
|
-
* @example
|
|
26260
|
-
* ```typescript
|
|
26261
|
-
* const frame = getFrame("1d-backtest");
|
|
26262
|
-
* console.log(frame.interval); // "1m"
|
|
26263
|
-
* console.log(frame.startDate); // Date object
|
|
26264
|
-
* console.log(frame.endDate); // Date object
|
|
26265
|
-
* ```
|
|
26266
|
-
*/
|
|
26267
|
-
function getFrameSchema(frameName) {
|
|
26268
|
-
bt.loggerService.log(GET_FRAME_METHOD_NAME, {
|
|
26269
|
-
frameName,
|
|
26270
|
-
});
|
|
26271
|
-
bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
|
|
26272
|
-
return bt.frameSchemaService.get(frameName);
|
|
26273
|
-
}
|
|
26274
|
-
/**
|
|
26275
|
-
* Retrieves a registered walker schema by name.
|
|
26276
|
-
*
|
|
26277
|
-
* @param walkerName - Unique walker identifier
|
|
26278
|
-
* @returns The walker schema configuration object
|
|
26279
|
-
* @throws Error if walker is not registered
|
|
26280
|
-
*
|
|
26281
|
-
* @example
|
|
26282
|
-
* ```typescript
|
|
26283
|
-
* const walker = getWalker("llm-prompt-optimizer");
|
|
26284
|
-
* console.log(walker.exchangeName); // "binance"
|
|
26285
|
-
* console.log(walker.frameName); // "1d-backtest"
|
|
26286
|
-
* console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
|
|
26287
|
-
* console.log(walker.metric); // "sharpeRatio"
|
|
26288
|
-
* ```
|
|
26289
|
-
*/
|
|
26290
|
-
function getWalkerSchema(walkerName) {
|
|
26291
|
-
bt.loggerService.log(GET_WALKER_METHOD_NAME, {
|
|
26292
|
-
walkerName,
|
|
26293
|
-
});
|
|
26294
|
-
bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
|
|
26295
|
-
return bt.walkerSchemaService.get(walkerName);
|
|
26296
|
-
}
|
|
26297
|
-
/**
|
|
26298
|
-
* Retrieves a registered sizing schema by name.
|
|
26299
|
-
*
|
|
26300
|
-
* @param sizingName - Unique sizing identifier
|
|
26301
|
-
* @returns The sizing schema configuration object
|
|
26302
|
-
* @throws Error if sizing is not registered
|
|
26118
|
+
* Creates exchange instance with methods resolved once during construction.
|
|
26119
|
+
* Applies default implementations where schema methods are not provided.
|
|
26303
26120
|
*
|
|
26304
|
-
* @
|
|
26305
|
-
*
|
|
26306
|
-
* const sizing = getSizing("conservative");
|
|
26307
|
-
* console.log(sizing.method); // "fixed-percentage"
|
|
26308
|
-
* console.log(sizing.riskPercentage); // 1
|
|
26309
|
-
* console.log(sizing.maxPositionPercentage); // 10
|
|
26310
|
-
* ```
|
|
26121
|
+
* @param schema - Exchange schema from registry
|
|
26122
|
+
* @returns Object with resolved exchange methods
|
|
26311
26123
|
*/
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
return
|
|
26318
|
-
|
|
26124
|
+
const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
26125
|
+
const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
|
|
26126
|
+
const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
|
|
26127
|
+
const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
|
|
26128
|
+
const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
|
|
26129
|
+
return {
|
|
26130
|
+
getCandles,
|
|
26131
|
+
formatQuantity,
|
|
26132
|
+
formatPrice,
|
|
26133
|
+
getOrderBook,
|
|
26134
|
+
};
|
|
26135
|
+
};
|
|
26319
26136
|
/**
|
|
26320
|
-
*
|
|
26137
|
+
* Attempts to read candles from cache.
|
|
26321
26138
|
*
|
|
26322
|
-
*
|
|
26323
|
-
*
|
|
26324
|
-
*
|
|
26139
|
+
* Cache lookup calculates expected timestamps:
|
|
26140
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
26141
|
+
* Returns all candles if found, null if any missing.
|
|
26325
26142
|
*
|
|
26326
|
-
* @
|
|
26327
|
-
*
|
|
26328
|
-
*
|
|
26329
|
-
*
|
|
26330
|
-
*
|
|
26331
|
-
* ```
|
|
26143
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
26144
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
26145
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
26146
|
+
* @param exchangeName - Exchange name
|
|
26147
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
26332
26148
|
*/
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26149
|
+
const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
26150
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
26151
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
26152
|
+
// Returns all candles if found, null if any missing
|
|
26153
|
+
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
26154
|
+
// Return cached data only if we have exactly the requested limit
|
|
26155
|
+
if (cachedCandles?.length === dto.limit) {
|
|
26156
|
+
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
26157
|
+
return cachedCandles;
|
|
26158
|
+
}
|
|
26159
|
+
bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
26160
|
+
return null;
|
|
26161
|
+
}, {
|
|
26162
|
+
fallback: async (error) => {
|
|
26163
|
+
const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
|
|
26164
|
+
const payload = {
|
|
26165
|
+
error: errorData(error),
|
|
26166
|
+
message: getErrorMessage(error),
|
|
26167
|
+
};
|
|
26168
|
+
bt.loggerService.warn(message, payload);
|
|
26169
|
+
console.warn(message, payload);
|
|
26170
|
+
errorEmitter.next(error);
|
|
26171
|
+
},
|
|
26172
|
+
defaultValue: null,
|
|
26173
|
+
});
|
|
26340
26174
|
/**
|
|
26341
|
-
*
|
|
26175
|
+
* Writes candles to cache with error handling.
|
|
26342
26176
|
*
|
|
26343
|
-
*
|
|
26344
|
-
*
|
|
26345
|
-
*
|
|
26177
|
+
* The candles passed to this function should be validated:
|
|
26178
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
26179
|
+
* - Exact number of candles as requested (limit)
|
|
26180
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
26346
26181
|
*
|
|
26347
|
-
* @
|
|
26348
|
-
*
|
|
26349
|
-
*
|
|
26350
|
-
* console.log(action.handler); // Class constructor or object
|
|
26351
|
-
* console.log(action.callbacks); // Optional lifecycle callbacks
|
|
26352
|
-
* ```
|
|
26182
|
+
* @param candles - Array of validated candle data to cache
|
|
26183
|
+
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
26184
|
+
* @param exchangeName - Exchange name
|
|
26353
26185
|
*/
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
26361
|
-
|
|
26362
|
-
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
|
|
26366
|
-
|
|
26367
|
-
|
|
26368
|
-
|
|
26369
|
-
|
|
26370
|
-
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
26371
|
-
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
26372
|
-
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
26373
|
-
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
26186
|
+
const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
|
|
26187
|
+
await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
|
|
26188
|
+
bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
|
|
26189
|
+
}), {
|
|
26190
|
+
fallback: async (error) => {
|
|
26191
|
+
const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
|
|
26192
|
+
const payload = {
|
|
26193
|
+
error: errorData(error),
|
|
26194
|
+
message: getErrorMessage(error),
|
|
26195
|
+
};
|
|
26196
|
+
bt.loggerService.warn(message, payload);
|
|
26197
|
+
console.warn(message, payload);
|
|
26198
|
+
errorEmitter.next(error);
|
|
26199
|
+
},
|
|
26200
|
+
defaultValue: null,
|
|
26201
|
+
});
|
|
26374
26202
|
/**
|
|
26375
|
-
*
|
|
26376
|
-
*
|
|
26377
|
-
* Returns true when both contexts are active, which is required for calling
|
|
26378
|
-
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
26379
|
-
* getDate, and getMode.
|
|
26203
|
+
* Instance class for exchange operations on a specific exchange.
|
|
26380
26204
|
*
|
|
26381
|
-
*
|
|
26205
|
+
* Provides isolated exchange operations for a single exchange.
|
|
26206
|
+
* Each instance maintains its own context and exposes IExchangeSchema methods.
|
|
26207
|
+
* The schema is retrieved once during construction for better performance.
|
|
26382
26208
|
*
|
|
26383
26209
|
* @example
|
|
26384
26210
|
* ```typescript
|
|
26385
|
-
*
|
|
26386
|
-
*
|
|
26387
|
-
* if (hasTradeContext()) {
|
|
26388
|
-
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
26389
|
-
* } else {
|
|
26390
|
-
* console.log("Trade context not active");
|
|
26391
|
-
* }
|
|
26392
|
-
* ```
|
|
26393
|
-
*/
|
|
26394
|
-
function hasTradeContext() {
|
|
26395
|
-
bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
26396
|
-
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
26397
|
-
}
|
|
26398
|
-
/**
|
|
26399
|
-
* Fetches historical candle data from the registered exchange.
|
|
26400
|
-
*
|
|
26401
|
-
* Candles are fetched backwards from the current execution context time.
|
|
26402
|
-
* Uses the exchange's getCandles implementation.
|
|
26403
|
-
*
|
|
26404
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26405
|
-
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26406
|
-
* @param limit - Number of candles to fetch
|
|
26407
|
-
* @returns Promise resolving to array of candle data
|
|
26211
|
+
* const instance = new ExchangeInstance("binance");
|
|
26408
26212
|
*
|
|
26409
|
-
*
|
|
26410
|
-
*
|
|
26411
|
-
* const
|
|
26412
|
-
*
|
|
26213
|
+
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
26214
|
+
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
26215
|
+
* const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
26216
|
+
* const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
26413
26217
|
* ```
|
|
26414
26218
|
*/
|
|
26415
|
-
|
|
26416
|
-
|
|
26417
|
-
|
|
26418
|
-
|
|
26219
|
+
class ExchangeInstance {
|
|
26220
|
+
/**
|
|
26221
|
+
* Creates a new ExchangeInstance for a specific exchange.
|
|
26222
|
+
*
|
|
26223
|
+
* @param exchangeName - Exchange name (e.g., "binance")
|
|
26224
|
+
*/
|
|
26225
|
+
constructor(exchangeName) {
|
|
26226
|
+
this.exchangeName = exchangeName;
|
|
26227
|
+
/**
|
|
26228
|
+
* Fetch candles from data source (API or database).
|
|
26229
|
+
*
|
|
26230
|
+
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
26231
|
+
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
26232
|
+
*
|
|
26233
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26234
|
+
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
26235
|
+
* @param limit - Maximum number of candles to fetch
|
|
26236
|
+
* @returns Promise resolving to array of OHLCV candle data
|
|
26237
|
+
*
|
|
26238
|
+
* @example
|
|
26239
|
+
* ```typescript
|
|
26240
|
+
* const instance = new ExchangeInstance("binance");
|
|
26241
|
+
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
26242
|
+
* ```
|
|
26243
|
+
*/
|
|
26244
|
+
this.getCandles = async (symbol, interval, limit) => {
|
|
26245
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
|
|
26246
|
+
exchangeName: this.exchangeName,
|
|
26247
|
+
symbol,
|
|
26248
|
+
interval,
|
|
26249
|
+
limit,
|
|
26250
|
+
});
|
|
26251
|
+
const getCandles = this._methods.getCandles;
|
|
26252
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
26253
|
+
if (!step) {
|
|
26254
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
26255
|
+
}
|
|
26256
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
26257
|
+
// Align when down to interval boundary
|
|
26258
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26259
|
+
const whenTimestamp = when.getTime();
|
|
26260
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
26261
|
+
// Calculate since: go back limit candles from aligned when
|
|
26262
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
26263
|
+
const since = new Date(sinceTimestamp);
|
|
26264
|
+
const untilTimestamp = alignedWhen;
|
|
26265
|
+
// Try to read from cache first
|
|
26266
|
+
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
26267
|
+
if (cachedCandles !== null) {
|
|
26268
|
+
return cachedCandles;
|
|
26269
|
+
}
|
|
26270
|
+
let allData = [];
|
|
26271
|
+
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
26272
|
+
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
26273
|
+
let remaining = limit;
|
|
26274
|
+
let currentSince = new Date(since.getTime());
|
|
26275
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26276
|
+
while (remaining > 0) {
|
|
26277
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26278
|
+
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
26279
|
+
allData.push(...chunkData);
|
|
26280
|
+
remaining -= chunkLimit;
|
|
26281
|
+
if (remaining > 0) {
|
|
26282
|
+
// Move currentSince forward by the number of candles fetched
|
|
26283
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
26284
|
+
}
|
|
26285
|
+
}
|
|
26286
|
+
}
|
|
26287
|
+
else {
|
|
26288
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26289
|
+
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
26290
|
+
}
|
|
26291
|
+
// Apply distinct by timestamp to remove duplicates
|
|
26292
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
26293
|
+
if (allData.length !== uniqueData.length) {
|
|
26294
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
26295
|
+
}
|
|
26296
|
+
// Validate adapter returned data
|
|
26297
|
+
if (uniqueData.length === 0) {
|
|
26298
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
26299
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
26300
|
+
}
|
|
26301
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
26302
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
26303
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
26304
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
26305
|
+
}
|
|
26306
|
+
if (uniqueData.length !== limit) {
|
|
26307
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
26308
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
26309
|
+
`Adapter must return exact number of candles requested.`);
|
|
26310
|
+
}
|
|
26311
|
+
// Write to cache after successful fetch
|
|
26312
|
+
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
26313
|
+
return uniqueData;
|
|
26314
|
+
};
|
|
26315
|
+
/**
|
|
26316
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
26317
|
+
* The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
|
|
26318
|
+
*
|
|
26319
|
+
* Formula:
|
|
26320
|
+
* - Typical Price = (high + low + close) / 3
|
|
26321
|
+
* - VWAP = sum(typical_price * volume) / sum(volume)
|
|
26322
|
+
*
|
|
26323
|
+
* If volume is zero, returns simple average of close prices.
|
|
26324
|
+
*
|
|
26325
|
+
* @param symbol - Trading pair symbol
|
|
26326
|
+
* @returns Promise resolving to VWAP price
|
|
26327
|
+
* @throws Error if no candles available
|
|
26328
|
+
*
|
|
26329
|
+
* @example
|
|
26330
|
+
* ```typescript
|
|
26331
|
+
* const instance = new ExchangeInstance("binance");
|
|
26332
|
+
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
26333
|
+
* console.log(vwap); // 50125.43
|
|
26334
|
+
* ```
|
|
26335
|
+
*/
|
|
26336
|
+
this.getAveragePrice = async (symbol) => {
|
|
26337
|
+
bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
|
|
26338
|
+
exchangeName: this.exchangeName,
|
|
26339
|
+
symbol,
|
|
26340
|
+
});
|
|
26341
|
+
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
26342
|
+
if (candles.length === 0) {
|
|
26343
|
+
throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
|
|
26344
|
+
}
|
|
26345
|
+
// VWAP (Volume Weighted Average Price)
|
|
26346
|
+
// Используем типичную цену (typical price) = (high + low + close) / 3
|
|
26347
|
+
const sumPriceVolume = candles.reduce((acc, candle) => {
|
|
26348
|
+
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
|
|
26349
|
+
return acc + typicalPrice * candle.volume;
|
|
26350
|
+
}, 0);
|
|
26351
|
+
const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
|
|
26352
|
+
if (totalVolume === 0) {
|
|
26353
|
+
// Если объем нулевой, возвращаем простое среднее close цен
|
|
26354
|
+
const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
|
|
26355
|
+
return sum / candles.length;
|
|
26356
|
+
}
|
|
26357
|
+
const vwap = sumPriceVolume / totalVolume;
|
|
26358
|
+
return vwap;
|
|
26359
|
+
};
|
|
26360
|
+
/**
|
|
26361
|
+
* Format quantity according to exchange precision rules.
|
|
26362
|
+
*
|
|
26363
|
+
* @param symbol - Trading pair symbol
|
|
26364
|
+
* @param quantity - Raw quantity value
|
|
26365
|
+
* @returns Promise resolving to formatted quantity string
|
|
26366
|
+
*
|
|
26367
|
+
* @example
|
|
26368
|
+
* ```typescript
|
|
26369
|
+
* const instance = new ExchangeInstance("binance");
|
|
26370
|
+
* const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
26371
|
+
* console.log(formatted); // "0.00100000"
|
|
26372
|
+
* ```
|
|
26373
|
+
*/
|
|
26374
|
+
this.formatQuantity = async (symbol, quantity) => {
|
|
26375
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_QUANTITY, {
|
|
26376
|
+
exchangeName: this.exchangeName,
|
|
26377
|
+
symbol,
|
|
26378
|
+
quantity,
|
|
26379
|
+
});
|
|
26380
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26381
|
+
return await this._methods.formatQuantity(symbol, quantity, isBacktest);
|
|
26382
|
+
};
|
|
26383
|
+
/**
|
|
26384
|
+
* Format price according to exchange precision rules.
|
|
26385
|
+
*
|
|
26386
|
+
* @param symbol - Trading pair symbol
|
|
26387
|
+
* @param price - Raw price value
|
|
26388
|
+
* @returns Promise resolving to formatted price string
|
|
26389
|
+
*
|
|
26390
|
+
* @example
|
|
26391
|
+
* ```typescript
|
|
26392
|
+
* const instance = new ExchangeInstance("binance");
|
|
26393
|
+
* const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
26394
|
+
* console.log(formatted); // "50000.12"
|
|
26395
|
+
* ```
|
|
26396
|
+
*/
|
|
26397
|
+
this.formatPrice = async (symbol, price) => {
|
|
26398
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_PRICE, {
|
|
26399
|
+
exchangeName: this.exchangeName,
|
|
26400
|
+
symbol,
|
|
26401
|
+
price,
|
|
26402
|
+
});
|
|
26403
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26404
|
+
return await this._methods.formatPrice(symbol, price, isBacktest);
|
|
26405
|
+
};
|
|
26406
|
+
/**
|
|
26407
|
+
* Fetch order book for a trading pair.
|
|
26408
|
+
*
|
|
26409
|
+
* Calculates time range using CC_ORDER_BOOK_TIME_OFFSET_MINUTES (default 10 minutes)
|
|
26410
|
+
* and passes it to the exchange schema implementation. The implementation may use
|
|
26411
|
+
* the time range (backtest) or ignore it (live trading).
|
|
26412
|
+
*
|
|
26413
|
+
* @param symbol - Trading pair symbol
|
|
26414
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
26415
|
+
* @returns Promise resolving to order book data
|
|
26416
|
+
* @throws Error if getOrderBook is not implemented
|
|
26417
|
+
*
|
|
26418
|
+
* @example
|
|
26419
|
+
* ```typescript
|
|
26420
|
+
* const instance = new ExchangeInstance("binance");
|
|
26421
|
+
* const orderBook = await instance.getOrderBook("BTCUSDT");
|
|
26422
|
+
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
26423
|
+
* const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
|
|
26424
|
+
* ```
|
|
26425
|
+
*/
|
|
26426
|
+
this.getOrderBook = async (symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
26427
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
|
|
26428
|
+
exchangeName: this.exchangeName,
|
|
26429
|
+
symbol,
|
|
26430
|
+
depth,
|
|
26431
|
+
});
|
|
26432
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26433
|
+
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
26434
|
+
const to = new Date(alignedTo);
|
|
26435
|
+
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
|
|
26436
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26437
|
+
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
26438
|
+
};
|
|
26439
|
+
/**
|
|
26440
|
+
* Fetches raw candles with flexible date/limit parameters.
|
|
26441
|
+
*
|
|
26442
|
+
* Uses Date.now() instead of execution context when for look-ahead bias protection.
|
|
26443
|
+
*
|
|
26444
|
+
* Parameter combinations:
|
|
26445
|
+
* 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
|
|
26446
|
+
* 2. sDate + eDate: calculates limit from date range, validates eDate <= now
|
|
26447
|
+
* 3. eDate + limit: calculates sDate backward, validates eDate <= now
|
|
26448
|
+
* 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
|
|
26449
|
+
* 5. Only limit: uses Date.now() as reference (backward)
|
|
26450
|
+
*
|
|
26451
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26452
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
26453
|
+
* @param limit - Optional number of candles to fetch
|
|
26454
|
+
* @param sDate - Optional start date in milliseconds
|
|
26455
|
+
* @param eDate - Optional end date in milliseconds
|
|
26456
|
+
* @returns Promise resolving to array of candle data
|
|
26457
|
+
*
|
|
26458
|
+
* @example
|
|
26459
|
+
* ```typescript
|
|
26460
|
+
* const instance = new ExchangeInstance("binance");
|
|
26461
|
+
*
|
|
26462
|
+
* // Fetch 100 candles backward from now
|
|
26463
|
+
* const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
|
|
26464
|
+
*
|
|
26465
|
+
* // Fetch candles for specific date range
|
|
26466
|
+
* const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
|
|
26467
|
+
* ```
|
|
26468
|
+
*/
|
|
26469
|
+
this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
|
|
26470
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
|
|
26471
|
+
exchangeName: this.exchangeName,
|
|
26472
|
+
symbol,
|
|
26473
|
+
interval,
|
|
26474
|
+
limit,
|
|
26475
|
+
sDate,
|
|
26476
|
+
eDate,
|
|
26477
|
+
});
|
|
26478
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
26479
|
+
if (!step) {
|
|
26480
|
+
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
26481
|
+
}
|
|
26482
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
26483
|
+
const when = await GET_TIMESTAMP_FN();
|
|
26484
|
+
const nowTimestamp = when.getTime();
|
|
26485
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
|
|
26486
|
+
let sinceTimestamp;
|
|
26487
|
+
let calculatedLimit;
|
|
26488
|
+
// Case 1: all three parameters provided
|
|
26489
|
+
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
26490
|
+
if (sDate >= eDate) {
|
|
26491
|
+
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
26492
|
+
}
|
|
26493
|
+
if (eDate > nowTimestamp) {
|
|
26494
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26495
|
+
}
|
|
26496
|
+
// Align sDate down to interval boundary
|
|
26497
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26498
|
+
calculatedLimit = limit;
|
|
26499
|
+
}
|
|
26500
|
+
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
26501
|
+
else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
|
|
26502
|
+
if (sDate >= eDate) {
|
|
26503
|
+
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
26504
|
+
}
|
|
26505
|
+
if (eDate > nowTimestamp) {
|
|
26506
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26507
|
+
}
|
|
26508
|
+
// Align sDate down to interval boundary
|
|
26509
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26510
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
26511
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
26512
|
+
if (calculatedLimit <= 0) {
|
|
26513
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
26514
|
+
}
|
|
26515
|
+
}
|
|
26516
|
+
// Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
|
|
26517
|
+
else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
|
|
26518
|
+
if (eDate > nowTimestamp) {
|
|
26519
|
+
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26520
|
+
}
|
|
26521
|
+
// Align eDate down and calculate sinceTimestamp
|
|
26522
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
26523
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
26524
|
+
calculatedLimit = limit;
|
|
26525
|
+
}
|
|
26526
|
+
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
26527
|
+
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
26528
|
+
// Align sDate down to interval boundary
|
|
26529
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
26530
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
26531
|
+
if (endTimestamp > nowTimestamp) {
|
|
26532
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
26533
|
+
}
|
|
26534
|
+
calculatedLimit = limit;
|
|
26535
|
+
}
|
|
26536
|
+
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
26537
|
+
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
26538
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
26539
|
+
calculatedLimit = limit;
|
|
26540
|
+
}
|
|
26541
|
+
// Invalid: no parameters or only sDate or only eDate
|
|
26542
|
+
else {
|
|
26543
|
+
throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
|
|
26544
|
+
`Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
|
|
26545
|
+
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
26546
|
+
}
|
|
26547
|
+
// Try to read from cache first
|
|
26548
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
26549
|
+
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
26550
|
+
if (cachedCandles !== null) {
|
|
26551
|
+
return cachedCandles;
|
|
26552
|
+
}
|
|
26553
|
+
// Fetch candles
|
|
26554
|
+
const since = new Date(sinceTimestamp);
|
|
26555
|
+
let allData = [];
|
|
26556
|
+
const isBacktest = await GET_BACKTEST_FN();
|
|
26557
|
+
const getCandles = this._methods.getCandles;
|
|
26558
|
+
if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
26559
|
+
let remaining = calculatedLimit;
|
|
26560
|
+
let currentSince = new Date(since.getTime());
|
|
26561
|
+
while (remaining > 0) {
|
|
26562
|
+
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26563
|
+
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
26564
|
+
allData.push(...chunkData);
|
|
26565
|
+
remaining -= chunkLimit;
|
|
26566
|
+
if (remaining > 0) {
|
|
26567
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
26568
|
+
}
|
|
26569
|
+
}
|
|
26570
|
+
}
|
|
26571
|
+
else {
|
|
26572
|
+
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
26573
|
+
}
|
|
26574
|
+
// Apply distinct by timestamp to remove duplicates
|
|
26575
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
26576
|
+
if (allData.length !== uniqueData.length) {
|
|
26577
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
26578
|
+
}
|
|
26579
|
+
// Validate adapter returned data
|
|
26580
|
+
if (uniqueData.length === 0) {
|
|
26581
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
26582
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
26583
|
+
}
|
|
26584
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
26585
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
26586
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
26587
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
26588
|
+
}
|
|
26589
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
26590
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
26591
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
26592
|
+
`Adapter must return exact number of candles requested.`);
|
|
26593
|
+
}
|
|
26594
|
+
// Write to cache after successful fetch
|
|
26595
|
+
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
26596
|
+
return uniqueData;
|
|
26597
|
+
};
|
|
26598
|
+
const schema = bt.exchangeSchemaService.get(this.exchangeName);
|
|
26599
|
+
this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
|
|
26600
|
+
}
|
|
26601
|
+
}
|
|
26602
|
+
/**
|
|
26603
|
+
* Utility class for exchange operations.
|
|
26604
|
+
*
|
|
26605
|
+
* Provides simplified access to exchange schema methods with validation.
|
|
26606
|
+
* Exported as singleton instance for convenient usage.
|
|
26607
|
+
*
|
|
26608
|
+
* @example
|
|
26609
|
+
* ```typescript
|
|
26610
|
+
* import { Exchange } from "./classes/Exchange";
|
|
26611
|
+
*
|
|
26612
|
+
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
26613
|
+
* exchangeName: "binance"
|
|
26614
|
+
* });
|
|
26615
|
+
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
26616
|
+
* exchangeName: "binance"
|
|
26617
|
+
* });
|
|
26618
|
+
* const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
26619
|
+
* exchangeName: "binance"
|
|
26620
|
+
* });
|
|
26621
|
+
* ```
|
|
26622
|
+
*/
|
|
26623
|
+
class ExchangeUtils {
|
|
26624
|
+
constructor() {
|
|
26625
|
+
/**
|
|
26626
|
+
* Memoized function to get or create ExchangeInstance for an exchange.
|
|
26627
|
+
* Each exchange gets its own isolated instance.
|
|
26628
|
+
*/
|
|
26629
|
+
this._getInstance = memoize(([exchangeName]) => exchangeName, (exchangeName) => new ExchangeInstance(exchangeName));
|
|
26630
|
+
/**
|
|
26631
|
+
* Fetch candles from data source (API or database).
|
|
26632
|
+
*
|
|
26633
|
+
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
26634
|
+
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
26635
|
+
*
|
|
26636
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26637
|
+
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
26638
|
+
* @param limit - Maximum number of candles to fetch
|
|
26639
|
+
* @param context - Execution context with exchange name
|
|
26640
|
+
* @returns Promise resolving to array of OHLCV candle data
|
|
26641
|
+
*/
|
|
26642
|
+
this.getCandles = async (symbol, interval, limit, context) => {
|
|
26643
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_CANDLES);
|
|
26644
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26645
|
+
return await instance.getCandles(symbol, interval, limit);
|
|
26646
|
+
};
|
|
26647
|
+
/**
|
|
26648
|
+
* Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
|
|
26649
|
+
*
|
|
26650
|
+
* @param symbol - Trading pair symbol
|
|
26651
|
+
* @param context - Execution context with exchange name
|
|
26652
|
+
* @returns Promise resolving to VWAP price
|
|
26653
|
+
*/
|
|
26654
|
+
this.getAveragePrice = async (symbol, context) => {
|
|
26655
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE);
|
|
26656
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26657
|
+
return await instance.getAveragePrice(symbol);
|
|
26658
|
+
};
|
|
26659
|
+
/**
|
|
26660
|
+
* Format quantity according to exchange precision rules.
|
|
26661
|
+
*
|
|
26662
|
+
* @param symbol - Trading pair symbol
|
|
26663
|
+
* @param quantity - Raw quantity value
|
|
26664
|
+
* @param context - Execution context with exchange name
|
|
26665
|
+
* @returns Promise resolving to formatted quantity string
|
|
26666
|
+
*/
|
|
26667
|
+
this.formatQuantity = async (symbol, quantity, context) => {
|
|
26668
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_QUANTITY);
|
|
26669
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26670
|
+
return await instance.formatQuantity(symbol, quantity);
|
|
26671
|
+
};
|
|
26672
|
+
/**
|
|
26673
|
+
* Format price according to exchange precision rules.
|
|
26674
|
+
*
|
|
26675
|
+
* @param symbol - Trading pair symbol
|
|
26676
|
+
* @param price - Raw price value
|
|
26677
|
+
* @param context - Execution context with exchange name
|
|
26678
|
+
* @returns Promise resolving to formatted price string
|
|
26679
|
+
*/
|
|
26680
|
+
this.formatPrice = async (symbol, price, context) => {
|
|
26681
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_PRICE);
|
|
26682
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26683
|
+
return await instance.formatPrice(symbol, price);
|
|
26684
|
+
};
|
|
26685
|
+
/**
|
|
26686
|
+
* Fetch order book for a trading pair.
|
|
26687
|
+
*
|
|
26688
|
+
* Delegates to ExchangeInstance which calculates time range and passes it
|
|
26689
|
+
* to the exchange schema implementation. The from/to parameters may be used
|
|
26690
|
+
* (backtest) or ignored (live) depending on the implementation.
|
|
26691
|
+
*
|
|
26692
|
+
* @param symbol - Trading pair symbol
|
|
26693
|
+
* @param context - Execution context with exchange name
|
|
26694
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
26695
|
+
* @returns Promise resolving to order book data
|
|
26696
|
+
*/
|
|
26697
|
+
this.getOrderBook = async (symbol, context, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
26698
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
|
|
26699
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26700
|
+
return await instance.getOrderBook(symbol, depth);
|
|
26701
|
+
};
|
|
26702
|
+
/**
|
|
26703
|
+
* Fetches raw candles with flexible date/limit parameters.
|
|
26704
|
+
*
|
|
26705
|
+
* Uses Date.now() instead of execution context when for look-ahead bias protection.
|
|
26706
|
+
*
|
|
26707
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26708
|
+
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
26709
|
+
* @param context - Execution context with exchange name
|
|
26710
|
+
* @param limit - Optional number of candles to fetch
|
|
26711
|
+
* @param sDate - Optional start date in milliseconds
|
|
26712
|
+
* @param eDate - Optional end date in milliseconds
|
|
26713
|
+
* @returns Promise resolving to array of candle data
|
|
26714
|
+
*/
|
|
26715
|
+
this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
|
|
26716
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
|
|
26717
|
+
const instance = this._getInstance(context.exchangeName);
|
|
26718
|
+
return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
26719
|
+
};
|
|
26720
|
+
}
|
|
26721
|
+
}
|
|
26722
|
+
/**
|
|
26723
|
+
* Singleton instance of ExchangeUtils for convenient exchange operations.
|
|
26724
|
+
*
|
|
26725
|
+
* @example
|
|
26726
|
+
* ```typescript
|
|
26727
|
+
* import { Exchange } from "./classes/Exchange";
|
|
26728
|
+
*
|
|
26729
|
+
* // Using static-like API with context
|
|
26730
|
+
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
26731
|
+
* exchangeName: "binance"
|
|
26732
|
+
* });
|
|
26733
|
+
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
26734
|
+
* exchangeName: "binance"
|
|
26735
|
+
* });
|
|
26736
|
+
* const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
26737
|
+
* exchangeName: "binance"
|
|
26738
|
+
* });
|
|
26739
|
+
* const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
|
|
26740
|
+
* exchangeName: "binance"
|
|
26741
|
+
* });
|
|
26742
|
+
*
|
|
26743
|
+
* // Using instance API (no context needed, exchange set in constructor)
|
|
26744
|
+
* const binance = new ExchangeInstance("binance");
|
|
26745
|
+
* const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
|
|
26746
|
+
* const vwap2 = await binance.getAveragePrice("BTCUSDT");
|
|
26747
|
+
* ```
|
|
26748
|
+
*/
|
|
26749
|
+
const Exchange = new ExchangeUtils();
|
|
26750
|
+
|
|
26751
|
+
const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
|
|
26752
|
+
const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
|
|
26753
|
+
const MS_PER_MINUTE$1 = 60000;
|
|
26754
|
+
const INTERVAL_MINUTES$1 = {
|
|
26755
|
+
"1m": 1,
|
|
26756
|
+
"3m": 3,
|
|
26757
|
+
"5m": 5,
|
|
26758
|
+
"15m": 15,
|
|
26759
|
+
"30m": 30,
|
|
26760
|
+
"1h": 60,
|
|
26761
|
+
"2h": 120,
|
|
26762
|
+
"4h": 240,
|
|
26763
|
+
"6h": 360,
|
|
26764
|
+
"8h": 480,
|
|
26765
|
+
};
|
|
26766
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
26767
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
26768
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
26769
|
+
};
|
|
26770
|
+
const BAR_LENGTH = 30;
|
|
26771
|
+
const BAR_FILLED_CHAR = "\u2588";
|
|
26772
|
+
const BAR_EMPTY_CHAR = "\u2591";
|
|
26773
|
+
const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
26774
|
+
const percent = Math.round((fetched / total) * 100);
|
|
26775
|
+
const filled = Math.round((fetched / total) * BAR_LENGTH);
|
|
26776
|
+
const empty = BAR_LENGTH - filled;
|
|
26777
|
+
const bar = BAR_FILLED_CHAR.repeat(filled) + BAR_EMPTY_CHAR.repeat(empty);
|
|
26778
|
+
process.stdout.write(`\r[${bar}] ${percent}% (${fetched}/${total}) ${symbol} ${interval}`);
|
|
26779
|
+
if (fetched === total) {
|
|
26780
|
+
process.stdout.write("\n");
|
|
26781
|
+
}
|
|
26782
|
+
};
|
|
26783
|
+
/**
|
|
26784
|
+
* Checks cached candle timestamps for correct interval alignment.
|
|
26785
|
+
* Reads JSON files directly from persist storage without using abstractions.
|
|
26786
|
+
*
|
|
26787
|
+
* @param params - Validation parameters
|
|
26788
|
+
*/
|
|
26789
|
+
async function checkCandles(params) {
|
|
26790
|
+
const { symbol, exchangeName, interval, baseDir = "./dump/data/candle" } = params;
|
|
26791
|
+
bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
|
|
26792
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
26793
|
+
if (!step) {
|
|
26794
|
+
throw new Error(`checkCandles: unsupported interval=${interval}`);
|
|
26795
|
+
}
|
|
26796
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
26797
|
+
const dir = join(baseDir, exchangeName, symbol, interval);
|
|
26798
|
+
try {
|
|
26799
|
+
await stat(dir);
|
|
26800
|
+
}
|
|
26801
|
+
catch {
|
|
26802
|
+
throw new Error(`checkCandles: cache directory not found: ${dir}`);
|
|
26803
|
+
}
|
|
26804
|
+
// First pass: count files via async iterator (no memory allocation for file list)
|
|
26805
|
+
let totalFiles = 0;
|
|
26806
|
+
for await (const entry of await opendir(dir)) {
|
|
26807
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
26808
|
+
totalFiles++;
|
|
26809
|
+
}
|
|
26810
|
+
}
|
|
26811
|
+
if (totalFiles === 0) {
|
|
26812
|
+
throw new Error(`checkCandles: no cached candles in ${dir}`);
|
|
26813
|
+
}
|
|
26814
|
+
// Second pass: check each file via async iterator with progress
|
|
26815
|
+
let checkd = 0;
|
|
26816
|
+
let prevTimestamp = null;
|
|
26817
|
+
let prevName = null;
|
|
26818
|
+
PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
|
|
26819
|
+
for await (const entry of await opendir(dir)) {
|
|
26820
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
26821
|
+
continue;
|
|
26822
|
+
}
|
|
26823
|
+
const filePath = join(dir, entry.name);
|
|
26824
|
+
const raw = await readFile(filePath, "utf-8");
|
|
26825
|
+
const candle = JSON.parse(raw);
|
|
26826
|
+
const { timestamp } = candle;
|
|
26827
|
+
const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
|
|
26828
|
+
if (timestamp !== aligned) {
|
|
26829
|
+
throw new Error(`checkCandles: ${entry.name} timestamp=${timestamp} is not aligned to ${interval} boundary (expected=${aligned})`);
|
|
26830
|
+
}
|
|
26831
|
+
if (prevTimestamp !== null) {
|
|
26832
|
+
const gap = timestamp - prevTimestamp;
|
|
26833
|
+
if (gap !== stepMs) {
|
|
26834
|
+
throw new Error(`checkCandles: gap between ${prevName} and ${entry.name} is ${gap}ms, expected ${stepMs}ms`);
|
|
26835
|
+
}
|
|
26836
|
+
}
|
|
26837
|
+
prevTimestamp = timestamp;
|
|
26838
|
+
prevName = entry.name;
|
|
26839
|
+
checkd++;
|
|
26840
|
+
PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
|
|
26841
|
+
}
|
|
26842
|
+
console.log(`checkCandles: OK ${totalFiles} candles ${symbol} ${interval}`);
|
|
26843
|
+
}
|
|
26844
|
+
/**
|
|
26845
|
+
* Pre-caches candles for a date range into persist storage.
|
|
26846
|
+
* Downloads all candles matching the interval from `from` to `to`.
|
|
26847
|
+
*
|
|
26848
|
+
* @param params - Cache parameters
|
|
26849
|
+
*/
|
|
26850
|
+
async function warmCandles(params) {
|
|
26851
|
+
const { symbol, exchangeName, interval, from, to } = params;
|
|
26852
|
+
bt.loggerService.info(WARM_CANDLES_METHOD_NAME, {
|
|
26853
|
+
symbol,
|
|
26854
|
+
exchangeName,
|
|
26855
|
+
interval,
|
|
26856
|
+
from,
|
|
26857
|
+
to,
|
|
26858
|
+
});
|
|
26859
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
26860
|
+
if (!step) {
|
|
26861
|
+
throw new Error(`warmCandles: unsupported interval=${interval}`);
|
|
26862
|
+
}
|
|
26863
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
26864
|
+
const instance = new ExchangeInstance(exchangeName);
|
|
26865
|
+
const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
26866
|
+
const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
26867
|
+
const totalCandles = Math.ceil((untilTimestamp - sinceTimestamp) / stepMs);
|
|
26868
|
+
if (totalCandles <= 0) {
|
|
26869
|
+
throw new Error(`warmCandles: no candles to cache (from >= to after alignment)`);
|
|
26870
|
+
}
|
|
26871
|
+
let fetched = 0;
|
|
26872
|
+
let currentSince = sinceTimestamp;
|
|
26873
|
+
PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
|
|
26874
|
+
while (fetched < totalCandles) {
|
|
26875
|
+
const chunkLimit = Math.min(totalCandles - fetched, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
26876
|
+
await instance.getRawCandles(symbol, interval, chunkLimit, currentSince);
|
|
26877
|
+
fetched += chunkLimit;
|
|
26878
|
+
currentSince += chunkLimit * stepMs;
|
|
26879
|
+
PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
|
|
26880
|
+
}
|
|
26881
|
+
}
|
|
26882
|
+
|
|
26883
|
+
const METHOD_NAME = "validate.validate";
|
|
26884
|
+
/**
|
|
26885
|
+
* Retrieves all registered exchanges as a map
|
|
26886
|
+
* @private
|
|
26887
|
+
* @returns Map of exchange names
|
|
26888
|
+
*/
|
|
26889
|
+
const getExchangeMap = async () => {
|
|
26890
|
+
const exchangeMap = {};
|
|
26891
|
+
for (const { exchangeName } of await bt.exchangeValidationService.list()) {
|
|
26892
|
+
Object.assign(exchangeMap, { [exchangeName]: exchangeName });
|
|
26893
|
+
}
|
|
26894
|
+
return exchangeMap;
|
|
26895
|
+
};
|
|
26896
|
+
/**
|
|
26897
|
+
* Retrieves all registered frames as a map
|
|
26898
|
+
* @private
|
|
26899
|
+
* @returns Map of frame names
|
|
26900
|
+
*/
|
|
26901
|
+
const getFrameMap = async () => {
|
|
26902
|
+
const frameMap = {};
|
|
26903
|
+
for (const { frameName } of await bt.frameValidationService.list()) {
|
|
26904
|
+
Object.assign(frameMap, { [frameName]: frameName });
|
|
26905
|
+
}
|
|
26906
|
+
return frameMap;
|
|
26907
|
+
};
|
|
26908
|
+
/**
|
|
26909
|
+
* Retrieves all registered strategies as a map
|
|
26910
|
+
* @private
|
|
26911
|
+
* @returns Map of strategy names
|
|
26912
|
+
*/
|
|
26913
|
+
const getStrategyMap = async () => {
|
|
26914
|
+
const strategyMap = {};
|
|
26915
|
+
for (const { strategyName } of await bt.strategyValidationService.list()) {
|
|
26916
|
+
Object.assign(strategyMap, { [strategyName]: strategyName });
|
|
26917
|
+
}
|
|
26918
|
+
return strategyMap;
|
|
26919
|
+
};
|
|
26920
|
+
/**
|
|
26921
|
+
* Retrieves all registered risk profiles as a map
|
|
26922
|
+
* @private
|
|
26923
|
+
* @returns Map of risk names
|
|
26924
|
+
*/
|
|
26925
|
+
const getRiskMap = async () => {
|
|
26926
|
+
const riskMap = {};
|
|
26927
|
+
for (const { riskName } of await bt.riskValidationService.list()) {
|
|
26928
|
+
Object.assign(riskMap, { [riskName]: riskName });
|
|
26929
|
+
}
|
|
26930
|
+
return riskMap;
|
|
26931
|
+
};
|
|
26932
|
+
/**
|
|
26933
|
+
* Retrieves all registered action handlers as a map
|
|
26934
|
+
* @private
|
|
26935
|
+
* @returns Map of action names
|
|
26936
|
+
*/
|
|
26937
|
+
const getActionMap = async () => {
|
|
26938
|
+
const actionMap = {};
|
|
26939
|
+
for (const { actionName } of await bt.actionValidationService.list()) {
|
|
26940
|
+
Object.assign(actionMap, { [actionName]: actionName });
|
|
26941
|
+
}
|
|
26942
|
+
return actionMap;
|
|
26943
|
+
};
|
|
26944
|
+
/**
|
|
26945
|
+
* Retrieves all registered sizing strategies as a map
|
|
26946
|
+
* @private
|
|
26947
|
+
* @returns Map of sizing names
|
|
26948
|
+
*/
|
|
26949
|
+
const getSizingMap = async () => {
|
|
26950
|
+
const sizingMap = {};
|
|
26951
|
+
for (const { sizingName } of await bt.sizingValidationService.list()) {
|
|
26952
|
+
Object.assign(sizingMap, { [sizingName]: sizingName });
|
|
26953
|
+
}
|
|
26954
|
+
return sizingMap;
|
|
26955
|
+
};
|
|
26956
|
+
/**
|
|
26957
|
+
* Retrieves all registered walkers as a map
|
|
26958
|
+
* @private
|
|
26959
|
+
* @returns Map of walker names
|
|
26960
|
+
*/
|
|
26961
|
+
const getWalkerMap = async () => {
|
|
26962
|
+
const walkerMap = {};
|
|
26963
|
+
for (const { walkerName } of await bt.walkerValidationService.list()) {
|
|
26964
|
+
Object.assign(walkerMap, { [walkerName]: walkerName });
|
|
26965
|
+
}
|
|
26966
|
+
return walkerMap;
|
|
26967
|
+
};
|
|
26968
|
+
/**
|
|
26969
|
+
* Internal validation function that processes all provided entity enums.
|
|
26970
|
+
*
|
|
26971
|
+
* Iterates through each enum's values and validates them against their
|
|
26972
|
+
* respective validation services. Uses memoized validation for performance.
|
|
26973
|
+
*
|
|
26974
|
+
* If entity enums are not provided, fetches all registered entities from
|
|
26975
|
+
* their respective validation services and validates them.
|
|
26976
|
+
*
|
|
26977
|
+
* @private
|
|
26978
|
+
* @param args - Validation arguments containing entity name enums
|
|
26979
|
+
* @throws {Error} If any entity name is not found in its registry
|
|
26980
|
+
*/
|
|
26981
|
+
const validateInternal = async (args) => {
|
|
26982
|
+
const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
|
|
26983
|
+
for (const exchangeName of Object.values(ExchangeName)) {
|
|
26984
|
+
bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
|
|
26985
|
+
}
|
|
26986
|
+
for (const frameName of Object.values(FrameName)) {
|
|
26987
|
+
bt.frameValidationService.validate(frameName, METHOD_NAME);
|
|
26988
|
+
}
|
|
26989
|
+
for (const strategyName of Object.values(StrategyName)) {
|
|
26990
|
+
bt.strategyValidationService.validate(strategyName, METHOD_NAME);
|
|
26991
|
+
}
|
|
26992
|
+
for (const riskName of Object.values(RiskName)) {
|
|
26993
|
+
bt.riskValidationService.validate(riskName, METHOD_NAME);
|
|
26994
|
+
}
|
|
26995
|
+
for (const actionName of Object.values(ActionName)) {
|
|
26996
|
+
bt.actionValidationService.validate(actionName, METHOD_NAME);
|
|
26997
|
+
}
|
|
26998
|
+
for (const sizingName of Object.values(SizingName)) {
|
|
26999
|
+
bt.sizingValidationService.validate(sizingName, METHOD_NAME);
|
|
27000
|
+
}
|
|
27001
|
+
for (const walkerName of Object.values(WalkerName)) {
|
|
27002
|
+
bt.walkerValidationService.validate(walkerName, METHOD_NAME);
|
|
27003
|
+
}
|
|
27004
|
+
};
|
|
27005
|
+
/**
|
|
27006
|
+
* Validates the existence of all provided entity names across validation services.
|
|
27007
|
+
*
|
|
27008
|
+
* This function accepts enum objects for various entity types (exchanges, frames,
|
|
27009
|
+
* strategies, risks, sizings, walkers) and validates that each entity
|
|
27010
|
+
* name exists in its respective registry. Validation results are memoized for performance.
|
|
27011
|
+
*
|
|
27012
|
+
* If no arguments are provided (or specific entity types are omitted), the function
|
|
27013
|
+
* automatically fetches and validates ALL registered entities from their respective
|
|
27014
|
+
* validation services. This is useful for comprehensive validation of the entire setup.
|
|
27015
|
+
*
|
|
27016
|
+
* Use this before running backtests or optimizations to ensure all referenced
|
|
27017
|
+
* entities are properly registered and configured.
|
|
27018
|
+
*
|
|
27019
|
+
* @public
|
|
27020
|
+
* @param args - Partial validation arguments containing entity name enums to validate.
|
|
27021
|
+
* If empty or omitted, validates all registered entities.
|
|
27022
|
+
* @throws {Error} If any entity name is not found in its validation service
|
|
27023
|
+
*
|
|
27024
|
+
* @example
|
|
27025
|
+
* ```typescript
|
|
27026
|
+
* // Validate ALL registered entities (exchanges, frames, strategies, etc.)
|
|
27027
|
+
* await validate({});
|
|
27028
|
+
* ```
|
|
27029
|
+
*
|
|
27030
|
+
* @example
|
|
27031
|
+
* ```typescript
|
|
27032
|
+
* // Define your entity name enums
|
|
27033
|
+
* enum ExchangeName {
|
|
27034
|
+
* BINANCE = "binance",
|
|
27035
|
+
* BYBIT = "bybit"
|
|
27036
|
+
* }
|
|
27037
|
+
*
|
|
27038
|
+
* enum StrategyName {
|
|
27039
|
+
* MOMENTUM_BTC = "momentum-btc"
|
|
27040
|
+
* }
|
|
27041
|
+
*
|
|
27042
|
+
* // Validate specific entities before running backtest
|
|
27043
|
+
* await validate({
|
|
27044
|
+
* ExchangeName,
|
|
27045
|
+
* StrategyName,
|
|
27046
|
+
* });
|
|
27047
|
+
* ```
|
|
27048
|
+
*
|
|
27049
|
+
* @example
|
|
27050
|
+
* ```typescript
|
|
27051
|
+
* // Validate specific entity types
|
|
27052
|
+
* await validate({
|
|
27053
|
+
* RiskName: { CONSERVATIVE: "conservative" },
|
|
27054
|
+
* SizingName: { FIXED_1000: "fixed-1000" },
|
|
27055
|
+
* });
|
|
27056
|
+
* ```
|
|
27057
|
+
*/
|
|
27058
|
+
async function validate(args = {}) {
|
|
27059
|
+
bt.loggerService.log(METHOD_NAME);
|
|
27060
|
+
return await validateInternal(args);
|
|
27061
|
+
}
|
|
27062
|
+
|
|
27063
|
+
const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
|
|
27064
|
+
const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
|
|
27065
|
+
const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
|
|
27066
|
+
const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
|
|
27067
|
+
const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
|
|
27068
|
+
const GET_RISK_METHOD_NAME = "get.getRiskSchema";
|
|
27069
|
+
const GET_ACTION_METHOD_NAME = "get.getActionSchema";
|
|
27070
|
+
/**
|
|
27071
|
+
* Retrieves a registered strategy schema by name.
|
|
27072
|
+
*
|
|
27073
|
+
* @param strategyName - Unique strategy identifier
|
|
27074
|
+
* @returns The strategy schema configuration object
|
|
27075
|
+
* @throws Error if strategy is not registered
|
|
27076
|
+
*
|
|
27077
|
+
* @example
|
|
27078
|
+
* ```typescript
|
|
27079
|
+
* const strategy = getStrategy("my-strategy");
|
|
27080
|
+
* console.log(strategy.interval); // "5m"
|
|
27081
|
+
* console.log(strategy.getSignal); // async function
|
|
27082
|
+
* ```
|
|
27083
|
+
*/
|
|
27084
|
+
function getStrategySchema(strategyName) {
|
|
27085
|
+
bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
|
|
27086
|
+
strategyName,
|
|
27087
|
+
});
|
|
27088
|
+
bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
|
|
27089
|
+
return bt.strategySchemaService.get(strategyName);
|
|
27090
|
+
}
|
|
27091
|
+
/**
|
|
27092
|
+
* Retrieves a registered exchange schema by name.
|
|
27093
|
+
*
|
|
27094
|
+
* @param exchangeName - Unique exchange identifier
|
|
27095
|
+
* @returns The exchange schema configuration object
|
|
27096
|
+
* @throws Error if exchange is not registered
|
|
27097
|
+
*
|
|
27098
|
+
* @example
|
|
27099
|
+
* ```typescript
|
|
27100
|
+
* const exchange = getExchange("binance");
|
|
27101
|
+
* console.log(exchange.getCandles); // async function
|
|
27102
|
+
* console.log(exchange.formatPrice); // async function
|
|
27103
|
+
* ```
|
|
27104
|
+
*/
|
|
27105
|
+
function getExchangeSchema(exchangeName) {
|
|
27106
|
+
bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
|
|
27107
|
+
exchangeName,
|
|
27108
|
+
});
|
|
27109
|
+
bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
|
|
27110
|
+
return bt.exchangeSchemaService.get(exchangeName);
|
|
27111
|
+
}
|
|
27112
|
+
/**
|
|
27113
|
+
* Retrieves a registered frame schema by name.
|
|
27114
|
+
*
|
|
27115
|
+
* @param frameName - Unique frame identifier
|
|
27116
|
+
* @returns The frame schema configuration object
|
|
27117
|
+
* @throws Error if frame is not registered
|
|
27118
|
+
*
|
|
27119
|
+
* @example
|
|
27120
|
+
* ```typescript
|
|
27121
|
+
* const frame = getFrame("1d-backtest");
|
|
27122
|
+
* console.log(frame.interval); // "1m"
|
|
27123
|
+
* console.log(frame.startDate); // Date object
|
|
27124
|
+
* console.log(frame.endDate); // Date object
|
|
27125
|
+
* ```
|
|
27126
|
+
*/
|
|
27127
|
+
function getFrameSchema(frameName) {
|
|
27128
|
+
bt.loggerService.log(GET_FRAME_METHOD_NAME, {
|
|
27129
|
+
frameName,
|
|
27130
|
+
});
|
|
27131
|
+
bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
|
|
27132
|
+
return bt.frameSchemaService.get(frameName);
|
|
27133
|
+
}
|
|
27134
|
+
/**
|
|
27135
|
+
* Retrieves a registered walker schema by name.
|
|
27136
|
+
*
|
|
27137
|
+
* @param walkerName - Unique walker identifier
|
|
27138
|
+
* @returns The walker schema configuration object
|
|
27139
|
+
* @throws Error if walker is not registered
|
|
27140
|
+
*
|
|
27141
|
+
* @example
|
|
27142
|
+
* ```typescript
|
|
27143
|
+
* const walker = getWalker("llm-prompt-optimizer");
|
|
27144
|
+
* console.log(walker.exchangeName); // "binance"
|
|
27145
|
+
* console.log(walker.frameName); // "1d-backtest"
|
|
27146
|
+
* console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
|
|
27147
|
+
* console.log(walker.metric); // "sharpeRatio"
|
|
27148
|
+
* ```
|
|
27149
|
+
*/
|
|
27150
|
+
function getWalkerSchema(walkerName) {
|
|
27151
|
+
bt.loggerService.log(GET_WALKER_METHOD_NAME, {
|
|
27152
|
+
walkerName,
|
|
27153
|
+
});
|
|
27154
|
+
bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
|
|
27155
|
+
return bt.walkerSchemaService.get(walkerName);
|
|
27156
|
+
}
|
|
27157
|
+
/**
|
|
27158
|
+
* Retrieves a registered sizing schema by name.
|
|
27159
|
+
*
|
|
27160
|
+
* @param sizingName - Unique sizing identifier
|
|
27161
|
+
* @returns The sizing schema configuration object
|
|
27162
|
+
* @throws Error if sizing is not registered
|
|
27163
|
+
*
|
|
27164
|
+
* @example
|
|
27165
|
+
* ```typescript
|
|
27166
|
+
* const sizing = getSizing("conservative");
|
|
27167
|
+
* console.log(sizing.method); // "fixed-percentage"
|
|
27168
|
+
* console.log(sizing.riskPercentage); // 1
|
|
27169
|
+
* console.log(sizing.maxPositionPercentage); // 10
|
|
27170
|
+
* ```
|
|
27171
|
+
*/
|
|
27172
|
+
function getSizingSchema(sizingName) {
|
|
27173
|
+
bt.loggerService.log(GET_SIZING_METHOD_NAME, {
|
|
27174
|
+
sizingName,
|
|
27175
|
+
});
|
|
27176
|
+
bt.sizingValidationService.validate(sizingName, GET_SIZING_METHOD_NAME);
|
|
27177
|
+
return bt.sizingSchemaService.get(sizingName);
|
|
27178
|
+
}
|
|
27179
|
+
/**
|
|
27180
|
+
* Retrieves a registered risk schema by name.
|
|
27181
|
+
*
|
|
27182
|
+
* @param riskName - Unique risk identifier
|
|
27183
|
+
* @returns The risk schema configuration object
|
|
27184
|
+
* @throws Error if risk is not registered
|
|
27185
|
+
*
|
|
27186
|
+
* @example
|
|
27187
|
+
* ```typescript
|
|
27188
|
+
* const risk = getRisk("conservative");
|
|
27189
|
+
* console.log(risk.maxConcurrentPositions); // 5
|
|
27190
|
+
* console.log(risk.validations); // Array of validation functions
|
|
27191
|
+
* ```
|
|
27192
|
+
*/
|
|
27193
|
+
function getRiskSchema(riskName) {
|
|
27194
|
+
bt.loggerService.log(GET_RISK_METHOD_NAME, {
|
|
27195
|
+
riskName,
|
|
27196
|
+
});
|
|
27197
|
+
bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
|
|
27198
|
+
return bt.riskSchemaService.get(riskName);
|
|
27199
|
+
}
|
|
27200
|
+
/**
|
|
27201
|
+
* Retrieves a registered action schema by name.
|
|
27202
|
+
*
|
|
27203
|
+
* @param actionName - Unique action identifier
|
|
27204
|
+
* @returns The action schema configuration object
|
|
27205
|
+
* @throws Error if action is not registered
|
|
27206
|
+
*
|
|
27207
|
+
* @example
|
|
27208
|
+
* ```typescript
|
|
27209
|
+
* const action = getAction("telegram-notifier");
|
|
27210
|
+
* console.log(action.handler); // Class constructor or object
|
|
27211
|
+
* console.log(action.callbacks); // Optional lifecycle callbacks
|
|
27212
|
+
* ```
|
|
27213
|
+
*/
|
|
27214
|
+
function getActionSchema(actionName) {
|
|
27215
|
+
bt.loggerService.log(GET_ACTION_METHOD_NAME, {
|
|
27216
|
+
actionName,
|
|
27217
|
+
});
|
|
27218
|
+
bt.actionValidationService.validate(actionName, GET_ACTION_METHOD_NAME);
|
|
27219
|
+
return bt.actionSchemaService.get(actionName);
|
|
27220
|
+
}
|
|
27221
|
+
|
|
27222
|
+
const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
|
|
27223
|
+
const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
|
|
27224
|
+
const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
|
|
27225
|
+
const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
27226
|
+
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
27227
|
+
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
27228
|
+
const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
|
|
27229
|
+
const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
27230
|
+
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
27231
|
+
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
27232
|
+
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
27233
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
27234
|
+
/**
|
|
27235
|
+
* Checks if trade context is active (execution and method contexts).
|
|
27236
|
+
*
|
|
27237
|
+
* Returns true when both contexts are active, which is required for calling
|
|
27238
|
+
* exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
|
|
27239
|
+
* getDate, and getMode.
|
|
27240
|
+
*
|
|
27241
|
+
* @returns true if trade context is active, false otherwise
|
|
27242
|
+
*
|
|
27243
|
+
* @example
|
|
27244
|
+
* ```typescript
|
|
27245
|
+
* import { hasTradeContext, getCandles } from "backtest-kit";
|
|
27246
|
+
*
|
|
27247
|
+
* if (hasTradeContext()) {
|
|
27248
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
27249
|
+
* } else {
|
|
27250
|
+
* console.log("Trade context not active");
|
|
27251
|
+
* }
|
|
27252
|
+
* ```
|
|
27253
|
+
*/
|
|
27254
|
+
function hasTradeContext() {
|
|
27255
|
+
bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
|
|
27256
|
+
return ExecutionContextService.hasContext() && MethodContextService.hasContext();
|
|
27257
|
+
}
|
|
27258
|
+
/**
|
|
27259
|
+
* Fetches historical candle data from the registered exchange.
|
|
27260
|
+
*
|
|
27261
|
+
* Candles are fetched backwards from the current execution context time.
|
|
27262
|
+
* Uses the exchange's getCandles implementation.
|
|
27263
|
+
*
|
|
27264
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
27265
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
27266
|
+
* @param limit - Number of candles to fetch
|
|
27267
|
+
* @returns Promise resolving to array of candle data
|
|
27268
|
+
*
|
|
27269
|
+
* @example
|
|
27270
|
+
* ```typescript
|
|
27271
|
+
* const candles = await getCandles("BTCUSDT", "1m", 100);
|
|
27272
|
+
* console.log(candles[0]); // { timestamp, open, high, low, close, volume }
|
|
27273
|
+
* ```
|
|
27274
|
+
*/
|
|
27275
|
+
async function getCandles(symbol, interval, limit) {
|
|
27276
|
+
bt.loggerService.info(GET_CANDLES_METHOD_NAME, {
|
|
27277
|
+
symbol,
|
|
27278
|
+
interval,
|
|
26419
27279
|
limit,
|
|
26420
27280
|
});
|
|
26421
27281
|
if (!ExecutionContextService.hasContext()) {
|
|
@@ -35155,143 +36015,7 @@ class NotificationBacktestAdapter {
|
|
|
35155
36015
|
* @param data - The risk contract data
|
|
35156
36016
|
*/
|
|
35157
36017
|
this.handleRisk = async (data) => {
|
|
35158
|
-
return await this._notificationBacktestUtils.handleRisk(data);
|
|
35159
|
-
};
|
|
35160
|
-
/**
|
|
35161
|
-
* Handles error event.
|
|
35162
|
-
* Proxies call to the underlying notification adapter.
|
|
35163
|
-
* @param error - The error object
|
|
35164
|
-
*/
|
|
35165
|
-
this.handleError = async (error) => {
|
|
35166
|
-
return await this._notificationBacktestUtils.handleError(error);
|
|
35167
|
-
};
|
|
35168
|
-
/**
|
|
35169
|
-
* Handles critical error event.
|
|
35170
|
-
* Proxies call to the underlying notification adapter.
|
|
35171
|
-
* @param error - The error object
|
|
35172
|
-
*/
|
|
35173
|
-
this.handleCriticalError = async (error) => {
|
|
35174
|
-
return await this._notificationBacktestUtils.handleCriticalError(error);
|
|
35175
|
-
};
|
|
35176
|
-
/**
|
|
35177
|
-
* Handles validation error event.
|
|
35178
|
-
* Proxies call to the underlying notification adapter.
|
|
35179
|
-
* @param error - The error object
|
|
35180
|
-
*/
|
|
35181
|
-
this.handleValidationError = async (error) => {
|
|
35182
|
-
return await this._notificationBacktestUtils.handleValidationError(error);
|
|
35183
|
-
};
|
|
35184
|
-
/**
|
|
35185
|
-
* Gets all stored notifications.
|
|
35186
|
-
* Proxies call to the underlying notification adapter.
|
|
35187
|
-
* @returns Array of all notification models
|
|
35188
|
-
*/
|
|
35189
|
-
this.getData = async () => {
|
|
35190
|
-
return await this._notificationBacktestUtils.getData();
|
|
35191
|
-
};
|
|
35192
|
-
/**
|
|
35193
|
-
* Clears all stored notifications.
|
|
35194
|
-
* Proxies call to the underlying notification adapter.
|
|
35195
|
-
*/
|
|
35196
|
-
this.clear = async () => {
|
|
35197
|
-
return await this._notificationBacktestUtils.clear();
|
|
35198
|
-
};
|
|
35199
|
-
/**
|
|
35200
|
-
* Sets the notification adapter constructor.
|
|
35201
|
-
* All future notification operations will use this adapter.
|
|
35202
|
-
*
|
|
35203
|
-
* @param Ctor - Constructor for notification adapter
|
|
35204
|
-
*/
|
|
35205
|
-
this.useNotificationAdapter = (Ctor) => {
|
|
35206
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
35207
|
-
this._notificationBacktestUtils = Reflect.construct(Ctor, []);
|
|
35208
|
-
};
|
|
35209
|
-
/**
|
|
35210
|
-
* Switches to dummy notification adapter.
|
|
35211
|
-
* All future notification writes will be no-ops.
|
|
35212
|
-
*/
|
|
35213
|
-
this.useDummy = () => {
|
|
35214
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
35215
|
-
this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
|
|
35216
|
-
};
|
|
35217
|
-
/**
|
|
35218
|
-
* Switches to in-memory notification adapter (default).
|
|
35219
|
-
* Notifications will be stored in memory only.
|
|
35220
|
-
*/
|
|
35221
|
-
this.useMemory = () => {
|
|
35222
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
35223
|
-
this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
|
|
35224
|
-
};
|
|
35225
|
-
/**
|
|
35226
|
-
* Switches to persistent notification adapter.
|
|
35227
|
-
* Notifications will be persisted to disk.
|
|
35228
|
-
*/
|
|
35229
|
-
this.usePersist = () => {
|
|
35230
|
-
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
35231
|
-
this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
|
|
35232
|
-
};
|
|
35233
|
-
}
|
|
35234
|
-
}
|
|
35235
|
-
/**
|
|
35236
|
-
* Live trading notification adapter with pluggable notification backend.
|
|
35237
|
-
*
|
|
35238
|
-
* Features:
|
|
35239
|
-
* - Adapter pattern for swappable notification implementations
|
|
35240
|
-
* - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
|
|
35241
|
-
* - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
|
|
35242
|
-
* - Convenience methods: usePersist(), useMemory(), useDummy()
|
|
35243
|
-
*/
|
|
35244
|
-
class NotificationLiveAdapter {
|
|
35245
|
-
constructor() {
|
|
35246
|
-
/** Internal notification utils instance */
|
|
35247
|
-
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
35248
|
-
/**
|
|
35249
|
-
* Handles signal events.
|
|
35250
|
-
* Proxies call to the underlying notification adapter.
|
|
35251
|
-
* @param data - The strategy tick result data
|
|
35252
|
-
*/
|
|
35253
|
-
this.handleSignal = async (data) => {
|
|
35254
|
-
return await this._notificationLiveUtils.handleSignal(data);
|
|
35255
|
-
};
|
|
35256
|
-
/**
|
|
35257
|
-
* Handles partial profit availability event.
|
|
35258
|
-
* Proxies call to the underlying notification adapter.
|
|
35259
|
-
* @param data - The partial profit contract data
|
|
35260
|
-
*/
|
|
35261
|
-
this.handlePartialProfit = async (data) => {
|
|
35262
|
-
return await this._notificationLiveUtils.handlePartialProfit(data);
|
|
35263
|
-
};
|
|
35264
|
-
/**
|
|
35265
|
-
* Handles partial loss availability event.
|
|
35266
|
-
* Proxies call to the underlying notification adapter.
|
|
35267
|
-
* @param data - The partial loss contract data
|
|
35268
|
-
*/
|
|
35269
|
-
this.handlePartialLoss = async (data) => {
|
|
35270
|
-
return await this._notificationLiveUtils.handlePartialLoss(data);
|
|
35271
|
-
};
|
|
35272
|
-
/**
|
|
35273
|
-
* Handles breakeven availability event.
|
|
35274
|
-
* Proxies call to the underlying notification adapter.
|
|
35275
|
-
* @param data - The breakeven contract data
|
|
35276
|
-
*/
|
|
35277
|
-
this.handleBreakeven = async (data) => {
|
|
35278
|
-
return await this._notificationLiveUtils.handleBreakeven(data);
|
|
35279
|
-
};
|
|
35280
|
-
/**
|
|
35281
|
-
* Handles strategy commit events.
|
|
35282
|
-
* Proxies call to the underlying notification adapter.
|
|
35283
|
-
* @param data - The strategy commit contract data
|
|
35284
|
-
*/
|
|
35285
|
-
this.handleStrategyCommit = async (data) => {
|
|
35286
|
-
return await this._notificationLiveUtils.handleStrategyCommit(data);
|
|
35287
|
-
};
|
|
35288
|
-
/**
|
|
35289
|
-
* Handles risk rejection event.
|
|
35290
|
-
* Proxies call to the underlying notification adapter.
|
|
35291
|
-
* @param data - The risk contract data
|
|
35292
|
-
*/
|
|
35293
|
-
this.handleRisk = async (data) => {
|
|
35294
|
-
return await this._notificationLiveUtils.handleRisk(data);
|
|
36018
|
+
return await this._notificationBacktestUtils.handleRisk(data);
|
|
35295
36019
|
};
|
|
35296
36020
|
/**
|
|
35297
36021
|
* Handles error event.
|
|
@@ -35299,7 +36023,7 @@ class NotificationLiveAdapter {
|
|
|
35299
36023
|
* @param error - The error object
|
|
35300
36024
|
*/
|
|
35301
36025
|
this.handleError = async (error) => {
|
|
35302
|
-
return await this.
|
|
36026
|
+
return await this._notificationBacktestUtils.handleError(error);
|
|
35303
36027
|
};
|
|
35304
36028
|
/**
|
|
35305
36029
|
* Handles critical error event.
|
|
@@ -35307,7 +36031,7 @@ class NotificationLiveAdapter {
|
|
|
35307
36031
|
* @param error - The error object
|
|
35308
36032
|
*/
|
|
35309
36033
|
this.handleCriticalError = async (error) => {
|
|
35310
|
-
return await this.
|
|
36034
|
+
return await this._notificationBacktestUtils.handleCriticalError(error);
|
|
35311
36035
|
};
|
|
35312
36036
|
/**
|
|
35313
36037
|
* Handles validation error event.
|
|
@@ -35315,7 +36039,7 @@ class NotificationLiveAdapter {
|
|
|
35315
36039
|
* @param error - The error object
|
|
35316
36040
|
*/
|
|
35317
36041
|
this.handleValidationError = async (error) => {
|
|
35318
|
-
return await this.
|
|
36042
|
+
return await this._notificationBacktestUtils.handleValidationError(error);
|
|
35319
36043
|
};
|
|
35320
36044
|
/**
|
|
35321
36045
|
* Gets all stored notifications.
|
|
@@ -35323,14 +36047,14 @@ class NotificationLiveAdapter {
|
|
|
35323
36047
|
* @returns Array of all notification models
|
|
35324
36048
|
*/
|
|
35325
36049
|
this.getData = async () => {
|
|
35326
|
-
return await this.
|
|
36050
|
+
return await this._notificationBacktestUtils.getData();
|
|
35327
36051
|
};
|
|
35328
36052
|
/**
|
|
35329
36053
|
* Clears all stored notifications.
|
|
35330
36054
|
* Proxies call to the underlying notification adapter.
|
|
35331
36055
|
*/
|
|
35332
36056
|
this.clear = async () => {
|
|
35333
|
-
return await this.
|
|
36057
|
+
return await this._notificationBacktestUtils.clear();
|
|
35334
36058
|
};
|
|
35335
36059
|
/**
|
|
35336
36060
|
* Sets the notification adapter constructor.
|
|
@@ -35339,897 +36063,305 @@ class NotificationLiveAdapter {
|
|
|
35339
36063
|
* @param Ctor - Constructor for notification adapter
|
|
35340
36064
|
*/
|
|
35341
36065
|
this.useNotificationAdapter = (Ctor) => {
|
|
35342
|
-
bt.loggerService.info(
|
|
35343
|
-
this.
|
|
36066
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
36067
|
+
this._notificationBacktestUtils = Reflect.construct(Ctor, []);
|
|
35344
36068
|
};
|
|
35345
36069
|
/**
|
|
35346
36070
|
* Switches to dummy notification adapter.
|
|
35347
36071
|
* All future notification writes will be no-ops.
|
|
35348
36072
|
*/
|
|
35349
36073
|
this.useDummy = () => {
|
|
35350
|
-
bt.loggerService.info(
|
|
35351
|
-
this.
|
|
36074
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
36075
|
+
this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
|
|
35352
36076
|
};
|
|
35353
36077
|
/**
|
|
35354
36078
|
* Switches to in-memory notification adapter (default).
|
|
35355
36079
|
* Notifications will be stored in memory only.
|
|
35356
36080
|
*/
|
|
35357
36081
|
this.useMemory = () => {
|
|
35358
|
-
bt.loggerService.info(
|
|
35359
|
-
this.
|
|
36082
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
36083
|
+
this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
|
|
35360
36084
|
};
|
|
35361
36085
|
/**
|
|
35362
36086
|
* Switches to persistent notification adapter.
|
|
35363
36087
|
* Notifications will be persisted to disk.
|
|
35364
36088
|
*/
|
|
35365
36089
|
this.usePersist = () => {
|
|
35366
|
-
bt.loggerService.info(
|
|
35367
|
-
this.
|
|
36090
|
+
bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
36091
|
+
this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
|
|
35368
36092
|
};
|
|
35369
36093
|
}
|
|
35370
36094
|
}
|
|
35371
36095
|
/**
|
|
35372
|
-
*
|
|
36096
|
+
* Live trading notification adapter with pluggable notification backend.
|
|
35373
36097
|
*
|
|
35374
36098
|
* Features:
|
|
35375
|
-
* -
|
|
35376
|
-
* -
|
|
35377
|
-
* -
|
|
35378
|
-
* -
|
|
36099
|
+
* - Adapter pattern for swappable notification implementations
|
|
36100
|
+
* - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
|
|
36101
|
+
* - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
|
|
36102
|
+
* - Convenience methods: usePersist(), useMemory(), useDummy()
|
|
35379
36103
|
*/
|
|
35380
|
-
class
|
|
36104
|
+
class NotificationLiveAdapter {
|
|
35381
36105
|
constructor() {
|
|
36106
|
+
/** Internal notification utils instance */
|
|
36107
|
+
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
35382
36108
|
/**
|
|
35383
|
-
*
|
|
35384
|
-
*
|
|
35385
|
-
*
|
|
35386
|
-
* @returns Cleanup function that unsubscribes from all emitters
|
|
36109
|
+
* Handles signal events.
|
|
36110
|
+
* Proxies call to the underlying notification adapter.
|
|
36111
|
+
* @param data - The strategy tick result data
|
|
35387
36112
|
*/
|
|
35388
|
-
this.
|
|
35389
|
-
|
|
35390
|
-
|
|
35391
|
-
let unBacktest;
|
|
35392
|
-
{
|
|
35393
|
-
const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
|
|
35394
|
-
const unBacktestPartialProfit = partialProfitSubject
|
|
35395
|
-
.filter(({ backtest }) => backtest)
|
|
35396
|
-
.connect((data) => NotificationBacktest.handlePartialProfit(data));
|
|
35397
|
-
const unBacktestPartialLoss = partialLossSubject
|
|
35398
|
-
.filter(({ backtest }) => backtest)
|
|
35399
|
-
.connect((data) => NotificationBacktest.handlePartialLoss(data));
|
|
35400
|
-
const unBacktestBreakeven = breakevenSubject
|
|
35401
|
-
.filter(({ backtest }) => backtest)
|
|
35402
|
-
.connect((data) => NotificationBacktest.handleBreakeven(data));
|
|
35403
|
-
const unBacktestStrategyCommit = strategyCommitSubject
|
|
35404
|
-
.filter(({ backtest }) => backtest)
|
|
35405
|
-
.connect((data) => NotificationBacktest.handleStrategyCommit(data));
|
|
35406
|
-
const unBacktestRisk = riskSubject
|
|
35407
|
-
.filter(({ backtest }) => backtest)
|
|
35408
|
-
.connect((data) => NotificationBacktest.handleRisk(data));
|
|
35409
|
-
const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
|
|
35410
|
-
const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
|
|
35411
|
-
const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
|
|
35412
|
-
unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
|
|
35413
|
-
}
|
|
35414
|
-
{
|
|
35415
|
-
const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
|
|
35416
|
-
const unLivePartialProfit = partialProfitSubject
|
|
35417
|
-
.filter(({ backtest }) => !backtest)
|
|
35418
|
-
.connect((data) => NotificationLive.handlePartialProfit(data));
|
|
35419
|
-
const unLivePartialLoss = partialLossSubject
|
|
35420
|
-
.filter(({ backtest }) => !backtest)
|
|
35421
|
-
.connect((data) => NotificationLive.handlePartialLoss(data));
|
|
35422
|
-
const unLiveBreakeven = breakevenSubject
|
|
35423
|
-
.filter(({ backtest }) => !backtest)
|
|
35424
|
-
.connect((data) => NotificationLive.handleBreakeven(data));
|
|
35425
|
-
const unLiveStrategyCommit = strategyCommitSubject
|
|
35426
|
-
.filter(({ backtest }) => !backtest)
|
|
35427
|
-
.connect((data) => NotificationLive.handleStrategyCommit(data));
|
|
35428
|
-
const unLiveRisk = riskSubject
|
|
35429
|
-
.filter(({ backtest }) => !backtest)
|
|
35430
|
-
.connect((data) => NotificationLive.handleRisk(data));
|
|
35431
|
-
const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
|
|
35432
|
-
const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
|
|
35433
|
-
const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
|
|
35434
|
-
unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
|
|
35435
|
-
}
|
|
35436
|
-
return () => {
|
|
35437
|
-
unLive();
|
|
35438
|
-
unBacktest();
|
|
35439
|
-
this.enable.clear();
|
|
35440
|
-
};
|
|
35441
|
-
});
|
|
36113
|
+
this.handleSignal = async (data) => {
|
|
36114
|
+
return await this._notificationLiveUtils.handleSignal(data);
|
|
36115
|
+
};
|
|
35442
36116
|
/**
|
|
35443
|
-
*
|
|
35444
|
-
*
|
|
36117
|
+
* Handles partial profit availability event.
|
|
36118
|
+
* Proxies call to the underlying notification adapter.
|
|
36119
|
+
* @param data - The partial profit contract data
|
|
35445
36120
|
*/
|
|
35446
|
-
this.
|
|
35447
|
-
|
|
35448
|
-
if (this.enable.hasValue()) {
|
|
35449
|
-
const lastSubscription = this.enable();
|
|
35450
|
-
lastSubscription();
|
|
35451
|
-
}
|
|
36121
|
+
this.handlePartialProfit = async (data) => {
|
|
36122
|
+
return await this._notificationLiveUtils.handlePartialProfit(data);
|
|
35452
36123
|
};
|
|
35453
36124
|
/**
|
|
35454
|
-
*
|
|
35455
|
-
*
|
|
35456
|
-
* @
|
|
35457
|
-
* @throws Error if NotificationAdapter is not enabled
|
|
36125
|
+
* Handles partial loss availability event.
|
|
36126
|
+
* Proxies call to the underlying notification adapter.
|
|
36127
|
+
* @param data - The partial loss contract data
|
|
35458
36128
|
*/
|
|
35459
|
-
this.
|
|
35460
|
-
|
|
35461
|
-
backtest: isBacktest,
|
|
35462
|
-
});
|
|
35463
|
-
if (!this.enable.hasValue()) {
|
|
35464
|
-
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
35465
|
-
}
|
|
35466
|
-
if (isBacktest) {
|
|
35467
|
-
return await NotificationBacktest.getData();
|
|
35468
|
-
}
|
|
35469
|
-
return await NotificationLive.getData();
|
|
36129
|
+
this.handlePartialLoss = async (data) => {
|
|
36130
|
+
return await this._notificationLiveUtils.handlePartialLoss(data);
|
|
35470
36131
|
};
|
|
35471
36132
|
/**
|
|
35472
|
-
*
|
|
35473
|
-
*
|
|
35474
|
-
* @
|
|
36133
|
+
* Handles breakeven availability event.
|
|
36134
|
+
* Proxies call to the underlying notification adapter.
|
|
36135
|
+
* @param data - The breakeven contract data
|
|
35475
36136
|
*/
|
|
35476
|
-
this.
|
|
35477
|
-
|
|
35478
|
-
backtest: isBacktest,
|
|
35479
|
-
});
|
|
35480
|
-
if (!this.enable.hasValue()) {
|
|
35481
|
-
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
35482
|
-
}
|
|
35483
|
-
if (isBacktest) {
|
|
35484
|
-
return await NotificationBacktest.clear();
|
|
35485
|
-
}
|
|
35486
|
-
return await NotificationLive.clear();
|
|
35487
|
-
};
|
|
35488
|
-
}
|
|
35489
|
-
}
|
|
35490
|
-
/**
|
|
35491
|
-
* Global singleton instance of NotificationAdapter.
|
|
35492
|
-
* Provides unified notification management for backtest and live trading.
|
|
35493
|
-
*/
|
|
35494
|
-
const Notification = new NotificationAdapter();
|
|
35495
|
-
/**
|
|
35496
|
-
* Global singleton instance of NotificationLiveAdapter.
|
|
35497
|
-
* Provides live trading notification storage with pluggable backends.
|
|
35498
|
-
*/
|
|
35499
|
-
const NotificationLive = new NotificationLiveAdapter();
|
|
35500
|
-
/**
|
|
35501
|
-
* Global singleton instance of NotificationBacktestAdapter.
|
|
35502
|
-
* Provides backtest notification storage with pluggable backends.
|
|
35503
|
-
*/
|
|
35504
|
-
const NotificationBacktest = new NotificationBacktestAdapter();
|
|
35505
|
-
|
|
35506
|
-
const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
|
|
35507
|
-
const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
|
|
35508
|
-
const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
|
|
35509
|
-
const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
35510
|
-
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
35511
|
-
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
35512
|
-
const MS_PER_MINUTE$1 = 60000;
|
|
35513
|
-
/**
|
|
35514
|
-
* Gets current timestamp from execution context if available.
|
|
35515
|
-
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
35516
|
-
*/
|
|
35517
|
-
const GET_TIMESTAMP_FN = async () => {
|
|
35518
|
-
if (ExecutionContextService.hasContext()) {
|
|
35519
|
-
return new Date(bt.executionContextService.context.when);
|
|
35520
|
-
}
|
|
35521
|
-
return new Date();
|
|
35522
|
-
};
|
|
35523
|
-
/**
|
|
35524
|
-
* Gets backtest mode flag from execution context if available.
|
|
35525
|
-
* Returns false if no execution context exists (live mode).
|
|
35526
|
-
*/
|
|
35527
|
-
const GET_BACKTEST_FN = async () => {
|
|
35528
|
-
if (ExecutionContextService.hasContext()) {
|
|
35529
|
-
return bt.executionContextService.context.backtest;
|
|
35530
|
-
}
|
|
35531
|
-
return false;
|
|
35532
|
-
};
|
|
35533
|
-
/**
|
|
35534
|
-
* Default implementation for getCandles.
|
|
35535
|
-
* Throws an error indicating the method is not implemented.
|
|
35536
|
-
*/
|
|
35537
|
-
const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
|
|
35538
|
-
throw new Error(`getCandles is not implemented for this exchange`);
|
|
35539
|
-
};
|
|
35540
|
-
/**
|
|
35541
|
-
* Default implementation for formatQuantity.
|
|
35542
|
-
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
35543
|
-
*/
|
|
35544
|
-
const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
|
|
35545
|
-
return quantity.toFixed(8);
|
|
35546
|
-
};
|
|
35547
|
-
/**
|
|
35548
|
-
* Default implementation for formatPrice.
|
|
35549
|
-
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
35550
|
-
*/
|
|
35551
|
-
const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
|
|
35552
|
-
return price.toFixed(2);
|
|
35553
|
-
};
|
|
35554
|
-
/**
|
|
35555
|
-
* Default implementation for getOrderBook.
|
|
35556
|
-
* Throws an error indicating the method is not implemented.
|
|
35557
|
-
*
|
|
35558
|
-
* @param _symbol - Trading pair symbol (unused)
|
|
35559
|
-
* @param _depth - Maximum depth levels (unused)
|
|
35560
|
-
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
35561
|
-
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
35562
|
-
* @param _backtest - Whether running in backtest mode (unused)
|
|
35563
|
-
*/
|
|
35564
|
-
const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
|
|
35565
|
-
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
35566
|
-
};
|
|
35567
|
-
const INTERVAL_MINUTES$1 = {
|
|
35568
|
-
"1m": 1,
|
|
35569
|
-
"3m": 3,
|
|
35570
|
-
"5m": 5,
|
|
35571
|
-
"15m": 15,
|
|
35572
|
-
"30m": 30,
|
|
35573
|
-
"1h": 60,
|
|
35574
|
-
"2h": 120,
|
|
35575
|
-
"4h": 240,
|
|
35576
|
-
"6h": 360,
|
|
35577
|
-
"8h": 480,
|
|
35578
|
-
};
|
|
35579
|
-
/**
|
|
35580
|
-
* Aligns timestamp down to the nearest interval boundary.
|
|
35581
|
-
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
35582
|
-
*
|
|
35583
|
-
* Candle timestamp convention:
|
|
35584
|
-
* - Candle timestamp = openTime (when candle opens)
|
|
35585
|
-
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
35586
|
-
*
|
|
35587
|
-
* Adapter contract:
|
|
35588
|
-
* - Adapter must return candles with timestamp = openTime
|
|
35589
|
-
* - First returned candle.timestamp must equal aligned since
|
|
35590
|
-
* - Adapter must return exactly `limit` candles
|
|
35591
|
-
*
|
|
35592
|
-
* @param timestamp - Timestamp in milliseconds
|
|
35593
|
-
* @param intervalMinutes - Interval in minutes
|
|
35594
|
-
* @returns Aligned timestamp rounded down to interval boundary
|
|
35595
|
-
*/
|
|
35596
|
-
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
35597
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
35598
|
-
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
35599
|
-
};
|
|
35600
|
-
/**
|
|
35601
|
-
* Creates exchange instance with methods resolved once during construction.
|
|
35602
|
-
* Applies default implementations where schema methods are not provided.
|
|
35603
|
-
*
|
|
35604
|
-
* @param schema - Exchange schema from registry
|
|
35605
|
-
* @returns Object with resolved exchange methods
|
|
35606
|
-
*/
|
|
35607
|
-
const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
35608
|
-
const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
|
|
35609
|
-
const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
|
|
35610
|
-
const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
|
|
35611
|
-
const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
|
|
35612
|
-
return {
|
|
35613
|
-
getCandles,
|
|
35614
|
-
formatQuantity,
|
|
35615
|
-
formatPrice,
|
|
35616
|
-
getOrderBook,
|
|
35617
|
-
};
|
|
35618
|
-
};
|
|
35619
|
-
/**
|
|
35620
|
-
* Attempts to read candles from cache.
|
|
35621
|
-
*
|
|
35622
|
-
* Cache lookup calculates expected timestamps:
|
|
35623
|
-
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
35624
|
-
* Returns all candles if found, null if any missing.
|
|
35625
|
-
*
|
|
35626
|
-
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
35627
|
-
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
35628
|
-
* @param untilTimestamp - Unused, kept for API compatibility
|
|
35629
|
-
* @param exchangeName - Exchange name
|
|
35630
|
-
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
35631
|
-
*/
|
|
35632
|
-
const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
35633
|
-
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
35634
|
-
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
35635
|
-
// Returns all candles if found, null if any missing
|
|
35636
|
-
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
35637
|
-
// Return cached data only if we have exactly the requested limit
|
|
35638
|
-
if (cachedCandles?.length === dto.limit) {
|
|
35639
|
-
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
35640
|
-
return cachedCandles;
|
|
35641
|
-
}
|
|
35642
|
-
bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
35643
|
-
return null;
|
|
35644
|
-
}, {
|
|
35645
|
-
fallback: async (error) => {
|
|
35646
|
-
const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
|
|
35647
|
-
const payload = {
|
|
35648
|
-
error: errorData(error),
|
|
35649
|
-
message: getErrorMessage(error),
|
|
35650
|
-
};
|
|
35651
|
-
bt.loggerService.warn(message, payload);
|
|
35652
|
-
console.warn(message, payload);
|
|
35653
|
-
errorEmitter.next(error);
|
|
35654
|
-
},
|
|
35655
|
-
defaultValue: null,
|
|
35656
|
-
});
|
|
35657
|
-
/**
|
|
35658
|
-
* Writes candles to cache with error handling.
|
|
35659
|
-
*
|
|
35660
|
-
* The candles passed to this function should be validated:
|
|
35661
|
-
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
35662
|
-
* - Exact number of candles as requested (limit)
|
|
35663
|
-
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
35664
|
-
*
|
|
35665
|
-
* @param candles - Array of validated candle data to cache
|
|
35666
|
-
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
35667
|
-
* @param exchangeName - Exchange name
|
|
35668
|
-
*/
|
|
35669
|
-
const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
|
|
35670
|
-
await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
|
|
35671
|
-
bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
|
|
35672
|
-
}), {
|
|
35673
|
-
fallback: async (error) => {
|
|
35674
|
-
const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
|
|
35675
|
-
const payload = {
|
|
35676
|
-
error: errorData(error),
|
|
35677
|
-
message: getErrorMessage(error),
|
|
36137
|
+
this.handleBreakeven = async (data) => {
|
|
36138
|
+
return await this._notificationLiveUtils.handleBreakeven(data);
|
|
35678
36139
|
};
|
|
35679
|
-
bt.loggerService.warn(message, payload);
|
|
35680
|
-
console.warn(message, payload);
|
|
35681
|
-
errorEmitter.next(error);
|
|
35682
|
-
},
|
|
35683
|
-
defaultValue: null,
|
|
35684
|
-
});
|
|
35685
|
-
/**
|
|
35686
|
-
* Instance class for exchange operations on a specific exchange.
|
|
35687
|
-
*
|
|
35688
|
-
* Provides isolated exchange operations for a single exchange.
|
|
35689
|
-
* Each instance maintains its own context and exposes IExchangeSchema methods.
|
|
35690
|
-
* The schema is retrieved once during construction for better performance.
|
|
35691
|
-
*
|
|
35692
|
-
* @example
|
|
35693
|
-
* ```typescript
|
|
35694
|
-
* const instance = new ExchangeInstance("binance");
|
|
35695
|
-
*
|
|
35696
|
-
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
35697
|
-
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
35698
|
-
* const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
35699
|
-
* const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
35700
|
-
* ```
|
|
35701
|
-
*/
|
|
35702
|
-
class ExchangeInstance {
|
|
35703
|
-
/**
|
|
35704
|
-
* Creates a new ExchangeInstance for a specific exchange.
|
|
35705
|
-
*
|
|
35706
|
-
* @param exchangeName - Exchange name (e.g., "binance")
|
|
35707
|
-
*/
|
|
35708
|
-
constructor(exchangeName) {
|
|
35709
|
-
this.exchangeName = exchangeName;
|
|
35710
36140
|
/**
|
|
35711
|
-
*
|
|
35712
|
-
*
|
|
35713
|
-
*
|
|
35714
|
-
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
35715
|
-
*
|
|
35716
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
35717
|
-
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
35718
|
-
* @param limit - Maximum number of candles to fetch
|
|
35719
|
-
* @returns Promise resolving to array of OHLCV candle data
|
|
35720
|
-
*
|
|
35721
|
-
* @example
|
|
35722
|
-
* ```typescript
|
|
35723
|
-
* const instance = new ExchangeInstance("binance");
|
|
35724
|
-
* const candles = await instance.getCandles("BTCUSDT", "1m", 100);
|
|
35725
|
-
* ```
|
|
36141
|
+
* Handles strategy commit events.
|
|
36142
|
+
* Proxies call to the underlying notification adapter.
|
|
36143
|
+
* @param data - The strategy commit contract data
|
|
35726
36144
|
*/
|
|
35727
|
-
this.
|
|
35728
|
-
|
|
35729
|
-
exchangeName: this.exchangeName,
|
|
35730
|
-
symbol,
|
|
35731
|
-
interval,
|
|
35732
|
-
limit,
|
|
35733
|
-
});
|
|
35734
|
-
const getCandles = this._methods.getCandles;
|
|
35735
|
-
const step = INTERVAL_MINUTES$1[interval];
|
|
35736
|
-
if (!step) {
|
|
35737
|
-
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
35738
|
-
}
|
|
35739
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
35740
|
-
// Align when down to interval boundary
|
|
35741
|
-
const when = await GET_TIMESTAMP_FN();
|
|
35742
|
-
const whenTimestamp = when.getTime();
|
|
35743
|
-
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
35744
|
-
// Calculate since: go back limit candles from aligned when
|
|
35745
|
-
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
35746
|
-
const since = new Date(sinceTimestamp);
|
|
35747
|
-
const untilTimestamp = alignedWhen;
|
|
35748
|
-
// Try to read from cache first
|
|
35749
|
-
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
35750
|
-
if (cachedCandles !== null) {
|
|
35751
|
-
return cachedCandles;
|
|
35752
|
-
}
|
|
35753
|
-
let allData = [];
|
|
35754
|
-
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
35755
|
-
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
35756
|
-
let remaining = limit;
|
|
35757
|
-
let currentSince = new Date(since.getTime());
|
|
35758
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35759
|
-
while (remaining > 0) {
|
|
35760
|
-
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
35761
|
-
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
35762
|
-
allData.push(...chunkData);
|
|
35763
|
-
remaining -= chunkLimit;
|
|
35764
|
-
if (remaining > 0) {
|
|
35765
|
-
// Move currentSince forward by the number of candles fetched
|
|
35766
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
35767
|
-
}
|
|
35768
|
-
}
|
|
35769
|
-
}
|
|
35770
|
-
else {
|
|
35771
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35772
|
-
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
35773
|
-
}
|
|
35774
|
-
// Apply distinct by timestamp to remove duplicates
|
|
35775
|
-
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
35776
|
-
if (allData.length !== uniqueData.length) {
|
|
35777
|
-
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
35778
|
-
}
|
|
35779
|
-
// Validate adapter returned data
|
|
35780
|
-
if (uniqueData.length === 0) {
|
|
35781
|
-
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
35782
|
-
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
35783
|
-
}
|
|
35784
|
-
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
35785
|
-
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
35786
|
-
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
35787
|
-
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
35788
|
-
}
|
|
35789
|
-
if (uniqueData.length !== limit) {
|
|
35790
|
-
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
35791
|
-
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
35792
|
-
`Adapter must return exact number of candles requested.`);
|
|
35793
|
-
}
|
|
35794
|
-
// Write to cache after successful fetch
|
|
35795
|
-
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
35796
|
-
return uniqueData;
|
|
36145
|
+
this.handleStrategyCommit = async (data) => {
|
|
36146
|
+
return await this._notificationLiveUtils.handleStrategyCommit(data);
|
|
35797
36147
|
};
|
|
35798
36148
|
/**
|
|
35799
|
-
*
|
|
35800
|
-
*
|
|
35801
|
-
*
|
|
35802
|
-
|
|
35803
|
-
|
|
35804
|
-
|
|
35805
|
-
*
|
|
35806
|
-
* If volume is zero, returns simple average of close prices.
|
|
35807
|
-
*
|
|
35808
|
-
* @param symbol - Trading pair symbol
|
|
35809
|
-
* @returns Promise resolving to VWAP price
|
|
35810
|
-
* @throws Error if no candles available
|
|
35811
|
-
*
|
|
35812
|
-
* @example
|
|
35813
|
-
* ```typescript
|
|
35814
|
-
* const instance = new ExchangeInstance("binance");
|
|
35815
|
-
* const vwap = await instance.getAveragePrice("BTCUSDT");
|
|
35816
|
-
* console.log(vwap); // 50125.43
|
|
35817
|
-
* ```
|
|
35818
|
-
*/
|
|
35819
|
-
this.getAveragePrice = async (symbol) => {
|
|
35820
|
-
bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
|
|
35821
|
-
exchangeName: this.exchangeName,
|
|
35822
|
-
symbol,
|
|
35823
|
-
});
|
|
35824
|
-
const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
|
|
35825
|
-
if (candles.length === 0) {
|
|
35826
|
-
throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
|
|
35827
|
-
}
|
|
35828
|
-
// VWAP (Volume Weighted Average Price)
|
|
35829
|
-
// Используем типичную цену (typical price) = (high + low + close) / 3
|
|
35830
|
-
const sumPriceVolume = candles.reduce((acc, candle) => {
|
|
35831
|
-
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
|
|
35832
|
-
return acc + typicalPrice * candle.volume;
|
|
35833
|
-
}, 0);
|
|
35834
|
-
const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
|
|
35835
|
-
if (totalVolume === 0) {
|
|
35836
|
-
// Если объем нулевой, возвращаем простое среднее close цен
|
|
35837
|
-
const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
|
|
35838
|
-
return sum / candles.length;
|
|
35839
|
-
}
|
|
35840
|
-
const vwap = sumPriceVolume / totalVolume;
|
|
35841
|
-
return vwap;
|
|
36149
|
+
* Handles risk rejection event.
|
|
36150
|
+
* Proxies call to the underlying notification adapter.
|
|
36151
|
+
* @param data - The risk contract data
|
|
36152
|
+
*/
|
|
36153
|
+
this.handleRisk = async (data) => {
|
|
36154
|
+
return await this._notificationLiveUtils.handleRisk(data);
|
|
35842
36155
|
};
|
|
35843
36156
|
/**
|
|
35844
|
-
*
|
|
35845
|
-
*
|
|
35846
|
-
* @param
|
|
35847
|
-
* @param quantity - Raw quantity value
|
|
35848
|
-
* @returns Promise resolving to formatted quantity string
|
|
35849
|
-
*
|
|
35850
|
-
* @example
|
|
35851
|
-
* ```typescript
|
|
35852
|
-
* const instance = new ExchangeInstance("binance");
|
|
35853
|
-
* const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
35854
|
-
* console.log(formatted); // "0.00100000"
|
|
35855
|
-
* ```
|
|
36157
|
+
* Handles error event.
|
|
36158
|
+
* Proxies call to the underlying notification adapter.
|
|
36159
|
+
* @param error - The error object
|
|
35856
36160
|
*/
|
|
35857
|
-
this.
|
|
35858
|
-
|
|
35859
|
-
exchangeName: this.exchangeName,
|
|
35860
|
-
symbol,
|
|
35861
|
-
quantity,
|
|
35862
|
-
});
|
|
35863
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35864
|
-
return await this._methods.formatQuantity(symbol, quantity, isBacktest);
|
|
36161
|
+
this.handleError = async (error) => {
|
|
36162
|
+
return await this._notificationLiveUtils.handleError(error);
|
|
35865
36163
|
};
|
|
35866
36164
|
/**
|
|
35867
|
-
*
|
|
35868
|
-
*
|
|
35869
|
-
* @param
|
|
35870
|
-
* @param price - Raw price value
|
|
35871
|
-
* @returns Promise resolving to formatted price string
|
|
35872
|
-
*
|
|
35873
|
-
* @example
|
|
35874
|
-
* ```typescript
|
|
35875
|
-
* const instance = new ExchangeInstance("binance");
|
|
35876
|
-
* const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
|
|
35877
|
-
* console.log(formatted); // "50000.12"
|
|
35878
|
-
* ```
|
|
36165
|
+
* Handles critical error event.
|
|
36166
|
+
* Proxies call to the underlying notification adapter.
|
|
36167
|
+
* @param error - The error object
|
|
35879
36168
|
*/
|
|
35880
|
-
this.
|
|
35881
|
-
|
|
35882
|
-
exchangeName: this.exchangeName,
|
|
35883
|
-
symbol,
|
|
35884
|
-
price,
|
|
35885
|
-
});
|
|
35886
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35887
|
-
return await this._methods.formatPrice(symbol, price, isBacktest);
|
|
36169
|
+
this.handleCriticalError = async (error) => {
|
|
36170
|
+
return await this._notificationLiveUtils.handleCriticalError(error);
|
|
35888
36171
|
};
|
|
35889
36172
|
/**
|
|
35890
|
-
*
|
|
35891
|
-
*
|
|
35892
|
-
*
|
|
35893
|
-
* and passes it to the exchange schema implementation. The implementation may use
|
|
35894
|
-
* the time range (backtest) or ignore it (live trading).
|
|
35895
|
-
*
|
|
35896
|
-
* @param symbol - Trading pair symbol
|
|
35897
|
-
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
35898
|
-
* @returns Promise resolving to order book data
|
|
35899
|
-
* @throws Error if getOrderBook is not implemented
|
|
35900
|
-
*
|
|
35901
|
-
* @example
|
|
35902
|
-
* ```typescript
|
|
35903
|
-
* const instance = new ExchangeInstance("binance");
|
|
35904
|
-
* const orderBook = await instance.getOrderBook("BTCUSDT");
|
|
35905
|
-
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
35906
|
-
* const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
|
|
35907
|
-
* ```
|
|
36173
|
+
* Handles validation error event.
|
|
36174
|
+
* Proxies call to the underlying notification adapter.
|
|
36175
|
+
* @param error - The error object
|
|
35908
36176
|
*/
|
|
35909
|
-
this.
|
|
35910
|
-
|
|
35911
|
-
exchangeName: this.exchangeName,
|
|
35912
|
-
symbol,
|
|
35913
|
-
depth,
|
|
35914
|
-
});
|
|
35915
|
-
const when = await GET_TIMESTAMP_FN();
|
|
35916
|
-
const alignedTo = ALIGN_TO_INTERVAL_FN(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
35917
|
-
const to = new Date(alignedTo);
|
|
35918
|
-
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
|
|
35919
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
35920
|
-
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
36177
|
+
this.handleValidationError = async (error) => {
|
|
36178
|
+
return await this._notificationLiveUtils.handleValidationError(error);
|
|
35921
36179
|
};
|
|
35922
36180
|
/**
|
|
35923
|
-
*
|
|
35924
|
-
*
|
|
35925
|
-
*
|
|
35926
|
-
*
|
|
35927
|
-
* Parameter combinations:
|
|
35928
|
-
* 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
|
|
35929
|
-
* 2. sDate + eDate: calculates limit from date range, validates eDate <= now
|
|
35930
|
-
* 3. eDate + limit: calculates sDate backward, validates eDate <= now
|
|
35931
|
-
* 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
|
|
35932
|
-
* 5. Only limit: uses Date.now() as reference (backward)
|
|
35933
|
-
*
|
|
35934
|
-
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
35935
|
-
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
35936
|
-
* @param limit - Optional number of candles to fetch
|
|
35937
|
-
* @param sDate - Optional start date in milliseconds
|
|
35938
|
-
* @param eDate - Optional end date in milliseconds
|
|
35939
|
-
* @returns Promise resolving to array of candle data
|
|
35940
|
-
*
|
|
35941
|
-
* @example
|
|
35942
|
-
* ```typescript
|
|
35943
|
-
* const instance = new ExchangeInstance("binance");
|
|
35944
|
-
*
|
|
35945
|
-
* // Fetch 100 candles backward from now
|
|
35946
|
-
* const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
|
|
35947
|
-
*
|
|
35948
|
-
* // Fetch candles for specific date range
|
|
35949
|
-
* const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
|
|
35950
|
-
* ```
|
|
36181
|
+
* Gets all stored notifications.
|
|
36182
|
+
* Proxies call to the underlying notification adapter.
|
|
36183
|
+
* @returns Array of all notification models
|
|
35951
36184
|
*/
|
|
35952
|
-
this.
|
|
35953
|
-
|
|
35954
|
-
exchangeName: this.exchangeName,
|
|
35955
|
-
symbol,
|
|
35956
|
-
interval,
|
|
35957
|
-
limit,
|
|
35958
|
-
sDate,
|
|
35959
|
-
eDate,
|
|
35960
|
-
});
|
|
35961
|
-
const step = INTERVAL_MINUTES$1[interval];
|
|
35962
|
-
if (!step) {
|
|
35963
|
-
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
35964
|
-
}
|
|
35965
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
35966
|
-
const when = await GET_TIMESTAMP_FN();
|
|
35967
|
-
const nowTimestamp = when.getTime();
|
|
35968
|
-
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
35969
|
-
let sinceTimestamp;
|
|
35970
|
-
let calculatedLimit;
|
|
35971
|
-
// Case 1: all three parameters provided
|
|
35972
|
-
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
35973
|
-
if (sDate >= eDate) {
|
|
35974
|
-
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
35975
|
-
}
|
|
35976
|
-
if (eDate > nowTimestamp) {
|
|
35977
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
35978
|
-
}
|
|
35979
|
-
// Align sDate down to interval boundary
|
|
35980
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
35981
|
-
calculatedLimit = limit;
|
|
35982
|
-
}
|
|
35983
|
-
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
35984
|
-
else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
|
|
35985
|
-
if (sDate >= eDate) {
|
|
35986
|
-
throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
|
|
35987
|
-
}
|
|
35988
|
-
if (eDate > nowTimestamp) {
|
|
35989
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
35990
|
-
}
|
|
35991
|
-
// Align sDate down to interval boundary
|
|
35992
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
35993
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
35994
|
-
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
35995
|
-
if (calculatedLimit <= 0) {
|
|
35996
|
-
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
35997
|
-
}
|
|
35998
|
-
}
|
|
35999
|
-
// Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
|
|
36000
|
-
else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
|
|
36001
|
-
if (eDate > nowTimestamp) {
|
|
36002
|
-
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
36003
|
-
}
|
|
36004
|
-
// Align eDate down and calculate sinceTimestamp
|
|
36005
|
-
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
36006
|
-
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
36007
|
-
calculatedLimit = limit;
|
|
36008
|
-
}
|
|
36009
|
-
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
36010
|
-
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
36011
|
-
// Align sDate down to interval boundary
|
|
36012
|
-
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
36013
|
-
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
36014
|
-
if (endTimestamp > nowTimestamp) {
|
|
36015
|
-
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
36016
|
-
}
|
|
36017
|
-
calculatedLimit = limit;
|
|
36018
|
-
}
|
|
36019
|
-
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
36020
|
-
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
36021
|
-
sinceTimestamp = alignedNow - limit * stepMs;
|
|
36022
|
-
calculatedLimit = limit;
|
|
36023
|
-
}
|
|
36024
|
-
// Invalid: no parameters or only sDate or only eDate
|
|
36025
|
-
else {
|
|
36026
|
-
throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
|
|
36027
|
-
`Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
|
|
36028
|
-
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
36029
|
-
}
|
|
36030
|
-
// Try to read from cache first
|
|
36031
|
-
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
36032
|
-
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
36033
|
-
if (cachedCandles !== null) {
|
|
36034
|
-
return cachedCandles;
|
|
36035
|
-
}
|
|
36036
|
-
// Fetch candles
|
|
36037
|
-
const since = new Date(sinceTimestamp);
|
|
36038
|
-
let allData = [];
|
|
36039
|
-
const isBacktest = await GET_BACKTEST_FN();
|
|
36040
|
-
const getCandles = this._methods.getCandles;
|
|
36041
|
-
if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
36042
|
-
let remaining = calculatedLimit;
|
|
36043
|
-
let currentSince = new Date(since.getTime());
|
|
36044
|
-
while (remaining > 0) {
|
|
36045
|
-
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
36046
|
-
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
|
|
36047
|
-
allData.push(...chunkData);
|
|
36048
|
-
remaining -= chunkLimit;
|
|
36049
|
-
if (remaining > 0) {
|
|
36050
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
36051
|
-
}
|
|
36052
|
-
}
|
|
36053
|
-
}
|
|
36054
|
-
else {
|
|
36055
|
-
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
36056
|
-
}
|
|
36057
|
-
// Apply distinct by timestamp to remove duplicates
|
|
36058
|
-
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
36059
|
-
if (allData.length !== uniqueData.length) {
|
|
36060
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
36061
|
-
}
|
|
36062
|
-
// Validate adapter returned data
|
|
36063
|
-
if (uniqueData.length === 0) {
|
|
36064
|
-
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
36065
|
-
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
36066
|
-
}
|
|
36067
|
-
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
36068
|
-
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
36069
|
-
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
36070
|
-
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
36071
|
-
}
|
|
36072
|
-
if (uniqueData.length !== calculatedLimit) {
|
|
36073
|
-
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
36074
|
-
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
36075
|
-
`Adapter must return exact number of candles requested.`);
|
|
36076
|
-
}
|
|
36077
|
-
// Write to cache after successful fetch
|
|
36078
|
-
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
36079
|
-
return uniqueData;
|
|
36185
|
+
this.getData = async () => {
|
|
36186
|
+
return await this._notificationLiveUtils.getData();
|
|
36080
36187
|
};
|
|
36081
|
-
const schema = bt.exchangeSchemaService.get(this.exchangeName);
|
|
36082
|
-
this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
|
|
36083
|
-
}
|
|
36084
|
-
}
|
|
36085
|
-
/**
|
|
36086
|
-
* Utility class for exchange operations.
|
|
36087
|
-
*
|
|
36088
|
-
* Provides simplified access to exchange schema methods with validation.
|
|
36089
|
-
* Exported as singleton instance for convenient usage.
|
|
36090
|
-
*
|
|
36091
|
-
* @example
|
|
36092
|
-
* ```typescript
|
|
36093
|
-
* import { Exchange } from "./classes/Exchange";
|
|
36094
|
-
*
|
|
36095
|
-
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
36096
|
-
* exchangeName: "binance"
|
|
36097
|
-
* });
|
|
36098
|
-
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
36099
|
-
* exchangeName: "binance"
|
|
36100
|
-
* });
|
|
36101
|
-
* const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
36102
|
-
* exchangeName: "binance"
|
|
36103
|
-
* });
|
|
36104
|
-
* ```
|
|
36105
|
-
*/
|
|
36106
|
-
class ExchangeUtils {
|
|
36107
|
-
constructor() {
|
|
36108
36188
|
/**
|
|
36109
|
-
*
|
|
36110
|
-
*
|
|
36189
|
+
* Clears all stored notifications.
|
|
36190
|
+
* Proxies call to the underlying notification adapter.
|
|
36111
36191
|
*/
|
|
36112
|
-
this.
|
|
36192
|
+
this.clear = async () => {
|
|
36193
|
+
return await this._notificationLiveUtils.clear();
|
|
36194
|
+
};
|
|
36113
36195
|
/**
|
|
36114
|
-
*
|
|
36115
|
-
*
|
|
36116
|
-
* Automatically calculates the start date based on Date.now() and the requested interval/limit.
|
|
36117
|
-
* Uses the same logic as ClientExchange to ensure backwards compatibility.
|
|
36196
|
+
* Sets the notification adapter constructor.
|
|
36197
|
+
* All future notification operations will use this adapter.
|
|
36118
36198
|
*
|
|
36119
|
-
* @param
|
|
36120
|
-
* @param interval - Candle time interval (e.g., "1m", "1h")
|
|
36121
|
-
* @param limit - Maximum number of candles to fetch
|
|
36122
|
-
* @param context - Execution context with exchange name
|
|
36123
|
-
* @returns Promise resolving to array of OHLCV candle data
|
|
36199
|
+
* @param Ctor - Constructor for notification adapter
|
|
36124
36200
|
*/
|
|
36125
|
-
this.
|
|
36126
|
-
bt.
|
|
36127
|
-
|
|
36128
|
-
return await instance.getCandles(symbol, interval, limit);
|
|
36201
|
+
this.useNotificationAdapter = (Ctor) => {
|
|
36202
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER);
|
|
36203
|
+
this._notificationLiveUtils = Reflect.construct(Ctor, []);
|
|
36129
36204
|
};
|
|
36130
36205
|
/**
|
|
36131
|
-
*
|
|
36132
|
-
*
|
|
36133
|
-
* @param symbol - Trading pair symbol
|
|
36134
|
-
* @param context - Execution context with exchange name
|
|
36135
|
-
* @returns Promise resolving to VWAP price
|
|
36206
|
+
* Switches to dummy notification adapter.
|
|
36207
|
+
* All future notification writes will be no-ops.
|
|
36136
36208
|
*/
|
|
36137
|
-
this.
|
|
36138
|
-
bt.
|
|
36139
|
-
|
|
36140
|
-
return await instance.getAveragePrice(symbol);
|
|
36209
|
+
this.useDummy = () => {
|
|
36210
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY);
|
|
36211
|
+
this._notificationLiveUtils = new NotificationDummyLiveUtils();
|
|
36141
36212
|
};
|
|
36142
36213
|
/**
|
|
36143
|
-
*
|
|
36144
|
-
*
|
|
36145
|
-
* @param symbol - Trading pair symbol
|
|
36146
|
-
* @param quantity - Raw quantity value
|
|
36147
|
-
* @param context - Execution context with exchange name
|
|
36148
|
-
* @returns Promise resolving to formatted quantity string
|
|
36214
|
+
* Switches to in-memory notification adapter (default).
|
|
36215
|
+
* Notifications will be stored in memory only.
|
|
36149
36216
|
*/
|
|
36150
|
-
this.
|
|
36151
|
-
bt.
|
|
36152
|
-
|
|
36153
|
-
return await instance.formatQuantity(symbol, quantity);
|
|
36217
|
+
this.useMemory = () => {
|
|
36218
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY);
|
|
36219
|
+
this._notificationLiveUtils = new NotificationMemoryLiveUtils();
|
|
36154
36220
|
};
|
|
36155
36221
|
/**
|
|
36156
|
-
*
|
|
36157
|
-
*
|
|
36158
|
-
* @param symbol - Trading pair symbol
|
|
36159
|
-
* @param price - Raw price value
|
|
36160
|
-
* @param context - Execution context with exchange name
|
|
36161
|
-
* @returns Promise resolving to formatted price string
|
|
36222
|
+
* Switches to persistent notification adapter.
|
|
36223
|
+
* Notifications will be persisted to disk.
|
|
36162
36224
|
*/
|
|
36163
|
-
this.
|
|
36164
|
-
bt.
|
|
36165
|
-
|
|
36166
|
-
return await instance.formatPrice(symbol, price);
|
|
36225
|
+
this.usePersist = () => {
|
|
36226
|
+
bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST);
|
|
36227
|
+
this._notificationLiveUtils = new NotificationPersistLiveUtils();
|
|
36167
36228
|
};
|
|
36229
|
+
}
|
|
36230
|
+
}
|
|
36231
|
+
/**
|
|
36232
|
+
* Main notification adapter that manages both backtest and live notification storage.
|
|
36233
|
+
*
|
|
36234
|
+
* Features:
|
|
36235
|
+
* - Subscribes to signal emitters for automatic notification updates
|
|
36236
|
+
* - Provides unified access to both backtest and live notifications
|
|
36237
|
+
* - Singleshot enable pattern prevents duplicate subscriptions
|
|
36238
|
+
* - Cleanup function for proper unsubscription
|
|
36239
|
+
*/
|
|
36240
|
+
class NotificationAdapter {
|
|
36241
|
+
constructor() {
|
|
36168
36242
|
/**
|
|
36169
|
-
*
|
|
36170
|
-
*
|
|
36171
|
-
* Delegates to ExchangeInstance which calculates time range and passes it
|
|
36172
|
-
* to the exchange schema implementation. The from/to parameters may be used
|
|
36173
|
-
* (backtest) or ignored (live) depending on the implementation.
|
|
36243
|
+
* Enables notification storage by subscribing to signal emitters.
|
|
36244
|
+
* Uses singleshot to ensure one-time subscription.
|
|
36174
36245
|
*
|
|
36175
|
-
* @
|
|
36176
|
-
* @param context - Execution context with exchange name
|
|
36177
|
-
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
36178
|
-
* @returns Promise resolving to order book data
|
|
36246
|
+
* @returns Cleanup function that unsubscribes from all emitters
|
|
36179
36247
|
*/
|
|
36180
|
-
this.
|
|
36181
|
-
bt.
|
|
36182
|
-
|
|
36183
|
-
|
|
36248
|
+
this.enable = singleshot(() => {
|
|
36249
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
|
|
36250
|
+
let unLive;
|
|
36251
|
+
let unBacktest;
|
|
36252
|
+
{
|
|
36253
|
+
const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
|
|
36254
|
+
const unBacktestPartialProfit = partialProfitSubject
|
|
36255
|
+
.filter(({ backtest }) => backtest)
|
|
36256
|
+
.connect((data) => NotificationBacktest.handlePartialProfit(data));
|
|
36257
|
+
const unBacktestPartialLoss = partialLossSubject
|
|
36258
|
+
.filter(({ backtest }) => backtest)
|
|
36259
|
+
.connect((data) => NotificationBacktest.handlePartialLoss(data));
|
|
36260
|
+
const unBacktestBreakeven = breakevenSubject
|
|
36261
|
+
.filter(({ backtest }) => backtest)
|
|
36262
|
+
.connect((data) => NotificationBacktest.handleBreakeven(data));
|
|
36263
|
+
const unBacktestStrategyCommit = strategyCommitSubject
|
|
36264
|
+
.filter(({ backtest }) => backtest)
|
|
36265
|
+
.connect((data) => NotificationBacktest.handleStrategyCommit(data));
|
|
36266
|
+
const unBacktestRisk = riskSubject
|
|
36267
|
+
.filter(({ backtest }) => backtest)
|
|
36268
|
+
.connect((data) => NotificationBacktest.handleRisk(data));
|
|
36269
|
+
const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
|
|
36270
|
+
const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
|
|
36271
|
+
const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
|
|
36272
|
+
unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
|
|
36273
|
+
}
|
|
36274
|
+
{
|
|
36275
|
+
const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
|
|
36276
|
+
const unLivePartialProfit = partialProfitSubject
|
|
36277
|
+
.filter(({ backtest }) => !backtest)
|
|
36278
|
+
.connect((data) => NotificationLive.handlePartialProfit(data));
|
|
36279
|
+
const unLivePartialLoss = partialLossSubject
|
|
36280
|
+
.filter(({ backtest }) => !backtest)
|
|
36281
|
+
.connect((data) => NotificationLive.handlePartialLoss(data));
|
|
36282
|
+
const unLiveBreakeven = breakevenSubject
|
|
36283
|
+
.filter(({ backtest }) => !backtest)
|
|
36284
|
+
.connect((data) => NotificationLive.handleBreakeven(data));
|
|
36285
|
+
const unLiveStrategyCommit = strategyCommitSubject
|
|
36286
|
+
.filter(({ backtest }) => !backtest)
|
|
36287
|
+
.connect((data) => NotificationLive.handleStrategyCommit(data));
|
|
36288
|
+
const unLiveRisk = riskSubject
|
|
36289
|
+
.filter(({ backtest }) => !backtest)
|
|
36290
|
+
.connect((data) => NotificationLive.handleRisk(data));
|
|
36291
|
+
const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
|
|
36292
|
+
const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
|
|
36293
|
+
const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
|
|
36294
|
+
unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
|
|
36295
|
+
}
|
|
36296
|
+
return () => {
|
|
36297
|
+
unLive();
|
|
36298
|
+
unBacktest();
|
|
36299
|
+
this.enable.clear();
|
|
36300
|
+
};
|
|
36301
|
+
});
|
|
36302
|
+
/**
|
|
36303
|
+
* Disables notification storage by unsubscribing from all emitters.
|
|
36304
|
+
* Safe to call multiple times.
|
|
36305
|
+
*/
|
|
36306
|
+
this.disable = () => {
|
|
36307
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE);
|
|
36308
|
+
if (this.enable.hasValue()) {
|
|
36309
|
+
const lastSubscription = this.enable();
|
|
36310
|
+
lastSubscription();
|
|
36311
|
+
}
|
|
36184
36312
|
};
|
|
36185
36313
|
/**
|
|
36186
|
-
*
|
|
36314
|
+
* Gets all backtest/live notifications from storage.
|
|
36187
36315
|
*
|
|
36188
|
-
*
|
|
36316
|
+
* @returns Array of all backtest notification models
|
|
36317
|
+
* @throws Error if NotificationAdapter is not enabled
|
|
36318
|
+
*/
|
|
36319
|
+
this.getData = async (isBacktest) => {
|
|
36320
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST, {
|
|
36321
|
+
backtest: isBacktest,
|
|
36322
|
+
});
|
|
36323
|
+
if (!this.enable.hasValue()) {
|
|
36324
|
+
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
36325
|
+
}
|
|
36326
|
+
if (isBacktest) {
|
|
36327
|
+
return await NotificationBacktest.getData();
|
|
36328
|
+
}
|
|
36329
|
+
return await NotificationLive.getData();
|
|
36330
|
+
};
|
|
36331
|
+
/**
|
|
36332
|
+
* Clears all backtest/live notifications from storage.
|
|
36189
36333
|
*
|
|
36190
|
-
* @
|
|
36191
|
-
* @param interval - Candle interval (e.g., "1m", "1h")
|
|
36192
|
-
* @param context - Execution context with exchange name
|
|
36193
|
-
* @param limit - Optional number of candles to fetch
|
|
36194
|
-
* @param sDate - Optional start date in milliseconds
|
|
36195
|
-
* @param eDate - Optional end date in milliseconds
|
|
36196
|
-
* @returns Promise resolving to array of candle data
|
|
36334
|
+
* @throws Error if NotificationAdapter is not enabled
|
|
36197
36335
|
*/
|
|
36198
|
-
this.
|
|
36199
|
-
bt.
|
|
36200
|
-
|
|
36201
|
-
|
|
36336
|
+
this.clear = async (isBacktest) => {
|
|
36337
|
+
bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE, {
|
|
36338
|
+
backtest: isBacktest,
|
|
36339
|
+
});
|
|
36340
|
+
if (!this.enable.hasValue()) {
|
|
36341
|
+
throw new Error("NotificationAdapter is not enabled. Call enable() first.");
|
|
36342
|
+
}
|
|
36343
|
+
if (isBacktest) {
|
|
36344
|
+
return await NotificationBacktest.clear();
|
|
36345
|
+
}
|
|
36346
|
+
return await NotificationLive.clear();
|
|
36202
36347
|
};
|
|
36203
36348
|
}
|
|
36204
36349
|
}
|
|
36205
36350
|
/**
|
|
36206
|
-
*
|
|
36207
|
-
*
|
|
36208
|
-
* @example
|
|
36209
|
-
* ```typescript
|
|
36210
|
-
* import { Exchange } from "./classes/Exchange";
|
|
36211
|
-
*
|
|
36212
|
-
* // Using static-like API with context
|
|
36213
|
-
* const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
|
|
36214
|
-
* exchangeName: "binance"
|
|
36215
|
-
* });
|
|
36216
|
-
* const vwap = await Exchange.getAveragePrice("BTCUSDT", {
|
|
36217
|
-
* exchangeName: "binance"
|
|
36218
|
-
* });
|
|
36219
|
-
* const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
|
|
36220
|
-
* exchangeName: "binance"
|
|
36221
|
-
* });
|
|
36222
|
-
* const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
|
|
36223
|
-
* exchangeName: "binance"
|
|
36224
|
-
* });
|
|
36225
|
-
*
|
|
36226
|
-
* // Using instance API (no context needed, exchange set in constructor)
|
|
36227
|
-
* const binance = new ExchangeInstance("binance");
|
|
36228
|
-
* const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
|
|
36229
|
-
* const vwap2 = await binance.getAveragePrice("BTCUSDT");
|
|
36230
|
-
* ```
|
|
36351
|
+
* Global singleton instance of NotificationAdapter.
|
|
36352
|
+
* Provides unified notification management for backtest and live trading.
|
|
36231
36353
|
*/
|
|
36232
|
-
const
|
|
36354
|
+
const Notification = new NotificationAdapter();
|
|
36355
|
+
/**
|
|
36356
|
+
* Global singleton instance of NotificationLiveAdapter.
|
|
36357
|
+
* Provides live trading notification storage with pluggable backends.
|
|
36358
|
+
*/
|
|
36359
|
+
const NotificationLive = new NotificationLiveAdapter();
|
|
36360
|
+
/**
|
|
36361
|
+
* Global singleton instance of NotificationBacktestAdapter.
|
|
36362
|
+
* Provides backtest notification storage with pluggable backends.
|
|
36363
|
+
*/
|
|
36364
|
+
const NotificationBacktest = new NotificationBacktestAdapter();
|
|
36233
36365
|
|
|
36234
36366
|
const CACHE_METHOD_NAME_FLUSH = "CacheUtils.flush";
|
|
36235
36367
|
const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
|
|
@@ -37161,4 +37293,4 @@ const set = (object, path, value) => {
|
|
|
37161
37293
|
}
|
|
37162
37294
|
};
|
|
37163
37295
|
|
|
37164
|
-
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
|
|
37296
|
+
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, checkCandles, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, warmCandles };
|