backtest-kit 2.3.1 → 2.3.3
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 +85 -54
- package/build/index.cjs +928 -266
- package/build/index.mjs +929 -268
- package/package.json +2 -2
- package/types.d.ts +383 -17
package/build/index.cjs
CHANGED
|
@@ -1578,73 +1578,73 @@ class PersistCandleUtils {
|
|
|
1578
1578
|
]));
|
|
1579
1579
|
/**
|
|
1580
1580
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1581
|
-
* Returns candles only if cache contains
|
|
1581
|
+
* Returns candles only if cache contains ALL requested candles.
|
|
1582
1582
|
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1583
|
+
* Algorithm (matches ClientExchange.ts logic):
|
|
1584
|
+
* 1. Calculate expected timestamps: sinceTimestamp, sinceTimestamp + stepMs, ..., sinceTimestamp + (limit-1) * stepMs
|
|
1585
|
+
* 2. Try to read each expected candle by timestamp key
|
|
1586
|
+
* 3. If ANY candle is missing, return null (cache miss)
|
|
1587
|
+
* 4. If all candles found, return them in order
|
|
1587
1588
|
*
|
|
1588
1589
|
* @param symbol - Trading pair symbol
|
|
1589
1590
|
* @param interval - Candle interval
|
|
1590
1591
|
* @param exchangeName - Exchange identifier
|
|
1591
1592
|
* @param limit - Number of candles requested
|
|
1592
|
-
* @param sinceTimestamp -
|
|
1593
|
-
* @param
|
|
1593
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1594
|
+
* @param _untilTimestamp - Unused, kept for API compatibility
|
|
1594
1595
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1595
1596
|
*/
|
|
1596
|
-
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp,
|
|
1597
|
+
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, _untilTimestamp) => {
|
|
1597
1598
|
bt.loggerService.info("PersistCandleUtils.readCandlesData", {
|
|
1598
1599
|
symbol,
|
|
1599
1600
|
interval,
|
|
1600
1601
|
exchangeName,
|
|
1601
1602
|
limit,
|
|
1602
1603
|
sinceTimestamp,
|
|
1603
|
-
untilTimestamp,
|
|
1604
1604
|
});
|
|
1605
1605
|
const key = `${symbol}:${interval}:${exchangeName}`;
|
|
1606
1606
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1607
1607
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1608
1608
|
await stateStorage.waitForInit(isInitial);
|
|
1609
1609
|
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1610
|
-
//
|
|
1610
|
+
// Calculate expected timestamps and fetch each candle directly
|
|
1611
1611
|
const cachedCandles = [];
|
|
1612
|
-
for
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1612
|
+
for (let i = 0; i < limit; i++) {
|
|
1613
|
+
const expectedTimestamp = sinceTimestamp + i * stepMs;
|
|
1614
|
+
const timestampKey = String(expectedTimestamp);
|
|
1615
|
+
if (await functoolsKit.not(stateStorage.hasValue(timestampKey))) {
|
|
1616
|
+
// Cache miss - candle not found
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
const candle = await stateStorage.readValue(timestampKey);
|
|
1621
|
+
cachedCandles.push(candle);
|
|
1622
|
+
}
|
|
1623
|
+
catch (error) {
|
|
1624
|
+
// Invalid candle in cache - treat as cache miss
|
|
1625
|
+
const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${expectedTimestamp}`;
|
|
1626
|
+
const payload = {
|
|
1627
|
+
error: functoolsKit.errorData(error),
|
|
1628
|
+
message: functoolsKit.getErrorMessage(error),
|
|
1629
|
+
};
|
|
1630
|
+
bt.loggerService.warn(message, payload);
|
|
1631
|
+
console.warn(message, payload);
|
|
1632
|
+
errorEmitter.next(error);
|
|
1633
|
+
return null;
|
|
1633
1634
|
}
|
|
1634
1635
|
}
|
|
1635
|
-
// Sort by timestamp ascending
|
|
1636
|
-
cachedCandles.sort((a, b) => a.timestamp - b.timestamp);
|
|
1637
1636
|
return cachedCandles;
|
|
1638
1637
|
};
|
|
1639
1638
|
/**
|
|
1640
1639
|
* Writes candles to cache with atomic file writes.
|
|
1641
1640
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1642
1641
|
*
|
|
1643
|
-
* The candles passed to this function
|
|
1644
|
-
* - candle.timestamp
|
|
1645
|
-
* -
|
|
1642
|
+
* The candles passed to this function should be validated candles from the adapter:
|
|
1643
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1644
|
+
* - Exact number of candles as requested
|
|
1645
|
+
* - All candles are fully closed (timestamp + stepMs < untilTimestamp)
|
|
1646
1646
|
*
|
|
1647
|
-
* @param candles - Array of candle data to cache (
|
|
1647
|
+
* @param candles - Array of candle data to cache (validated by the caller)
|
|
1648
1648
|
* @param symbol - Trading pair symbol
|
|
1649
1649
|
* @param interval - Candle interval
|
|
1650
1650
|
* @param exchangeName - Exchange identifier
|
|
@@ -1661,8 +1661,25 @@ class PersistCandleUtils {
|
|
|
1661
1661
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1662
1662
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1663
1663
|
await stateStorage.waitForInit(isInitial);
|
|
1664
|
-
//
|
|
1664
|
+
// Calculate step in milliseconds to determine candle close time
|
|
1665
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1666
|
+
const now = Date.now();
|
|
1667
|
+
// Write each candle as a separate file, skipping incomplete candles
|
|
1665
1668
|
for (const candle of candles) {
|
|
1669
|
+
// Skip incomplete candles: candle is complete when closeTime <= now
|
|
1670
|
+
// closeTime = timestamp + stepMs
|
|
1671
|
+
const candleCloseTime = candle.timestamp + stepMs;
|
|
1672
|
+
if (candleCloseTime > now) {
|
|
1673
|
+
bt.loggerService.debug("PersistCandleUtils.writeCandlesData: skipping incomplete candle", {
|
|
1674
|
+
symbol,
|
|
1675
|
+
interval,
|
|
1676
|
+
exchangeName,
|
|
1677
|
+
timestamp: candle.timestamp,
|
|
1678
|
+
closeTime: candleCloseTime,
|
|
1679
|
+
now,
|
|
1680
|
+
});
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1666
1683
|
if (await functoolsKit.not(stateStorage.hasValue(String(candle.timestamp)))) {
|
|
1667
1684
|
await stateStorage.writeValue(String(candle.timestamp), candle);
|
|
1668
1685
|
}
|
|
@@ -1820,6 +1837,27 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1820
1837
|
"6h": 360,
|
|
1821
1838
|
"8h": 480,
|
|
1822
1839
|
};
|
|
1840
|
+
/**
|
|
1841
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
1842
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
1843
|
+
*
|
|
1844
|
+
* Candle timestamp convention:
|
|
1845
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
1846
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
1847
|
+
*
|
|
1848
|
+
* Adapter contract:
|
|
1849
|
+
* - Adapter must return candles with timestamp = openTime
|
|
1850
|
+
* - First returned candle.timestamp must equal aligned since
|
|
1851
|
+
* - Adapter must return exactly `limit` candles
|
|
1852
|
+
*
|
|
1853
|
+
* @param timestamp - Timestamp in milliseconds
|
|
1854
|
+
* @param intervalMinutes - Interval in minutes
|
|
1855
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
1856
|
+
*/
|
|
1857
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
1858
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
1859
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1860
|
+
};
|
|
1823
1861
|
/**
|
|
1824
1862
|
* Validates that all candles have valid OHLCV data without anomalies.
|
|
1825
1863
|
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
@@ -1883,25 +1921,24 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1883
1921
|
};
|
|
1884
1922
|
/**
|
|
1885
1923
|
* Attempts to read candles from cache.
|
|
1886
|
-
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
1887
1924
|
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
* - Only fully closed candles within the exclusive range are returned
|
|
1925
|
+
* Cache lookup calculates expected timestamps:
|
|
1926
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1927
|
+
* Returns all candles if found, null if any missing.
|
|
1892
1928
|
*
|
|
1893
1929
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1894
|
-
* @param sinceTimestamp -
|
|
1895
|
-
* @param untilTimestamp -
|
|
1930
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1931
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
1896
1932
|
* @param self - Instance of ClientExchange
|
|
1897
|
-
* @returns Cached candles array or null if cache miss
|
|
1933
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
1898
1934
|
*/
|
|
1899
1935
|
const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1900
|
-
// PersistCandleAdapter.readCandlesData
|
|
1901
|
-
//
|
|
1936
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
1937
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1938
|
+
// Returns all candles if found, null if any missing
|
|
1902
1939
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1903
1940
|
// Return cached data only if we have exactly the requested limit
|
|
1904
|
-
if (cachedCandles
|
|
1941
|
+
if (cachedCandles?.length === dto.limit) {
|
|
1905
1942
|
self.params.logger.debug(`ClientExchange READ_CANDLES_CACHE_FN: cache hit for symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
1906
1943
|
return cachedCandles;
|
|
1907
1944
|
}
|
|
@@ -1923,11 +1960,12 @@ const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp
|
|
|
1923
1960
|
/**
|
|
1924
1961
|
* Writes candles to cache with error handling.
|
|
1925
1962
|
*
|
|
1926
|
-
* The candles passed to this function
|
|
1927
|
-
* - candle.timestamp
|
|
1928
|
-
* -
|
|
1963
|
+
* The candles passed to this function should be validated:
|
|
1964
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1965
|
+
* - Exact number of candles as requested (limit)
|
|
1966
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
1929
1967
|
*
|
|
1930
|
-
* @param candles - Array of candle data to cache
|
|
1968
|
+
* @param candles - Array of validated candle data to cache
|
|
1931
1969
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1932
1970
|
* @param self - Instance of ClientExchange
|
|
1933
1971
|
*/
|
|
@@ -2054,6 +2092,13 @@ class ClientExchange {
|
|
|
2054
2092
|
/**
|
|
2055
2093
|
* Fetches historical candles backwards from execution context time.
|
|
2056
2094
|
*
|
|
2095
|
+
* Algorithm:
|
|
2096
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2097
|
+
* 2. Calculate since = alignedWhen - limit * step
|
|
2098
|
+
* 3. Fetch candles starting from since
|
|
2099
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2100
|
+
* 5. Slice to limit
|
|
2101
|
+
*
|
|
2057
2102
|
* @param symbol - Trading pair symbol
|
|
2058
2103
|
* @param interval - Candle interval
|
|
2059
2104
|
* @param limit - Number of candles to fetch
|
|
@@ -2066,11 +2111,16 @@ class ClientExchange {
|
|
|
2066
2111
|
limit,
|
|
2067
2112
|
});
|
|
2068
2113
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
2114
|
+
if (!step) {
|
|
2115
|
+
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2072
2116
|
}
|
|
2073
|
-
const
|
|
2117
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2118
|
+
// Align when down to interval boundary
|
|
2119
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2120
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2121
|
+
// Calculate since: go back limit candles from aligned when
|
|
2122
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2123
|
+
const since = new Date(sinceTimestamp);
|
|
2074
2124
|
let allData = [];
|
|
2075
2125
|
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
2076
2126
|
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
@@ -2083,39 +2133,34 @@ class ClientExchange {
|
|
|
2083
2133
|
remaining -= chunkLimit;
|
|
2084
2134
|
if (remaining > 0) {
|
|
2085
2135
|
// Move currentSince forward by the number of candles fetched
|
|
2086
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2136
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2087
2137
|
}
|
|
2088
2138
|
}
|
|
2089
2139
|
}
|
|
2090
2140
|
else {
|
|
2091
2141
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2092
2142
|
}
|
|
2093
|
-
// Filter candles to strictly match the requested range
|
|
2094
|
-
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2095
|
-
const sinceTimestamp = since.getTime();
|
|
2096
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2097
|
-
const filteredData = allData.filter((candle) => {
|
|
2098
|
-
// EXCLUSIVE boundaries:
|
|
2099
|
-
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
2100
|
-
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
2101
|
-
if (candle.timestamp <= sinceTimestamp) {
|
|
2102
|
-
return false;
|
|
2103
|
-
}
|
|
2104
|
-
// Check against current time (when)
|
|
2105
|
-
// Only allow candles that have fully CLOSED before "when"
|
|
2106
|
-
return candle.timestamp + stepMs < whenTimestamp;
|
|
2107
|
-
});
|
|
2108
2143
|
// Apply distinct by timestamp to remove duplicates
|
|
2109
|
-
const uniqueData = Array.from(new Map(
|
|
2110
|
-
if (
|
|
2111
|
-
const msg = `ClientExchange Removed ${
|
|
2144
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2145
|
+
if (allData.length !== uniqueData.length) {
|
|
2146
|
+
const msg = `ClientExchange getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2112
2147
|
this.params.logger.warn(msg);
|
|
2113
2148
|
console.warn(msg);
|
|
2114
2149
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2150
|
+
// Validate adapter returned data
|
|
2151
|
+
if (uniqueData.length === 0) {
|
|
2152
|
+
throw new Error(`ClientExchange getCandles: adapter returned empty array. ` +
|
|
2153
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2154
|
+
}
|
|
2155
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2156
|
+
throw new Error(`ClientExchange getCandles: first candle timestamp mismatch. ` +
|
|
2157
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2158
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2159
|
+
}
|
|
2160
|
+
if (uniqueData.length !== limit) {
|
|
2161
|
+
throw new Error(`ClientExchange getCandles: candle count mismatch. ` +
|
|
2162
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2163
|
+
`Adapter must return exact number of candles requested.`);
|
|
2119
2164
|
}
|
|
2120
2165
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2121
2166
|
return uniqueData;
|
|
@@ -2124,6 +2169,13 @@ class ClientExchange {
|
|
|
2124
2169
|
* Fetches future candles forwards from execution context time.
|
|
2125
2170
|
* Used in backtest mode to get candles for signal duration.
|
|
2126
2171
|
*
|
|
2172
|
+
* Algorithm:
|
|
2173
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2174
|
+
* 2. since = alignedWhen (start from aligned when)
|
|
2175
|
+
* 3. Fetch candles starting from since
|
|
2176
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2177
|
+
* 5. Slice to limit
|
|
2178
|
+
*
|
|
2127
2179
|
* @param symbol - Trading pair symbol
|
|
2128
2180
|
* @param interval - Candle interval
|
|
2129
2181
|
* @param limit - Number of candles to fetch
|
|
@@ -2139,12 +2191,21 @@ class ClientExchange {
|
|
|
2139
2191
|
if (!this.params.execution.context.backtest) {
|
|
2140
2192
|
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2141
2193
|
}
|
|
2142
|
-
const since = new Date(this.params.execution.context.when.getTime());
|
|
2143
|
-
const now = Date.now();
|
|
2144
|
-
// Вычисляем конечное время запроса
|
|
2145
2194
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2146
|
-
|
|
2147
|
-
|
|
2195
|
+
if (!step) {
|
|
2196
|
+
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2197
|
+
}
|
|
2198
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2199
|
+
const now = Date.now();
|
|
2200
|
+
// Align when down to interval boundary
|
|
2201
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2202
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2203
|
+
// since = alignedWhen (start from aligned when, going forward)
|
|
2204
|
+
const sinceTimestamp = alignedWhen;
|
|
2205
|
+
const since = new Date(sinceTimestamp);
|
|
2206
|
+
// Calculate end time for Date.now() check
|
|
2207
|
+
const endTime = sinceTimestamp + limit * stepMs;
|
|
2208
|
+
// Check that requested period does not exceed Date.now()
|
|
2148
2209
|
if (endTime > now) {
|
|
2149
2210
|
return [];
|
|
2150
2211
|
}
|
|
@@ -2160,29 +2221,34 @@ class ClientExchange {
|
|
|
2160
2221
|
remaining -= chunkLimit;
|
|
2161
2222
|
if (remaining > 0) {
|
|
2162
2223
|
// Move currentSince forward by the number of candles fetched
|
|
2163
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2224
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2164
2225
|
}
|
|
2165
2226
|
}
|
|
2166
2227
|
}
|
|
2167
2228
|
else {
|
|
2168
2229
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2169
2230
|
}
|
|
2170
|
-
// Filter candles to strictly match the requested range
|
|
2171
|
-
const sinceTimestamp = since.getTime();
|
|
2172
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2173
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2174
|
-
candle.timestamp + stepMs < endTime);
|
|
2175
2231
|
// Apply distinct by timestamp to remove duplicates
|
|
2176
|
-
const uniqueData = Array.from(new Map(
|
|
2177
|
-
if (
|
|
2178
|
-
const msg = `ClientExchange getNextCandles: Removed ${
|
|
2232
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2233
|
+
if (allData.length !== uniqueData.length) {
|
|
2234
|
+
const msg = `ClientExchange getNextCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2179
2235
|
this.params.logger.warn(msg);
|
|
2180
2236
|
console.warn(msg);
|
|
2181
2237
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2238
|
+
// Validate adapter returned data
|
|
2239
|
+
if (uniqueData.length === 0) {
|
|
2240
|
+
throw new Error(`ClientExchange getNextCandles: adapter returned empty array. ` +
|
|
2241
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2242
|
+
}
|
|
2243
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2244
|
+
throw new Error(`ClientExchange getNextCandles: first candle timestamp mismatch. ` +
|
|
2245
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2246
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2247
|
+
}
|
|
2248
|
+
if (uniqueData.length !== limit) {
|
|
2249
|
+
throw new Error(`ClientExchange getNextCandles: candle count mismatch. ` +
|
|
2250
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2251
|
+
`Adapter must return exact number of candles requested.`);
|
|
2186
2252
|
}
|
|
2187
2253
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2188
2254
|
return uniqueData;
|
|
@@ -2257,6 +2323,12 @@ class ClientExchange {
|
|
|
2257
2323
|
/**
|
|
2258
2324
|
* Fetches raw candles with flexible date/limit parameters.
|
|
2259
2325
|
*
|
|
2326
|
+
* Algorithm:
|
|
2327
|
+
* 1. Align all timestamps down to interval boundary
|
|
2328
|
+
* 2. Fetch candles starting from aligned since
|
|
2329
|
+
* 3. Validate first candle timestamp matches aligned since (adapter must return inclusive data)
|
|
2330
|
+
* 4. Slice to limit
|
|
2331
|
+
*
|
|
2260
2332
|
* All modes respect execution context and prevent look-ahead bias.
|
|
2261
2333
|
*
|
|
2262
2334
|
* Parameter combinations:
|
|
@@ -2291,9 +2363,10 @@ class ClientExchange {
|
|
|
2291
2363
|
if (!step) {
|
|
2292
2364
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2293
2365
|
}
|
|
2366
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2294
2367
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2368
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2295
2369
|
let sinceTimestamp;
|
|
2296
|
-
let untilTimestamp;
|
|
2297
2370
|
let calculatedLimit;
|
|
2298
2371
|
// Case 1: all three parameters provided
|
|
2299
2372
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -2303,8 +2376,8 @@ class ClientExchange {
|
|
|
2303
2376
|
if (eDate > whenTimestamp) {
|
|
2304
2377
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2305
2378
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2379
|
+
// Align sDate down to interval boundary
|
|
2380
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2308
2381
|
calculatedLimit = limit;
|
|
2309
2382
|
}
|
|
2310
2383
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2315,9 +2388,10 @@ class ClientExchange {
|
|
|
2315
2388
|
if (eDate > whenTimestamp) {
|
|
2316
2389
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2317
2390
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2391
|
+
// Align sDate down to interval boundary
|
|
2392
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2393
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2394
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2321
2395
|
if (calculatedLimit <= 0) {
|
|
2322
2396
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
2323
2397
|
}
|
|
@@ -2327,23 +2401,24 @@ class ClientExchange {
|
|
|
2327
2401
|
if (eDate > whenTimestamp) {
|
|
2328
2402
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2329
2403
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2404
|
+
// Align eDate down and calculate sinceTimestamp
|
|
2405
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2406
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2332
2407
|
calculatedLimit = limit;
|
|
2333
2408
|
}
|
|
2334
2409
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2335
2410
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2411
|
+
// Align sDate down to interval boundary
|
|
2412
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2413
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2414
|
+
if (endTimestamp > whenTimestamp) {
|
|
2415
|
+
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2340
2416
|
}
|
|
2341
2417
|
calculatedLimit = limit;
|
|
2342
2418
|
}
|
|
2343
2419
|
// Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
|
|
2344
2420
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
2345
|
-
|
|
2346
|
-
sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
|
|
2421
|
+
sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2347
2422
|
calculatedLimit = limit;
|
|
2348
2423
|
}
|
|
2349
2424
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -2364,29 +2439,34 @@ class ClientExchange {
|
|
|
2364
2439
|
allData.push(...chunkData);
|
|
2365
2440
|
remaining -= chunkLimit;
|
|
2366
2441
|
if (remaining > 0) {
|
|
2367
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2442
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2368
2443
|
}
|
|
2369
2444
|
}
|
|
2370
2445
|
}
|
|
2371
2446
|
else {
|
|
2372
2447
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2373
2448
|
}
|
|
2374
|
-
// Filter candles to strictly match the requested range
|
|
2375
|
-
// Only include candles that have fully CLOSED before untilTimestamp
|
|
2376
|
-
const stepMs = step * MS_PER_MINUTE$1;
|
|
2377
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2378
|
-
candle.timestamp + stepMs < untilTimestamp);
|
|
2379
2449
|
// Apply distinct by timestamp to remove duplicates
|
|
2380
|
-
const uniqueData = Array.from(new Map(
|
|
2381
|
-
if (
|
|
2382
|
-
const msg = `ClientExchange getRawCandles: Removed ${
|
|
2450
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2451
|
+
if (allData.length !== uniqueData.length) {
|
|
2452
|
+
const msg = `ClientExchange getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2383
2453
|
this.params.logger.warn(msg);
|
|
2384
2454
|
console.warn(msg);
|
|
2385
2455
|
}
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2456
|
+
// Validate adapter returned data
|
|
2457
|
+
if (uniqueData.length === 0) {
|
|
2458
|
+
throw new Error(`ClientExchange getRawCandles: adapter returned empty array. ` +
|
|
2459
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2460
|
+
}
|
|
2461
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2462
|
+
throw new Error(`ClientExchange getRawCandles: first candle timestamp mismatch. ` +
|
|
2463
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2464
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2465
|
+
}
|
|
2466
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
2467
|
+
throw new Error(`ClientExchange getRawCandles: candle count mismatch. ` +
|
|
2468
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
2469
|
+
`Adapter must return exact number of candles requested.`);
|
|
2390
2470
|
}
|
|
2391
2471
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
|
|
2392
2472
|
return uniqueData;
|
|
@@ -4944,6 +5024,68 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
|
|
|
4944
5024
|
const result = await CANCEL_SCHEDULED_SIGNAL_IN_BACKTEST_FN(self, scheduled, averagePrice, candle.timestamp, "user");
|
|
4945
5025
|
return { activated: false, cancelled: true, activationIndex: i, result };
|
|
4946
5026
|
}
|
|
5027
|
+
// КРИТИЧНО: Проверяем был ли сигнал активирован пользователем через activateScheduled()
|
|
5028
|
+
// Обрабатываем inline (как в tick()) с риск-проверкой по averagePrice
|
|
5029
|
+
if (self._activatedSignal) {
|
|
5030
|
+
const activatedSignal = self._activatedSignal;
|
|
5031
|
+
self._activatedSignal = null;
|
|
5032
|
+
// Check if strategy was stopped
|
|
5033
|
+
if (self._isStopped) {
|
|
5034
|
+
self.params.logger.info("ClientStrategy backtest user-activated signal cancelled (stopped)", {
|
|
5035
|
+
symbol: self.params.execution.context.symbol,
|
|
5036
|
+
signalId: activatedSignal.id,
|
|
5037
|
+
});
|
|
5038
|
+
await self.setScheduledSignal(null);
|
|
5039
|
+
return { activated: false, cancelled: false, activationIndex: i, result: null };
|
|
5040
|
+
}
|
|
5041
|
+
// Риск-проверка по averagePrice (симметрия с LIVE tick())
|
|
5042
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, averagePrice, candle.timestamp, self.params.execution.context.backtest))) {
|
|
5043
|
+
self.params.logger.info("ClientStrategy backtest user-activated signal rejected by risk", {
|
|
5044
|
+
symbol: self.params.execution.context.symbol,
|
|
5045
|
+
signalId: activatedSignal.id,
|
|
5046
|
+
});
|
|
5047
|
+
await self.setScheduledSignal(null);
|
|
5048
|
+
return { activated: false, cancelled: false, activationIndex: i, result: null };
|
|
5049
|
+
}
|
|
5050
|
+
await self.setScheduledSignal(null);
|
|
5051
|
+
const pendingSignal = {
|
|
5052
|
+
...activatedSignal,
|
|
5053
|
+
pendingAt: candle.timestamp,
|
|
5054
|
+
_isScheduled: false,
|
|
5055
|
+
};
|
|
5056
|
+
await self.setPendingSignal(pendingSignal);
|
|
5057
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5058
|
+
// Emit commit AFTER successful risk check
|
|
5059
|
+
const publicSignalForCommit = TO_PUBLIC_SIGNAL(pendingSignal);
|
|
5060
|
+
await CALL_COMMIT_FN(self, {
|
|
5061
|
+
action: "activate-scheduled",
|
|
5062
|
+
symbol: self.params.execution.context.symbol,
|
|
5063
|
+
strategyName: self.params.strategyName,
|
|
5064
|
+
exchangeName: self.params.exchangeName,
|
|
5065
|
+
frameName: self.params.frameName,
|
|
5066
|
+
signalId: activatedSignal.id,
|
|
5067
|
+
backtest: self.params.execution.context.backtest,
|
|
5068
|
+
activateId: activatedSignal.activateId,
|
|
5069
|
+
timestamp: candle.timestamp,
|
|
5070
|
+
currentPrice: averagePrice,
|
|
5071
|
+
position: publicSignalForCommit.position,
|
|
5072
|
+
priceOpen: publicSignalForCommit.priceOpen,
|
|
5073
|
+
priceTakeProfit: publicSignalForCommit.priceTakeProfit,
|
|
5074
|
+
priceStopLoss: publicSignalForCommit.priceStopLoss,
|
|
5075
|
+
originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
|
|
5076
|
+
originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
|
|
5077
|
+
scheduledAt: publicSignalForCommit.scheduledAt,
|
|
5078
|
+
pendingAt: publicSignalForCommit.pendingAt,
|
|
5079
|
+
});
|
|
5080
|
+
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, pendingSignal, pendingSignal.priceOpen, candle.timestamp, self.params.execution.context.backtest);
|
|
5081
|
+
await CALL_BACKTEST_SCHEDULE_OPEN_FN(self, self.params.execution.context.symbol, pendingSignal, candle.timestamp, self.params.execution.context.backtest);
|
|
5082
|
+
return {
|
|
5083
|
+
activated: true,
|
|
5084
|
+
cancelled: false,
|
|
5085
|
+
activationIndex: i,
|
|
5086
|
+
result: null,
|
|
5087
|
+
};
|
|
5088
|
+
}
|
|
4947
5089
|
// КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
|
|
4948
5090
|
const elapsedTime = candle.timestamp - scheduled.scheduledAt;
|
|
4949
5091
|
if (elapsedTime >= maxTimeToWait) {
|
|
@@ -5166,6 +5308,7 @@ class ClientStrategy {
|
|
|
5166
5308
|
this._scheduledSignal = null;
|
|
5167
5309
|
this._cancelledSignal = null;
|
|
5168
5310
|
this._closedSignal = null;
|
|
5311
|
+
this._activatedSignal = null;
|
|
5169
5312
|
/** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
|
|
5170
5313
|
this._commitQueue = [];
|
|
5171
5314
|
/**
|
|
@@ -5203,6 +5346,16 @@ class ClientStrategy {
|
|
|
5203
5346
|
this.params.logger.debug("ClientStrategy setPendingSignal", {
|
|
5204
5347
|
pendingSignal,
|
|
5205
5348
|
});
|
|
5349
|
+
// КРИТИЧНО: Очищаем флаг закрытия при любом изменении pending signal
|
|
5350
|
+
// - при null: сигнал закрыт по TP/SL/timeout, флаг больше не нужен
|
|
5351
|
+
// - при новом сигнале: флаг от предыдущего сигнала не должен влиять на новый
|
|
5352
|
+
this._closedSignal = null;
|
|
5353
|
+
// ЗАЩИТА ИНВАРИАНТА: При установке нового pending сигнала очищаем scheduled
|
|
5354
|
+
// Не может быть одновременно pending И scheduled (взаимоисключающие состояния)
|
|
5355
|
+
// При null: scheduled может существовать (новый сигнал после закрытия позиции)
|
|
5356
|
+
if (pendingSignal !== null) {
|
|
5357
|
+
this._scheduledSignal = null;
|
|
5358
|
+
}
|
|
5206
5359
|
this._pendingSignal = pendingSignal;
|
|
5207
5360
|
// КРИТИЧНО: Всегда вызываем коллбек onWrite для тестирования persist storage
|
|
5208
5361
|
// даже в backtest режиме, чтобы тесты могли перехватывать вызовы через mock adapter
|
|
@@ -5228,6 +5381,11 @@ class ClientStrategy {
|
|
|
5228
5381
|
this.params.logger.debug("ClientStrategy setScheduledSignal", {
|
|
5229
5382
|
scheduledSignal,
|
|
5230
5383
|
});
|
|
5384
|
+
// КРИТИЧНО: Очищаем флаги отмены и активации при любом изменении scheduled signal
|
|
5385
|
+
// - при null: сигнал отменен/активирован по timeout/SL/user, флаги больше не нужны
|
|
5386
|
+
// - при новом сигнале: флаги от предыдущего сигнала не должны влиять на новый
|
|
5387
|
+
this._cancelledSignal = null;
|
|
5388
|
+
this._activatedSignal = null;
|
|
5231
5389
|
this._scheduledSignal = scheduledSignal;
|
|
5232
5390
|
if (this.params.execution.context.backtest) {
|
|
5233
5391
|
return;
|
|
@@ -5417,12 +5575,8 @@ class ClientStrategy {
|
|
|
5417
5575
|
const currentTime = this.params.execution.context.when.getTime();
|
|
5418
5576
|
// Process queued commit events with proper timestamp
|
|
5419
5577
|
await PROCESS_COMMIT_QUEUE_FN(this, currentTime);
|
|
5420
|
-
// Early return if strategy was stopped
|
|
5421
|
-
if (this._isStopped) {
|
|
5422
|
-
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5423
|
-
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5424
|
-
}
|
|
5425
5578
|
// Check if scheduled signal was cancelled - emit cancelled event once
|
|
5579
|
+
// NOTE: No _isStopped check here - cancellation must work for graceful shutdown
|
|
5426
5580
|
if (this._cancelledSignal) {
|
|
5427
5581
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5428
5582
|
const cancelledSignal = this._cancelledSignal;
|
|
@@ -5431,6 +5585,18 @@ class ClientStrategy {
|
|
|
5431
5585
|
symbol: this.params.execution.context.symbol,
|
|
5432
5586
|
signalId: cancelledSignal.id,
|
|
5433
5587
|
});
|
|
5588
|
+
// Emit commit with correct timestamp from tick context
|
|
5589
|
+
await CALL_COMMIT_FN(this, {
|
|
5590
|
+
action: "cancel-scheduled",
|
|
5591
|
+
symbol: this.params.execution.context.symbol,
|
|
5592
|
+
strategyName: this.params.strategyName,
|
|
5593
|
+
exchangeName: this.params.exchangeName,
|
|
5594
|
+
frameName: this.params.frameName,
|
|
5595
|
+
signalId: cancelledSignal.id,
|
|
5596
|
+
backtest: this.params.execution.context.backtest,
|
|
5597
|
+
cancelId: cancelledSignal.cancelId,
|
|
5598
|
+
timestamp: currentTime,
|
|
5599
|
+
});
|
|
5434
5600
|
// Call onCancel callback
|
|
5435
5601
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5436
5602
|
const result = {
|
|
@@ -5459,6 +5625,18 @@ class ClientStrategy {
|
|
|
5459
5625
|
symbol: this.params.execution.context.symbol,
|
|
5460
5626
|
signalId: closedSignal.id,
|
|
5461
5627
|
});
|
|
5628
|
+
// Emit commit with correct timestamp from tick context
|
|
5629
|
+
await CALL_COMMIT_FN(this, {
|
|
5630
|
+
action: "close-pending",
|
|
5631
|
+
symbol: this.params.execution.context.symbol,
|
|
5632
|
+
strategyName: this.params.strategyName,
|
|
5633
|
+
exchangeName: this.params.exchangeName,
|
|
5634
|
+
frameName: this.params.frameName,
|
|
5635
|
+
signalId: closedSignal.id,
|
|
5636
|
+
backtest: this.params.execution.context.backtest,
|
|
5637
|
+
closeId: closedSignal.closeId,
|
|
5638
|
+
timestamp: currentTime,
|
|
5639
|
+
});
|
|
5462
5640
|
// Call onClose callback
|
|
5463
5641
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5464
5642
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
@@ -5485,6 +5663,78 @@ class ClientStrategy {
|
|
|
5485
5663
|
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
|
|
5486
5664
|
return result;
|
|
5487
5665
|
}
|
|
5666
|
+
// Check if scheduled signal was activated - emit opened event once
|
|
5667
|
+
if (this._activatedSignal) {
|
|
5668
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5669
|
+
const activatedSignal = this._activatedSignal;
|
|
5670
|
+
this._activatedSignal = null; // Clear after emitting
|
|
5671
|
+
this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
|
|
5672
|
+
symbol: this.params.execution.context.symbol,
|
|
5673
|
+
signalId: activatedSignal.id,
|
|
5674
|
+
});
|
|
5675
|
+
// Check if strategy was stopped (symmetry with backtest PROCESS_SCHEDULED_SIGNAL_CANDLES_FN)
|
|
5676
|
+
if (this._isStopped) {
|
|
5677
|
+
this.params.logger.info("ClientStrategy tick: user-activated signal cancelled (stopped)", {
|
|
5678
|
+
symbol: this.params.execution.context.symbol,
|
|
5679
|
+
signalId: activatedSignal.id,
|
|
5680
|
+
});
|
|
5681
|
+
await this.setScheduledSignal(null);
|
|
5682
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5683
|
+
}
|
|
5684
|
+
// Check risk before activation
|
|
5685
|
+
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(this, this.params.execution.context.symbol, activatedSignal, currentPrice, currentTime, this.params.execution.context.backtest))) {
|
|
5686
|
+
this.params.logger.info("ClientStrategy tick: activated signal rejected by risk", {
|
|
5687
|
+
symbol: this.params.execution.context.symbol,
|
|
5688
|
+
signalId: activatedSignal.id,
|
|
5689
|
+
});
|
|
5690
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5691
|
+
}
|
|
5692
|
+
// КРИТИЧЕСКИ ВАЖНО: обновляем pendingAt при активации
|
|
5693
|
+
const pendingSignal = {
|
|
5694
|
+
...activatedSignal,
|
|
5695
|
+
pendingAt: currentTime,
|
|
5696
|
+
_isScheduled: false,
|
|
5697
|
+
};
|
|
5698
|
+
await this.setPendingSignal(pendingSignal);
|
|
5699
|
+
await CALL_RISK_ADD_SIGNAL_FN(this, this.params.execution.context.symbol, pendingSignal, currentTime, this.params.execution.context.backtest);
|
|
5700
|
+
// Emit commit AFTER successful risk check
|
|
5701
|
+
const publicSignalForCommit = TO_PUBLIC_SIGNAL(pendingSignal);
|
|
5702
|
+
await CALL_COMMIT_FN(this, {
|
|
5703
|
+
action: "activate-scheduled",
|
|
5704
|
+
symbol: this.params.execution.context.symbol,
|
|
5705
|
+
strategyName: this.params.strategyName,
|
|
5706
|
+
exchangeName: this.params.exchangeName,
|
|
5707
|
+
frameName: this.params.frameName,
|
|
5708
|
+
signalId: activatedSignal.id,
|
|
5709
|
+
backtest: this.params.execution.context.backtest,
|
|
5710
|
+
activateId: activatedSignal.activateId,
|
|
5711
|
+
timestamp: currentTime,
|
|
5712
|
+
currentPrice,
|
|
5713
|
+
position: publicSignalForCommit.position,
|
|
5714
|
+
priceOpen: publicSignalForCommit.priceOpen,
|
|
5715
|
+
priceTakeProfit: publicSignalForCommit.priceTakeProfit,
|
|
5716
|
+
priceStopLoss: publicSignalForCommit.priceStopLoss,
|
|
5717
|
+
originalPriceTakeProfit: publicSignalForCommit.originalPriceTakeProfit,
|
|
5718
|
+
originalPriceStopLoss: publicSignalForCommit.originalPriceStopLoss,
|
|
5719
|
+
scheduledAt: publicSignalForCommit.scheduledAt,
|
|
5720
|
+
pendingAt: publicSignalForCommit.pendingAt,
|
|
5721
|
+
});
|
|
5722
|
+
// Call onOpen callback
|
|
5723
|
+
await CALL_OPEN_CALLBACKS_FN(this, this.params.execution.context.symbol, pendingSignal, currentPrice, currentTime, this.params.execution.context.backtest);
|
|
5724
|
+
const result = {
|
|
5725
|
+
action: "opened",
|
|
5726
|
+
signal: TO_PUBLIC_SIGNAL(pendingSignal),
|
|
5727
|
+
strategyName: this.params.method.context.strategyName,
|
|
5728
|
+
exchangeName: this.params.method.context.exchangeName,
|
|
5729
|
+
frameName: this.params.method.context.frameName,
|
|
5730
|
+
symbol: this.params.execution.context.symbol,
|
|
5731
|
+
currentPrice,
|
|
5732
|
+
backtest: this.params.execution.context.backtest,
|
|
5733
|
+
createdAt: currentTime,
|
|
5734
|
+
};
|
|
5735
|
+
await CALL_TICK_CALLBACKS_FN(this, this.params.execution.context.symbol, result, currentTime, this.params.execution.context.backtest);
|
|
5736
|
+
return result;
|
|
5737
|
+
}
|
|
5488
5738
|
// Monitor scheduled signal
|
|
5489
5739
|
if (this._scheduledSignal && !this._pendingSignal) {
|
|
5490
5740
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
@@ -5508,7 +5758,12 @@ class ClientStrategy {
|
|
|
5508
5758
|
return await RETURN_SCHEDULED_SIGNAL_ACTIVE_FN(this, this._scheduledSignal, currentPrice);
|
|
5509
5759
|
}
|
|
5510
5760
|
// Generate new signal if none exists
|
|
5761
|
+
// NOTE: _isStopped blocks NEW signal generation but allows existing positions to continue
|
|
5511
5762
|
if (!this._pendingSignal && !this._scheduledSignal) {
|
|
5763
|
+
if (this._isStopped) {
|
|
5764
|
+
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
5765
|
+
return await RETURN_IDLE_FN(this, currentPrice);
|
|
5766
|
+
}
|
|
5512
5767
|
const signal = await GET_SIGNAL_FN(this);
|
|
5513
5768
|
if (signal) {
|
|
5514
5769
|
if (signal._isScheduled === true) {
|
|
@@ -5582,6 +5837,18 @@ class ClientStrategy {
|
|
|
5582
5837
|
const cancelledSignal = this._cancelledSignal;
|
|
5583
5838
|
this._cancelledSignal = null; // Clear after using
|
|
5584
5839
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
5840
|
+
// Emit commit with correct timestamp from backtest context
|
|
5841
|
+
await CALL_COMMIT_FN(this, {
|
|
5842
|
+
action: "cancel-scheduled",
|
|
5843
|
+
symbol: this.params.execution.context.symbol,
|
|
5844
|
+
strategyName: this.params.strategyName,
|
|
5845
|
+
exchangeName: this.params.exchangeName,
|
|
5846
|
+
frameName: this.params.frameName,
|
|
5847
|
+
signalId: cancelledSignal.id,
|
|
5848
|
+
backtest: true,
|
|
5849
|
+
cancelId: cancelledSignal.cancelId,
|
|
5850
|
+
timestamp: closeTimestamp,
|
|
5851
|
+
});
|
|
5585
5852
|
await CALL_CANCEL_CALLBACKS_FN(this, this.params.execution.context.symbol, cancelledSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
5586
5853
|
const cancelledResult = {
|
|
5587
5854
|
action: "cancelled",
|
|
@@ -5607,6 +5874,18 @@ class ClientStrategy {
|
|
|
5607
5874
|
const closedSignal = this._closedSignal;
|
|
5608
5875
|
this._closedSignal = null; // Clear after using
|
|
5609
5876
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
5877
|
+
// Emit commit with correct timestamp from backtest context
|
|
5878
|
+
await CALL_COMMIT_FN(this, {
|
|
5879
|
+
action: "close-pending",
|
|
5880
|
+
symbol: this.params.execution.context.symbol,
|
|
5881
|
+
strategyName: this.params.strategyName,
|
|
5882
|
+
exchangeName: this.params.exchangeName,
|
|
5883
|
+
frameName: this.params.frameName,
|
|
5884
|
+
signalId: closedSignal.id,
|
|
5885
|
+
backtest: true,
|
|
5886
|
+
closeId: closedSignal.closeId,
|
|
5887
|
+
timestamp: closeTimestamp,
|
|
5888
|
+
});
|
|
5610
5889
|
await CALL_CLOSE_CALLBACKS_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
5611
5890
|
// КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
|
|
5612
5891
|
await CALL_PARTIAL_CLEAR_FN(this, this.params.execution.context.symbol, closedSignal, currentPrice, closeTimestamp, this.params.execution.context.backtest);
|
|
@@ -5767,8 +6046,18 @@ class ClientStrategy {
|
|
|
5767
6046
|
symbol,
|
|
5768
6047
|
hasPendingSignal: this._pendingSignal !== null,
|
|
5769
6048
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
6049
|
+
hasActivatedSignal: this._activatedSignal !== null,
|
|
6050
|
+
hasCancelledSignal: this._cancelledSignal !== null,
|
|
6051
|
+
hasClosedSignal: this._closedSignal !== null,
|
|
5770
6052
|
});
|
|
5771
6053
|
this._isStopped = true;
|
|
6054
|
+
// Clear pending flags to start from clean state
|
|
6055
|
+
// NOTE: _isStopped blocks NEW position opening, but allows:
|
|
6056
|
+
// - cancelScheduled() / closePending() for graceful shutdown
|
|
6057
|
+
// - Monitoring existing _pendingSignal until TP/SL/timeout
|
|
6058
|
+
this._activatedSignal = null;
|
|
6059
|
+
this._cancelledSignal = null;
|
|
6060
|
+
this._closedSignal = null;
|
|
5772
6061
|
// Clear scheduled signal if exists
|
|
5773
6062
|
if (!this._scheduledSignal) {
|
|
5774
6063
|
return;
|
|
@@ -5806,8 +6095,9 @@ class ClientStrategy {
|
|
|
5806
6095
|
hasScheduledSignal: this._scheduledSignal !== null,
|
|
5807
6096
|
cancelId,
|
|
5808
6097
|
});
|
|
5809
|
-
//
|
|
5810
|
-
|
|
6098
|
+
// NOTE: No _isStopped check - cancellation must work for graceful shutdown
|
|
6099
|
+
// (cancelling scheduled signal is not opening new position)
|
|
6100
|
+
// Save cancelled signal for next tick/backtest to emit cancelled event with correct timestamp
|
|
5811
6101
|
if (this._scheduledSignal) {
|
|
5812
6102
|
this._cancelledSignal = Object.assign({}, this._scheduledSignal, {
|
|
5813
6103
|
cancelId,
|
|
@@ -5815,37 +6105,60 @@ class ClientStrategy {
|
|
|
5815
6105
|
this._scheduledSignal = null;
|
|
5816
6106
|
}
|
|
5817
6107
|
if (backtest) {
|
|
5818
|
-
//
|
|
5819
|
-
if (hadScheduledSignal) {
|
|
5820
|
-
await CALL_COMMIT_FN(this, {
|
|
5821
|
-
action: "cancel-scheduled",
|
|
5822
|
-
symbol,
|
|
5823
|
-
strategyName: this.params.strategyName,
|
|
5824
|
-
exchangeName: this.params.exchangeName,
|
|
5825
|
-
frameName: this.params.frameName,
|
|
5826
|
-
signalId: this._cancelledSignal.id,
|
|
5827
|
-
backtest,
|
|
5828
|
-
cancelId,
|
|
5829
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5830
|
-
});
|
|
5831
|
-
}
|
|
6108
|
+
// Commit will be emitted in backtest() with correct candle timestamp
|
|
5832
6109
|
return;
|
|
5833
6110
|
}
|
|
5834
6111
|
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
|
|
5835
|
-
//
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
6112
|
+
// Commit will be emitted in tick() with correct currentTime
|
|
6113
|
+
}
|
|
6114
|
+
/**
|
|
6115
|
+
* Activates the scheduled signal without waiting for price to reach priceOpen.
|
|
6116
|
+
*
|
|
6117
|
+
* Forces immediate activation of the scheduled signal at the current price.
|
|
6118
|
+
* Does NOT affect active pending signals or strategy operation.
|
|
6119
|
+
* Does NOT set stop flag - strategy can continue generating new signals.
|
|
6120
|
+
*
|
|
6121
|
+
* Use case: User-initiated early activation of a scheduled entry.
|
|
6122
|
+
*
|
|
6123
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
6124
|
+
* @param backtest - Whether running in backtest mode
|
|
6125
|
+
* @param activateId - Optional identifier for this activation operation
|
|
6126
|
+
* @returns Promise that resolves when scheduled signal is activated
|
|
6127
|
+
*
|
|
6128
|
+
* @example
|
|
6129
|
+
* ```typescript
|
|
6130
|
+
* // Activate scheduled signal without waiting for priceOpen
|
|
6131
|
+
* await strategy.activateScheduled("BTCUSDT", false, "user-activate-123");
|
|
6132
|
+
* // Scheduled signal becomes pending signal immediately
|
|
6133
|
+
* ```
|
|
6134
|
+
*/
|
|
6135
|
+
async activateScheduled(symbol, backtest, activateId) {
|
|
6136
|
+
this.params.logger.debug("ClientStrategy activateScheduled", {
|
|
6137
|
+
symbol,
|
|
6138
|
+
hasScheduledSignal: this._scheduledSignal !== null,
|
|
6139
|
+
activateId,
|
|
6140
|
+
});
|
|
6141
|
+
// Block activation if strategy stopped - activation = opening NEW position
|
|
6142
|
+
// (unlike cancelScheduled/closePending which handle existing signals for graceful shutdown)
|
|
6143
|
+
if (this._isStopped) {
|
|
6144
|
+
this.params.logger.debug("ClientStrategy activateScheduled: strategy stopped, skipping", {
|
|
5839
6145
|
symbol,
|
|
5840
|
-
strategyName: this.params.strategyName,
|
|
5841
|
-
exchangeName: this.params.exchangeName,
|
|
5842
|
-
frameName: this.params.frameName,
|
|
5843
|
-
signalId: this._cancelledSignal.id,
|
|
5844
|
-
backtest,
|
|
5845
|
-
cancelId,
|
|
5846
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5847
6146
|
});
|
|
6147
|
+
return;
|
|
6148
|
+
}
|
|
6149
|
+
// Save activated signal for next tick to emit opened event
|
|
6150
|
+
if (this._scheduledSignal) {
|
|
6151
|
+
this._activatedSignal = Object.assign({}, this._scheduledSignal, {
|
|
6152
|
+
activateId,
|
|
6153
|
+
});
|
|
6154
|
+
this._scheduledSignal = null;
|
|
6155
|
+
}
|
|
6156
|
+
if (backtest) {
|
|
6157
|
+
// Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
|
|
6158
|
+
return;
|
|
5848
6159
|
}
|
|
6160
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.method.context.strategyName, this.params.method.context.exchangeName);
|
|
6161
|
+
// Commit will be emitted AFTER successful risk check in tick()
|
|
5849
6162
|
}
|
|
5850
6163
|
/**
|
|
5851
6164
|
* Closes the pending signal without stopping the strategy.
|
|
@@ -5874,8 +6187,8 @@ class ClientStrategy {
|
|
|
5874
6187
|
hasPendingSignal: this._pendingSignal !== null,
|
|
5875
6188
|
closeId,
|
|
5876
6189
|
});
|
|
5877
|
-
//
|
|
5878
|
-
|
|
6190
|
+
// NOTE: No _isStopped check - closing position must work for graceful shutdown
|
|
6191
|
+
// Save closed signal for next tick/backtest to emit closed event with correct timestamp
|
|
5879
6192
|
if (this._pendingSignal) {
|
|
5880
6193
|
this._closedSignal = Object.assign({}, this._pendingSignal, {
|
|
5881
6194
|
closeId,
|
|
@@ -5883,37 +6196,11 @@ class ClientStrategy {
|
|
|
5883
6196
|
this._pendingSignal = null;
|
|
5884
6197
|
}
|
|
5885
6198
|
if (backtest) {
|
|
5886
|
-
//
|
|
5887
|
-
if (hadPendingSignal) {
|
|
5888
|
-
await CALL_COMMIT_FN(this, {
|
|
5889
|
-
action: "close-pending",
|
|
5890
|
-
symbol,
|
|
5891
|
-
strategyName: this.params.strategyName,
|
|
5892
|
-
exchangeName: this.params.exchangeName,
|
|
5893
|
-
frameName: this.params.frameName,
|
|
5894
|
-
signalId: this._closedSignal.id,
|
|
5895
|
-
backtest,
|
|
5896
|
-
closeId,
|
|
5897
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5898
|
-
});
|
|
5899
|
-
}
|
|
6199
|
+
// Commit will be emitted in backtest() with correct candle timestamp
|
|
5900
6200
|
return;
|
|
5901
6201
|
}
|
|
5902
6202
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
5903
|
-
//
|
|
5904
|
-
if (hadPendingSignal) {
|
|
5905
|
-
await CALL_COMMIT_FN(this, {
|
|
5906
|
-
action: "close-pending",
|
|
5907
|
-
symbol,
|
|
5908
|
-
strategyName: this.params.strategyName,
|
|
5909
|
-
exchangeName: this.params.exchangeName,
|
|
5910
|
-
frameName: this.params.frameName,
|
|
5911
|
-
signalId: this._closedSignal.id,
|
|
5912
|
-
backtest,
|
|
5913
|
-
closeId,
|
|
5914
|
-
timestamp: this.params.execution.context.when.getTime(),
|
|
5915
|
-
});
|
|
5916
|
-
}
|
|
6203
|
+
// Commit will be emitted in tick() with correct currentTime
|
|
5917
6204
|
}
|
|
5918
6205
|
/**
|
|
5919
6206
|
* Executes partial close at profit level (moving toward TP).
|
|
@@ -7752,6 +8039,39 @@ class StrategyConnectionService {
|
|
|
7752
8039
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
7753
8040
|
return await strategy.breakeven(symbol, currentPrice, backtest);
|
|
7754
8041
|
};
|
|
8042
|
+
/**
|
|
8043
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
8044
|
+
*
|
|
8045
|
+
* Delegates to ClientStrategy.activateScheduled() which sets _activatedSignal flag.
|
|
8046
|
+
* The actual activation happens on next tick() when strategy detects the flag.
|
|
8047
|
+
*
|
|
8048
|
+
* @param backtest - Whether running in backtest mode
|
|
8049
|
+
* @param symbol - Trading pair symbol
|
|
8050
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
8051
|
+
* @param activateId - Optional identifier for the activation reason
|
|
8052
|
+
* @returns Promise that resolves when activation flag is set
|
|
8053
|
+
*
|
|
8054
|
+
* @example
|
|
8055
|
+
* ```typescript
|
|
8056
|
+
* // Activate scheduled signal early
|
|
8057
|
+
* await strategyConnectionService.activateScheduled(
|
|
8058
|
+
* false,
|
|
8059
|
+
* "BTCUSDT",
|
|
8060
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
8061
|
+
* "manual-activation"
|
|
8062
|
+
* );
|
|
8063
|
+
* ```
|
|
8064
|
+
*/
|
|
8065
|
+
this.activateScheduled = async (backtest, symbol, context, activateId) => {
|
|
8066
|
+
this.loggerService.log("strategyConnectionService activateScheduled", {
|
|
8067
|
+
symbol,
|
|
8068
|
+
context,
|
|
8069
|
+
backtest,
|
|
8070
|
+
activateId,
|
|
8071
|
+
});
|
|
8072
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
8073
|
+
return await strategy.activateScheduled(symbol, backtest, activateId);
|
|
8074
|
+
};
|
|
7755
8075
|
}
|
|
7756
8076
|
}
|
|
7757
8077
|
|
|
@@ -11064,6 +11384,39 @@ class StrategyCoreService {
|
|
|
11064
11384
|
await this.validate(context);
|
|
11065
11385
|
return await this.strategyConnectionService.breakeven(backtest, symbol, currentPrice, context);
|
|
11066
11386
|
};
|
|
11387
|
+
/**
|
|
11388
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
11389
|
+
*
|
|
11390
|
+
* Validates strategy existence and delegates to connection service
|
|
11391
|
+
* to set the activation flag. The actual activation happens on next tick().
|
|
11392
|
+
*
|
|
11393
|
+
* @param backtest - Whether running in backtest mode
|
|
11394
|
+
* @param symbol - Trading pair symbol
|
|
11395
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
11396
|
+
* @param activateId - Optional identifier for the activation reason
|
|
11397
|
+
* @returns Promise that resolves when activation flag is set
|
|
11398
|
+
*
|
|
11399
|
+
* @example
|
|
11400
|
+
* ```typescript
|
|
11401
|
+
* // Activate scheduled signal early
|
|
11402
|
+
* await strategyCoreService.activateScheduled(
|
|
11403
|
+
* false,
|
|
11404
|
+
* "BTCUSDT",
|
|
11405
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" },
|
|
11406
|
+
* "manual-activation"
|
|
11407
|
+
* );
|
|
11408
|
+
* ```
|
|
11409
|
+
*/
|
|
11410
|
+
this.activateScheduled = async (backtest, symbol, context, activateId) => {
|
|
11411
|
+
this.loggerService.log("strategyCoreService activateScheduled", {
|
|
11412
|
+
symbol,
|
|
11413
|
+
context,
|
|
11414
|
+
backtest,
|
|
11415
|
+
activateId,
|
|
11416
|
+
});
|
|
11417
|
+
await this.validate(context);
|
|
11418
|
+
return await this.strategyConnectionService.activateScheduled(backtest, symbol, context, activateId);
|
|
11419
|
+
};
|
|
11067
11420
|
}
|
|
11068
11421
|
}
|
|
11069
11422
|
|
|
@@ -12343,13 +12696,8 @@ class WalkerSchemaService {
|
|
|
12343
12696
|
}
|
|
12344
12697
|
}
|
|
12345
12698
|
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
* ClientExchange.getNextCandles использует фильтр:
|
|
12349
|
-
* timestamp > since && timestamp + stepMs < endTime
|
|
12350
|
-
* который исключает первую и последнюю свечи из запрошенного диапазона.
|
|
12351
|
-
*/
|
|
12352
|
-
const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
|
|
12699
|
+
const ACTIVE_CANDLE_INCLUDED = 1;
|
|
12700
|
+
const SCHEDULE_ACTIVATION_CANDLE_SKIP = 1;
|
|
12353
12701
|
/**
|
|
12354
12702
|
* Private service for backtest orchestration using async generators.
|
|
12355
12703
|
*
|
|
@@ -12473,9 +12821,9 @@ class BacktestLogicPrivateService {
|
|
|
12473
12821
|
// - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
|
|
12474
12822
|
// - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
|
|
12475
12823
|
// - +1 потому что when включается как первая свеча
|
|
12476
|
-
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT -
|
|
12824
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - ACTIVE_CANDLE_INCLUDED;
|
|
12477
12825
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12478
|
-
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime +
|
|
12826
|
+
const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + SCHEDULE_ACTIVATION_CANDLE_SKIP;
|
|
12479
12827
|
let candles;
|
|
12480
12828
|
try {
|
|
12481
12829
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
|
|
@@ -12613,9 +12961,9 @@ class BacktestLogicPrivateService {
|
|
|
12613
12961
|
// КРИТИЧНО: Получаем свечи включая буфер для VWAP
|
|
12614
12962
|
// Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
|
|
12615
12963
|
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
12616
|
-
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT -
|
|
12964
|
+
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - ACTIVE_CANDLE_INCLUDED;
|
|
12617
12965
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12618
|
-
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT
|
|
12966
|
+
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
12619
12967
|
let candles;
|
|
12620
12968
|
try {
|
|
12621
12969
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
@@ -24303,6 +24651,67 @@ class StrategyReportService {
|
|
|
24303
24651
|
walkerName: "",
|
|
24304
24652
|
});
|
|
24305
24653
|
};
|
|
24654
|
+
/**
|
|
24655
|
+
* Logs an activate-scheduled event when a scheduled signal is activated early.
|
|
24656
|
+
*
|
|
24657
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
24658
|
+
* @param currentPrice - Current market price at time of activation
|
|
24659
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
24660
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
24661
|
+
* @param timestamp - Timestamp from StrategyCommitContract (execution context time)
|
|
24662
|
+
* @param position - Trade direction: "long" or "short"
|
|
24663
|
+
* @param priceOpen - Entry price for the position
|
|
24664
|
+
* @param priceTakeProfit - Effective take profit price
|
|
24665
|
+
* @param priceStopLoss - Effective stop loss price
|
|
24666
|
+
* @param originalPriceTakeProfit - Original take profit before trailing
|
|
24667
|
+
* @param originalPriceStopLoss - Original stop loss before trailing
|
|
24668
|
+
* @param scheduledAt - Signal creation timestamp in milliseconds
|
|
24669
|
+
* @param pendingAt - Pending timestamp in milliseconds
|
|
24670
|
+
* @param activateId - Optional identifier for the activation reason
|
|
24671
|
+
*/
|
|
24672
|
+
this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
|
|
24673
|
+
this.loggerService.log("strategyReportService activateScheduled", {
|
|
24674
|
+
symbol,
|
|
24675
|
+
currentPrice,
|
|
24676
|
+
isBacktest,
|
|
24677
|
+
activateId,
|
|
24678
|
+
});
|
|
24679
|
+
if (!this.subscribe.hasValue()) {
|
|
24680
|
+
return;
|
|
24681
|
+
}
|
|
24682
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
24683
|
+
exchangeName: context.exchangeName,
|
|
24684
|
+
strategyName: context.strategyName,
|
|
24685
|
+
frameName: context.frameName,
|
|
24686
|
+
});
|
|
24687
|
+
if (!scheduledRow) {
|
|
24688
|
+
return;
|
|
24689
|
+
}
|
|
24690
|
+
const createdAt = new Date(timestamp).toISOString();
|
|
24691
|
+
await Report.writeData("strategy", {
|
|
24692
|
+
action: "activate-scheduled",
|
|
24693
|
+
activateId,
|
|
24694
|
+
currentPrice,
|
|
24695
|
+
symbol,
|
|
24696
|
+
timestamp,
|
|
24697
|
+
createdAt,
|
|
24698
|
+
position,
|
|
24699
|
+
priceOpen,
|
|
24700
|
+
priceTakeProfit,
|
|
24701
|
+
priceStopLoss,
|
|
24702
|
+
originalPriceTakeProfit,
|
|
24703
|
+
originalPriceStopLoss,
|
|
24704
|
+
scheduledAt,
|
|
24705
|
+
pendingAt,
|
|
24706
|
+
}, {
|
|
24707
|
+
signalId: scheduledRow.id,
|
|
24708
|
+
exchangeName: context.exchangeName,
|
|
24709
|
+
frameName: context.frameName,
|
|
24710
|
+
strategyName: context.strategyName,
|
|
24711
|
+
symbol,
|
|
24712
|
+
walkerName: "",
|
|
24713
|
+
});
|
|
24714
|
+
};
|
|
24306
24715
|
/**
|
|
24307
24716
|
* Initializes the service for event logging.
|
|
24308
24717
|
*
|
|
@@ -24362,7 +24771,14 @@ class StrategyReportService {
|
|
|
24362
24771
|
frameName: event.frameName,
|
|
24363
24772
|
strategyName: event.strategyName,
|
|
24364
24773
|
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
|
|
24365
|
-
const
|
|
24774
|
+
const unActivateScheduled = strategyCommitSubject
|
|
24775
|
+
.filter(({ action }) => action === "activate-scheduled")
|
|
24776
|
+
.connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
|
|
24777
|
+
exchangeName: event.exchangeName,
|
|
24778
|
+
frameName: event.frameName,
|
|
24779
|
+
strategyName: event.strategyName,
|
|
24780
|
+
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
|
|
24781
|
+
const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
|
|
24366
24782
|
return () => {
|
|
24367
24783
|
disposeFn();
|
|
24368
24784
|
this.subscribe.clear();
|
|
@@ -24493,6 +24909,7 @@ class ReportStorage {
|
|
|
24493
24909
|
trailingStopCount: 0,
|
|
24494
24910
|
trailingTakeCount: 0,
|
|
24495
24911
|
breakevenCount: 0,
|
|
24912
|
+
activateScheduledCount: 0,
|
|
24496
24913
|
};
|
|
24497
24914
|
}
|
|
24498
24915
|
return {
|
|
@@ -24505,6 +24922,7 @@ class ReportStorage {
|
|
|
24505
24922
|
trailingStopCount: this._eventList.filter(e => e.action === "trailing-stop").length,
|
|
24506
24923
|
trailingTakeCount: this._eventList.filter(e => e.action === "trailing-take").length,
|
|
24507
24924
|
breakevenCount: this._eventList.filter(e => e.action === "breakeven").length,
|
|
24925
|
+
activateScheduledCount: this._eventList.filter(e => e.action === "activate-scheduled").length,
|
|
24508
24926
|
};
|
|
24509
24927
|
}
|
|
24510
24928
|
/**
|
|
@@ -24553,6 +24971,7 @@ class ReportStorage {
|
|
|
24553
24971
|
`- Trailing stop: ${stats.trailingStopCount}`,
|
|
24554
24972
|
`- Trailing take: ${stats.trailingTakeCount}`,
|
|
24555
24973
|
`- Breakeven: ${stats.breakevenCount}`,
|
|
24974
|
+
`- Activate scheduled: ${stats.activateScheduledCount}`,
|
|
24556
24975
|
].join("\n");
|
|
24557
24976
|
}
|
|
24558
24977
|
/**
|
|
@@ -25019,6 +25438,66 @@ class StrategyMarkdownService {
|
|
|
25019
25438
|
pendingAt,
|
|
25020
25439
|
});
|
|
25021
25440
|
};
|
|
25441
|
+
/**
|
|
25442
|
+
* Records an activate-scheduled event when a scheduled signal is activated early.
|
|
25443
|
+
*
|
|
25444
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
25445
|
+
* @param currentPrice - Current market price at time of activation
|
|
25446
|
+
* @param isBacktest - Whether this is a backtest or live trading event
|
|
25447
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
25448
|
+
* @param timestamp - Timestamp from StrategyCommitContract (execution context time)
|
|
25449
|
+
* @param position - Trade direction: "long" or "short"
|
|
25450
|
+
* @param priceOpen - Entry price for the position
|
|
25451
|
+
* @param priceTakeProfit - Effective take profit price
|
|
25452
|
+
* @param priceStopLoss - Effective stop loss price
|
|
25453
|
+
* @param originalPriceTakeProfit - Original take profit before trailing
|
|
25454
|
+
* @param originalPriceStopLoss - Original stop loss before trailing
|
|
25455
|
+
* @param scheduledAt - Signal creation timestamp in milliseconds
|
|
25456
|
+
* @param pendingAt - Pending timestamp in milliseconds
|
|
25457
|
+
* @param activateId - Optional identifier for the activation reason
|
|
25458
|
+
*/
|
|
25459
|
+
this.activateScheduled = async (symbol, currentPrice, isBacktest, context, timestamp, position, priceOpen, priceTakeProfit, priceStopLoss, originalPriceTakeProfit, originalPriceStopLoss, scheduledAt, pendingAt, activateId) => {
|
|
25460
|
+
this.loggerService.log("strategyMarkdownService activateScheduled", {
|
|
25461
|
+
symbol,
|
|
25462
|
+
currentPrice,
|
|
25463
|
+
isBacktest,
|
|
25464
|
+
activateId,
|
|
25465
|
+
});
|
|
25466
|
+
if (!this.subscribe.hasValue()) {
|
|
25467
|
+
return;
|
|
25468
|
+
}
|
|
25469
|
+
const scheduledRow = await this.strategyCoreService.getScheduledSignal(isBacktest, symbol, {
|
|
25470
|
+
exchangeName: context.exchangeName,
|
|
25471
|
+
strategyName: context.strategyName,
|
|
25472
|
+
frameName: context.frameName,
|
|
25473
|
+
});
|
|
25474
|
+
if (!scheduledRow) {
|
|
25475
|
+
return;
|
|
25476
|
+
}
|
|
25477
|
+
const createdAt = new Date(timestamp).toISOString();
|
|
25478
|
+
const storage = this.getStorage(symbol, context.strategyName, context.exchangeName, context.frameName, isBacktest);
|
|
25479
|
+
storage.addEvent({
|
|
25480
|
+
timestamp,
|
|
25481
|
+
symbol,
|
|
25482
|
+
strategyName: context.strategyName,
|
|
25483
|
+
exchangeName: context.exchangeName,
|
|
25484
|
+
frameName: context.frameName,
|
|
25485
|
+
signalId: scheduledRow.id,
|
|
25486
|
+
action: "activate-scheduled",
|
|
25487
|
+
activateId,
|
|
25488
|
+
currentPrice,
|
|
25489
|
+
createdAt,
|
|
25490
|
+
backtest: isBacktest,
|
|
25491
|
+
position,
|
|
25492
|
+
priceOpen,
|
|
25493
|
+
priceTakeProfit,
|
|
25494
|
+
priceStopLoss,
|
|
25495
|
+
originalPriceTakeProfit,
|
|
25496
|
+
originalPriceStopLoss,
|
|
25497
|
+
scheduledAt,
|
|
25498
|
+
pendingAt,
|
|
25499
|
+
});
|
|
25500
|
+
};
|
|
25022
25501
|
/**
|
|
25023
25502
|
* Retrieves aggregated statistics from accumulated strategy events.
|
|
25024
25503
|
*
|
|
@@ -25192,7 +25671,14 @@ class StrategyMarkdownService {
|
|
|
25192
25671
|
frameName: event.frameName,
|
|
25193
25672
|
strategyName: event.strategyName,
|
|
25194
25673
|
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt));
|
|
25195
|
-
const
|
|
25674
|
+
const unActivateScheduled = strategyCommitSubject
|
|
25675
|
+
.filter(({ action }) => action === "activate-scheduled")
|
|
25676
|
+
.connect(async (event) => await this.activateScheduled(event.symbol, event.currentPrice, event.backtest, {
|
|
25677
|
+
exchangeName: event.exchangeName,
|
|
25678
|
+
frameName: event.frameName,
|
|
25679
|
+
strategyName: event.strategyName,
|
|
25680
|
+
}, event.timestamp, event.position, event.priceOpen, event.priceTakeProfit, event.priceStopLoss, event.originalPriceTakeProfit, event.originalPriceStopLoss, event.scheduledAt, event.pendingAt, event.activateId));
|
|
25681
|
+
const disposeFn = functoolsKit.compose(() => unCancelSchedule(), () => unClosePending(), () => unPartialProfit(), () => unPartialLoss(), () => unTrailingStop(), () => unTrailingTake(), () => unBreakeven(), () => unActivateScheduled());
|
|
25196
25682
|
return () => {
|
|
25197
25683
|
disposeFn();
|
|
25198
25684
|
this.subscribe.clear();
|
|
@@ -26118,6 +26604,7 @@ const PARTIAL_LOSS_METHOD_NAME = "strategy.commitPartialLoss";
|
|
|
26118
26604
|
const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
|
|
26119
26605
|
const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
|
|
26120
26606
|
const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
|
|
26607
|
+
const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
|
|
26121
26608
|
/**
|
|
26122
26609
|
* Cancels the scheduled signal without stopping the strategy.
|
|
26123
26610
|
*
|
|
@@ -26435,6 +26922,41 @@ async function commitBreakeven(symbol) {
|
|
|
26435
26922
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
26436
26923
|
return await bt.strategyCoreService.breakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
|
|
26437
26924
|
}
|
|
26925
|
+
/**
|
|
26926
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
26927
|
+
*
|
|
26928
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
26929
|
+
* happens on the next tick() when strategy detects the flag.
|
|
26930
|
+
*
|
|
26931
|
+
* Automatically detects backtest/live mode from execution context.
|
|
26932
|
+
*
|
|
26933
|
+
* @param symbol - Trading pair symbol
|
|
26934
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
26935
|
+
* @returns Promise that resolves when activation flag is set
|
|
26936
|
+
*
|
|
26937
|
+
* @example
|
|
26938
|
+
* ```typescript
|
|
26939
|
+
* import { commitActivateScheduled } from "backtest-kit";
|
|
26940
|
+
*
|
|
26941
|
+
* // Activate scheduled signal early with custom ID
|
|
26942
|
+
* await commitActivateScheduled("BTCUSDT", "manual-activate-001");
|
|
26943
|
+
* ```
|
|
26944
|
+
*/
|
|
26945
|
+
async function commitActivateScheduled(symbol, activateId) {
|
|
26946
|
+
bt.loggerService.info(ACTIVATE_SCHEDULED_METHOD_NAME, {
|
|
26947
|
+
symbol,
|
|
26948
|
+
activateId,
|
|
26949
|
+
});
|
|
26950
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26951
|
+
throw new Error("commitActivateScheduled requires an execution context");
|
|
26952
|
+
}
|
|
26953
|
+
if (!MethodContextService.hasContext()) {
|
|
26954
|
+
throw new Error("commitActivateScheduled requires a method context");
|
|
26955
|
+
}
|
|
26956
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
26957
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
26958
|
+
await bt.strategyCoreService.activateScheduled(isBacktest, symbol, { exchangeName, frameName, strategyName }, activateId);
|
|
26959
|
+
}
|
|
26438
26960
|
|
|
26439
26961
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
26440
26962
|
/**
|
|
@@ -28605,6 +29127,7 @@ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
|
28605
29127
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
28606
29128
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
|
|
28607
29129
|
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
|
|
29130
|
+
const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
|
|
28608
29131
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
28609
29132
|
/**
|
|
28610
29133
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -29461,6 +29984,46 @@ class BacktestUtils {
|
|
|
29461
29984
|
}
|
|
29462
29985
|
return await bt.strategyCoreService.breakeven(true, symbol, currentPrice, context);
|
|
29463
29986
|
};
|
|
29987
|
+
/**
|
|
29988
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
29989
|
+
*
|
|
29990
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
29991
|
+
* happens on the next tick() when strategy detects the flag.
|
|
29992
|
+
*
|
|
29993
|
+
* @param symbol - Trading pair symbol
|
|
29994
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
29995
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
29996
|
+
* @returns Promise that resolves when activation flag is set
|
|
29997
|
+
*
|
|
29998
|
+
* @example
|
|
29999
|
+
* ```typescript
|
|
30000
|
+
* // Activate scheduled signal early with custom ID
|
|
30001
|
+
* await Backtest.commitActivateScheduled("BTCUSDT", {
|
|
30002
|
+
* strategyName: "my-strategy",
|
|
30003
|
+
* exchangeName: "binance",
|
|
30004
|
+
* frameName: "1h"
|
|
30005
|
+
* }, "manual-activate-001");
|
|
30006
|
+
* ```
|
|
30007
|
+
*/
|
|
30008
|
+
this.commitActivateScheduled = async (symbol, context, activateId) => {
|
|
30009
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED, {
|
|
30010
|
+
symbol,
|
|
30011
|
+
context,
|
|
30012
|
+
activateId,
|
|
30013
|
+
});
|
|
30014
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
30015
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
30016
|
+
{
|
|
30017
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
30018
|
+
riskName &&
|
|
30019
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
30020
|
+
riskList &&
|
|
30021
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
30022
|
+
actions &&
|
|
30023
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
30024
|
+
}
|
|
30025
|
+
await bt.strategyCoreService.activateScheduled(true, symbol, context, activateId);
|
|
30026
|
+
};
|
|
29464
30027
|
/**
|
|
29465
30028
|
* Gets statistical data from all closed signals for a symbol-strategy pair.
|
|
29466
30029
|
*
|
|
@@ -29636,6 +30199,7 @@ const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
|
29636
30199
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
29637
30200
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
|
|
29638
30201
|
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
|
|
30202
|
+
const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
|
|
29639
30203
|
/**
|
|
29640
30204
|
* Internal task function that runs live trading and handles completion.
|
|
29641
30205
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -30460,6 +31024,46 @@ class LiveUtils {
|
|
|
30460
31024
|
frameName: "",
|
|
30461
31025
|
});
|
|
30462
31026
|
};
|
|
31027
|
+
/**
|
|
31028
|
+
* Activates a scheduled signal early without waiting for price to reach priceOpen.
|
|
31029
|
+
*
|
|
31030
|
+
* Sets the activation flag on the scheduled signal. The actual activation
|
|
31031
|
+
* happens on the next tick() when strategy detects the flag.
|
|
31032
|
+
*
|
|
31033
|
+
* @param symbol - Trading pair symbol
|
|
31034
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
31035
|
+
* @param activateId - Optional activation ID for tracking user-initiated activations
|
|
31036
|
+
* @returns Promise that resolves when activation flag is set
|
|
31037
|
+
*
|
|
31038
|
+
* @example
|
|
31039
|
+
* ```typescript
|
|
31040
|
+
* // Activate scheduled signal early with custom ID
|
|
31041
|
+
* await Live.commitActivateScheduled("BTCUSDT", {
|
|
31042
|
+
* strategyName: "my-strategy",
|
|
31043
|
+
* exchangeName: "binance"
|
|
31044
|
+
* }, "manual-activate-001");
|
|
31045
|
+
* ```
|
|
31046
|
+
*/
|
|
31047
|
+
this.commitActivateScheduled = async (symbol, context, activateId) => {
|
|
31048
|
+
bt.loggerService.info(LIVE_METHOD_NAME_ACTIVATE_SCHEDULED, {
|
|
31049
|
+
symbol,
|
|
31050
|
+
context,
|
|
31051
|
+
activateId,
|
|
31052
|
+
});
|
|
31053
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31054
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31055
|
+
{
|
|
31056
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
31057
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED);
|
|
31058
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
31059
|
+
actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_ACTIVATE_SCHEDULED));
|
|
31060
|
+
}
|
|
31061
|
+
await bt.strategyCoreService.activateScheduled(false, symbol, {
|
|
31062
|
+
strategyName: context.strategyName,
|
|
31063
|
+
exchangeName: context.exchangeName,
|
|
31064
|
+
frameName: "",
|
|
31065
|
+
}, activateId);
|
|
31066
|
+
};
|
|
30463
31067
|
/**
|
|
30464
31068
|
* Gets statistical data from all live trading events for a symbol-strategy pair.
|
|
30465
31069
|
*
|
|
@@ -33109,6 +33713,27 @@ const INTERVAL_MINUTES$1 = {
|
|
|
33109
33713
|
"6h": 360,
|
|
33110
33714
|
"8h": 480,
|
|
33111
33715
|
};
|
|
33716
|
+
/**
|
|
33717
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
33718
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
33719
|
+
*
|
|
33720
|
+
* Candle timestamp convention:
|
|
33721
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
33722
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
33723
|
+
*
|
|
33724
|
+
* Adapter contract:
|
|
33725
|
+
* - Adapter must return candles with timestamp = openTime
|
|
33726
|
+
* - First returned candle.timestamp must equal aligned since
|
|
33727
|
+
* - Adapter must return exactly `limit` candles
|
|
33728
|
+
*
|
|
33729
|
+
* @param timestamp - Timestamp in milliseconds
|
|
33730
|
+
* @param intervalMinutes - Interval in minutes
|
|
33731
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
33732
|
+
*/
|
|
33733
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
33734
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
33735
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
33736
|
+
};
|
|
33112
33737
|
/**
|
|
33113
33738
|
* Creates exchange instance with methods resolved once during construction.
|
|
33114
33739
|
* Applies default implementations where schema methods are not provided.
|
|
@@ -33130,25 +33755,24 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
33130
33755
|
};
|
|
33131
33756
|
/**
|
|
33132
33757
|
* Attempts to read candles from cache.
|
|
33133
|
-
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
33134
33758
|
*
|
|
33135
|
-
*
|
|
33136
|
-
*
|
|
33137
|
-
*
|
|
33138
|
-
* - Only fully closed candles within the exclusive range are returned
|
|
33759
|
+
* Cache lookup calculates expected timestamps:
|
|
33760
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33761
|
+
* Returns all candles if found, null if any missing.
|
|
33139
33762
|
*
|
|
33140
33763
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
33141
|
-
* @param sinceTimestamp -
|
|
33142
|
-
* @param untilTimestamp -
|
|
33764
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
33765
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
33143
33766
|
* @param exchangeName - Exchange name
|
|
33144
|
-
* @returns Cached candles array or null if cache miss
|
|
33767
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
33145
33768
|
*/
|
|
33146
33769
|
const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33147
|
-
// PersistCandleAdapter.readCandlesData
|
|
33148
|
-
//
|
|
33770
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
33771
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33772
|
+
// Returns all candles if found, null if any missing
|
|
33149
33773
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
33150
33774
|
// Return cached data only if we have exactly the requested limit
|
|
33151
|
-
if (cachedCandles
|
|
33775
|
+
if (cachedCandles?.length === dto.limit) {
|
|
33152
33776
|
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
33153
33777
|
return cachedCandles;
|
|
33154
33778
|
}
|
|
@@ -33170,11 +33794,12 @@ const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp,
|
|
|
33170
33794
|
/**
|
|
33171
33795
|
* Writes candles to cache with error handling.
|
|
33172
33796
|
*
|
|
33173
|
-
* The candles passed to this function
|
|
33174
|
-
* - candle.timestamp
|
|
33175
|
-
* -
|
|
33797
|
+
* The candles passed to this function should be validated:
|
|
33798
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
33799
|
+
* - Exact number of candles as requested (limit)
|
|
33800
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
33176
33801
|
*
|
|
33177
|
-
* @param candles - Array of candle data to cache
|
|
33802
|
+
* @param candles - Array of validated candle data to cache
|
|
33178
33803
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
33179
33804
|
* @param exchangeName - Exchange name
|
|
33180
33805
|
*/
|
|
@@ -33245,14 +33870,18 @@ class ExchangeInstance {
|
|
|
33245
33870
|
});
|
|
33246
33871
|
const getCandles = this._methods.getCandles;
|
|
33247
33872
|
const step = INTERVAL_MINUTES$1[interval];
|
|
33248
|
-
|
|
33249
|
-
|
|
33250
|
-
throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
|
|
33873
|
+
if (!step) {
|
|
33874
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
33251
33875
|
}
|
|
33876
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33877
|
+
// Align when down to interval boundary
|
|
33252
33878
|
const when = await GET_TIMESTAMP_FN();
|
|
33253
|
-
const
|
|
33254
|
-
const
|
|
33255
|
-
|
|
33879
|
+
const whenTimestamp = when.getTime();
|
|
33880
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
33881
|
+
// Calculate since: go back limit candles from aligned when
|
|
33882
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
33883
|
+
const since = new Date(sinceTimestamp);
|
|
33884
|
+
const untilTimestamp = alignedWhen;
|
|
33256
33885
|
// Try to read from cache first
|
|
33257
33886
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33258
33887
|
if (cachedCandles !== null) {
|
|
@@ -33271,7 +33900,7 @@ class ExchangeInstance {
|
|
|
33271
33900
|
remaining -= chunkLimit;
|
|
33272
33901
|
if (remaining > 0) {
|
|
33273
33902
|
// Move currentSince forward by the number of candles fetched
|
|
33274
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33903
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33275
33904
|
}
|
|
33276
33905
|
}
|
|
33277
33906
|
}
|
|
@@ -33279,27 +33908,25 @@ class ExchangeInstance {
|
|
|
33279
33908
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33280
33909
|
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
33281
33910
|
}
|
|
33282
|
-
// Filter candles to strictly match the requested range
|
|
33283
|
-
const whenTimestamp = when.getTime();
|
|
33284
|
-
const stepMs = step * MS_PER_MINUTE;
|
|
33285
|
-
const filteredData = allData.filter((candle) => {
|
|
33286
|
-
// EXCLUSIVE boundaries:
|
|
33287
|
-
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
33288
|
-
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
33289
|
-
if (candle.timestamp <= sinceTimestamp) {
|
|
33290
|
-
return false;
|
|
33291
|
-
}
|
|
33292
|
-
// Check against current time (when)
|
|
33293
|
-
// Only allow candles that have fully CLOSED before "when"
|
|
33294
|
-
return candle.timestamp + stepMs < whenTimestamp;
|
|
33295
|
-
});
|
|
33296
33911
|
// Apply distinct by timestamp to remove duplicates
|
|
33297
|
-
const uniqueData = Array.from(new Map(
|
|
33298
|
-
if (
|
|
33299
|
-
bt.loggerService.warn(`ExchangeInstance Removed ${
|
|
33912
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33913
|
+
if (allData.length !== uniqueData.length) {
|
|
33914
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33915
|
+
}
|
|
33916
|
+
// Validate adapter returned data
|
|
33917
|
+
if (uniqueData.length === 0) {
|
|
33918
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
33919
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33300
33920
|
}
|
|
33301
|
-
if (uniqueData.
|
|
33302
|
-
|
|
33921
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33922
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
33923
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33924
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33925
|
+
}
|
|
33926
|
+
if (uniqueData.length !== limit) {
|
|
33927
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
33928
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
33929
|
+
`Adapter must return exact number of candles requested.`);
|
|
33303
33930
|
}
|
|
33304
33931
|
// Write to cache after successful fetch
|
|
33305
33932
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
@@ -33470,10 +34097,11 @@ class ExchangeInstance {
|
|
|
33470
34097
|
if (!step) {
|
|
33471
34098
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33472
34099
|
}
|
|
34100
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33473
34101
|
const when = await GET_TIMESTAMP_FN();
|
|
33474
34102
|
const nowTimestamp = when.getTime();
|
|
34103
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
33475
34104
|
let sinceTimestamp;
|
|
33476
|
-
let untilTimestamp;
|
|
33477
34105
|
let calculatedLimit;
|
|
33478
34106
|
// Case 1: all three parameters provided
|
|
33479
34107
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -33483,8 +34111,8 @@ class ExchangeInstance {
|
|
|
33483
34111
|
if (eDate > nowTimestamp) {
|
|
33484
34112
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33485
34113
|
}
|
|
33486
|
-
|
|
33487
|
-
|
|
34114
|
+
// Align sDate down to interval boundary
|
|
34115
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33488
34116
|
calculatedLimit = limit;
|
|
33489
34117
|
}
|
|
33490
34118
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -33495,9 +34123,10 @@ class ExchangeInstance {
|
|
|
33495
34123
|
if (eDate > nowTimestamp) {
|
|
33496
34124
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33497
34125
|
}
|
|
33498
|
-
|
|
33499
|
-
|
|
33500
|
-
|
|
34126
|
+
// Align sDate down to interval boundary
|
|
34127
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
34128
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
34129
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
33501
34130
|
if (calculatedLimit <= 0) {
|
|
33502
34131
|
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
33503
34132
|
}
|
|
@@ -33507,23 +34136,24 @@ class ExchangeInstance {
|
|
|
33507
34136
|
if (eDate > nowTimestamp) {
|
|
33508
34137
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33509
34138
|
}
|
|
33510
|
-
|
|
33511
|
-
|
|
34139
|
+
// Align eDate down and calculate sinceTimestamp
|
|
34140
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
34141
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
33512
34142
|
calculatedLimit = limit;
|
|
33513
34143
|
}
|
|
33514
34144
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
33515
34145
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
33516
|
-
|
|
33517
|
-
|
|
33518
|
-
|
|
33519
|
-
|
|
34146
|
+
// Align sDate down to interval boundary
|
|
34147
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
34148
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
34149
|
+
if (endTimestamp > nowTimestamp) {
|
|
34150
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33520
34151
|
}
|
|
33521
34152
|
calculatedLimit = limit;
|
|
33522
34153
|
}
|
|
33523
34154
|
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
33524
34155
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
33525
|
-
|
|
33526
|
-
sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
|
|
34156
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
33527
34157
|
calculatedLimit = limit;
|
|
33528
34158
|
}
|
|
33529
34159
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -33533,6 +34163,7 @@ class ExchangeInstance {
|
|
|
33533
34163
|
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
33534
34164
|
}
|
|
33535
34165
|
// Try to read from cache first
|
|
34166
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
33536
34167
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33537
34168
|
if (cachedCandles !== null) {
|
|
33538
34169
|
return cachedCandles;
|
|
@@ -33551,25 +34182,32 @@ class ExchangeInstance {
|
|
|
33551
34182
|
allData.push(...chunkData);
|
|
33552
34183
|
remaining -= chunkLimit;
|
|
33553
34184
|
if (remaining > 0) {
|
|
33554
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
34185
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33555
34186
|
}
|
|
33556
34187
|
}
|
|
33557
34188
|
}
|
|
33558
34189
|
else {
|
|
33559
34190
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33560
34191
|
}
|
|
33561
|
-
// Filter candles to strictly match the requested range
|
|
33562
|
-
// Only include candles that have fully CLOSED before untilTimestamp
|
|
33563
|
-
const stepMs = step * MS_PER_MINUTE;
|
|
33564
|
-
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
33565
|
-
candle.timestamp + stepMs < untilTimestamp);
|
|
33566
34192
|
// Apply distinct by timestamp to remove duplicates
|
|
33567
|
-
const uniqueData = Array.from(new Map(
|
|
33568
|
-
if (
|
|
33569
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${
|
|
34193
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
34194
|
+
if (allData.length !== uniqueData.length) {
|
|
34195
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
34196
|
+
}
|
|
34197
|
+
// Validate adapter returned data
|
|
34198
|
+
if (uniqueData.length === 0) {
|
|
34199
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
34200
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33570
34201
|
}
|
|
33571
|
-
if (uniqueData.
|
|
33572
|
-
|
|
34202
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
34203
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
34204
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
34205
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
34206
|
+
}
|
|
34207
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
34208
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
34209
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
34210
|
+
`Adapter must return exact number of candles requested.`);
|
|
33573
34211
|
}
|
|
33574
34212
|
// Write to cache after successful fetch
|
|
33575
34213
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
@@ -34361,6 +34999,29 @@ class NotificationInstance {
|
|
|
34361
34999
|
createdAt: data.timestamp,
|
|
34362
35000
|
});
|
|
34363
35001
|
}
|
|
35002
|
+
else if (data.action === "activate-scheduled") {
|
|
35003
|
+
this._addNotification({
|
|
35004
|
+
type: "activate_scheduled.commit",
|
|
35005
|
+
id: CREATE_KEY_FN(),
|
|
35006
|
+
timestamp: data.timestamp,
|
|
35007
|
+
backtest: data.backtest,
|
|
35008
|
+
symbol: data.symbol,
|
|
35009
|
+
strategyName: data.strategyName,
|
|
35010
|
+
exchangeName: data.exchangeName,
|
|
35011
|
+
signalId: data.signalId,
|
|
35012
|
+
activateId: data.activateId,
|
|
35013
|
+
currentPrice: data.currentPrice,
|
|
35014
|
+
position: data.position,
|
|
35015
|
+
priceOpen: data.priceOpen,
|
|
35016
|
+
priceTakeProfit: data.priceTakeProfit,
|
|
35017
|
+
priceStopLoss: data.priceStopLoss,
|
|
35018
|
+
originalPriceTakeProfit: data.originalPriceTakeProfit,
|
|
35019
|
+
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
35020
|
+
scheduledAt: data.scheduledAt,
|
|
35021
|
+
pendingAt: data.pendingAt,
|
|
35022
|
+
createdAt: data.timestamp,
|
|
35023
|
+
});
|
|
35024
|
+
}
|
|
34364
35025
|
};
|
|
34365
35026
|
/**
|
|
34366
35027
|
* Processes risk rejection events.
|
|
@@ -35206,6 +35867,7 @@ exports.addRiskSchema = addRiskSchema;
|
|
|
35206
35867
|
exports.addSizingSchema = addSizingSchema;
|
|
35207
35868
|
exports.addStrategySchema = addStrategySchema;
|
|
35208
35869
|
exports.addWalkerSchema = addWalkerSchema;
|
|
35870
|
+
exports.commitActivateScheduled = commitActivateScheduled;
|
|
35209
35871
|
exports.commitBreakeven = commitBreakeven;
|
|
35210
35872
|
exports.commitCancelScheduled = commitCancelScheduled;
|
|
35211
35873
|
exports.commitClosePending = commitClosePending;
|