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