backtest-kit 12.8.0 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +1997 -1997
- package/build/index.cjs +862 -189
- package/build/index.mjs +859 -190
- package/package.json +86 -86
- package/types.d.ts +435 -2
package/build/index.cjs
CHANGED
|
@@ -1001,6 +1001,12 @@ const PERSIST_SCHEDULE_UTILS_METHOD_NAME_WRITE_DATA = "PersistScheduleUtils.writ
|
|
|
1001
1001
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_JSON = "PersistScheduleUtils.useJson";
|
|
1002
1002
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_USE_DUMMY = "PersistScheduleUtils.useDummy";
|
|
1003
1003
|
const PERSIST_SCHEDULE_UTILS_METHOD_NAME_CLEAR = "PersistScheduleUtils.clear";
|
|
1004
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER = "PersistStrategyUtils.usePersistStrategyAdapter";
|
|
1005
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA = "PersistStrategyUtils.readStrategyData";
|
|
1006
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA = "PersistStrategyUtils.writeStrategyData";
|
|
1007
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON = "PersistStrategyUtils.useJson";
|
|
1008
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY = "PersistStrategyUtils.useDummy";
|
|
1009
|
+
const PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR = "PersistStrategyUtils.clear";
|
|
1004
1010
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_USE_PERSIST_PARTIAL_ADAPTER = "PersistPartialUtils.usePersistPartialAdapter";
|
|
1005
1011
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_READ_DATA = "PersistPartialUtils.readPartialData";
|
|
1006
1012
|
const PERSIST_PARTIAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistPartialUtils.writePartialData";
|
|
@@ -1864,6 +1870,212 @@ class PersistScheduleUtils {
|
|
|
1864
1870
|
* ```
|
|
1865
1871
|
*/
|
|
1866
1872
|
const PersistScheduleAdapter = new PersistScheduleUtils();
|
|
1873
|
+
/**
|
|
1874
|
+
* Default file-based implementation of IPersistStrategyInstance.
|
|
1875
|
+
*
|
|
1876
|
+
* Features:
|
|
1877
|
+
* - Wraps PersistBase for atomic JSON writes
|
|
1878
|
+
* - Uses fixed entity ID "strategy" within a per-context PersistBase
|
|
1879
|
+
* - Crash-safe via atomic writes
|
|
1880
|
+
*
|
|
1881
|
+
* @example
|
|
1882
|
+
* ```typescript
|
|
1883
|
+
* const instance = new PersistStrategyInstance("BTCUSDT", "my-strategy", "binance");
|
|
1884
|
+
* await instance.waitForInit(true);
|
|
1885
|
+
* await instance.writeStrategyData(snapshot);
|
|
1886
|
+
* const restored = await instance.readStrategyData();
|
|
1887
|
+
* ```
|
|
1888
|
+
*/
|
|
1889
|
+
class PersistStrategyInstance {
|
|
1890
|
+
/**
|
|
1891
|
+
* Creates new deferred strategy state persistence instance.
|
|
1892
|
+
*
|
|
1893
|
+
* @param symbol - Trading pair symbol
|
|
1894
|
+
* @param strategyName - Strategy identifier
|
|
1895
|
+
* @param exchangeName - Exchange identifier
|
|
1896
|
+
*/
|
|
1897
|
+
constructor(symbol, strategyName, exchangeName) {
|
|
1898
|
+
this.symbol = symbol;
|
|
1899
|
+
this.strategyName = strategyName;
|
|
1900
|
+
this.exchangeName = exchangeName;
|
|
1901
|
+
this._storage = new PersistBase(`${symbol}_${strategyName}_${exchangeName}`, `./dump/data/strategy/`);
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Initializes the underlying PersistBase storage.
|
|
1905
|
+
*
|
|
1906
|
+
* @param initial - Whether this is the first initialization
|
|
1907
|
+
* @returns Promise that resolves when initialization is complete
|
|
1908
|
+
*/
|
|
1909
|
+
async waitForInit(initial) {
|
|
1910
|
+
await this._storage.waitForInit(initial);
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Reads the persisted strategy state snapshot using the fixed STORAGE_KEY.
|
|
1914
|
+
*
|
|
1915
|
+
* @returns Promise resolving to strategy state snapshot or null if not found
|
|
1916
|
+
*/
|
|
1917
|
+
async readStrategyData() {
|
|
1918
|
+
if (await this._storage.hasValue(PersistStrategyInstance.STORAGE_KEY)) {
|
|
1919
|
+
const strategyData = await this._storage.readValue(PersistStrategyInstance.STORAGE_KEY);
|
|
1920
|
+
// JSON serializes Infinity as null, so an eternal-hold signal
|
|
1921
|
+
// (minuteEstimatedTime: Infinity) reads back as null — restore it.
|
|
1922
|
+
for (const signal of [strategyData.closedSignal, strategyData.cancelledSignal, strategyData.activatedSignal]) {
|
|
1923
|
+
if (signal && signal.minuteEstimatedTime == null) {
|
|
1924
|
+
signal.minuteEstimatedTime = Infinity;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return strategyData;
|
|
1928
|
+
}
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Writes the strategy state snapshot (or null to clear) using the fixed STORAGE_KEY.
|
|
1933
|
+
*
|
|
1934
|
+
* @param row - Strategy state snapshot to persist, or null to clear
|
|
1935
|
+
* @returns Promise that resolves when write is complete
|
|
1936
|
+
*/
|
|
1937
|
+
async writeStrategyData(row) {
|
|
1938
|
+
await this._storage.writeValue(PersistStrategyInstance.STORAGE_KEY, row);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/** Fixed entity key for storing the strategy state snapshot */
|
|
1942
|
+
PersistStrategyInstance.STORAGE_KEY = "strategy";
|
|
1943
|
+
/**
|
|
1944
|
+
* No-op IPersistStrategyInstance implementation used by PersistStrategyUtils.useDummy().
|
|
1945
|
+
* All reads return null, all writes are discarded.
|
|
1946
|
+
*/
|
|
1947
|
+
class PersistStrategyDummyInstance {
|
|
1948
|
+
/**
|
|
1949
|
+
* No-op constructor.
|
|
1950
|
+
* Context arguments are accepted to satisfy TPersistStrategyInstanceCtor.
|
|
1951
|
+
*/
|
|
1952
|
+
constructor(_symbol, _strategyName, _exchangeName) { }
|
|
1953
|
+
/**
|
|
1954
|
+
* No-op initialization.
|
|
1955
|
+
* @returns Promise that resolves immediately
|
|
1956
|
+
*/
|
|
1957
|
+
async waitForInit(_initial) { }
|
|
1958
|
+
/**
|
|
1959
|
+
* Always returns null (no persisted strategy state).
|
|
1960
|
+
* @returns Promise resolving to null
|
|
1961
|
+
*/
|
|
1962
|
+
async readStrategyData() { return null; }
|
|
1963
|
+
/**
|
|
1964
|
+
* No-op write (discards strategy state).
|
|
1965
|
+
* @returns Promise that resolves immediately
|
|
1966
|
+
*/
|
|
1967
|
+
async writeStrategyData(_row) { }
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Utility class for managing deferred strategy state persistence.
|
|
1971
|
+
*
|
|
1972
|
+
* Features:
|
|
1973
|
+
* - Memoized storage instances per strategy
|
|
1974
|
+
* - Custom adapter support
|
|
1975
|
+
* - Atomic read/write operations for the deferred state snapshot
|
|
1976
|
+
* - Crash-safe in-flight broker operation state management
|
|
1977
|
+
*
|
|
1978
|
+
* Used by ClientStrategy for live mode persistence of the commit queue and deferred
|
|
1979
|
+
* user actions (_commitQueue, _closedSignal, _cancelledSignal, _activatedSignal).
|
|
1980
|
+
*/
|
|
1981
|
+
class PersistStrategyUtils {
|
|
1982
|
+
constructor() {
|
|
1983
|
+
/**
|
|
1984
|
+
* Constructor used to create per-context strategy state instances.
|
|
1985
|
+
* Replaceable via usePersistStrategyAdapter() / useJson() / useDummy().
|
|
1986
|
+
*/
|
|
1987
|
+
this.PersistStrategyInstanceCtor = PersistStrategyInstance;
|
|
1988
|
+
/**
|
|
1989
|
+
* Memoized factory creating one IPersistStrategyInstance per (symbol, strategy, exchange) triple.
|
|
1990
|
+
*/
|
|
1991
|
+
this.getStrategyStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName]) => `${symbol}:${strategyName}:${exchangeName}`, (symbol, strategyName, exchangeName) => Reflect.construct(this.PersistStrategyInstanceCtor, [symbol, strategyName, exchangeName]));
|
|
1992
|
+
/**
|
|
1993
|
+
* Reads persisted deferred strategy state for the given context.
|
|
1994
|
+
* Lazily initializes the instance on first access.
|
|
1995
|
+
*
|
|
1996
|
+
* @param symbol - Trading pair symbol
|
|
1997
|
+
* @param strategyName - Strategy identifier
|
|
1998
|
+
* @param exchangeName - Exchange identifier
|
|
1999
|
+
* @returns Promise resolving to strategy state snapshot or null if none persisted
|
|
2000
|
+
*/
|
|
2001
|
+
this.readStrategyData = async (symbol, strategyName, exchangeName) => {
|
|
2002
|
+
LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_READ_DATA);
|
|
2003
|
+
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
2004
|
+
const isInitial = !this.getStrategyStorage.has(key);
|
|
2005
|
+
const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
|
|
2006
|
+
await instance.waitForInit(isInitial);
|
|
2007
|
+
return instance.readStrategyData();
|
|
2008
|
+
};
|
|
2009
|
+
/**
|
|
2010
|
+
* Writes deferred strategy state (or null to clear) for the given context.
|
|
2011
|
+
* Lazily initializes the instance on first access.
|
|
2012
|
+
*
|
|
2013
|
+
* @param strategyRow - Strategy state snapshot to persist, or null to clear
|
|
2014
|
+
* @param symbol - Trading pair symbol
|
|
2015
|
+
* @param strategyName - Strategy identifier
|
|
2016
|
+
* @param exchangeName - Exchange identifier
|
|
2017
|
+
* @returns Promise that resolves when write is complete
|
|
2018
|
+
*/
|
|
2019
|
+
this.writeStrategyData = async (strategyRow, symbol, strategyName, exchangeName) => {
|
|
2020
|
+
LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_WRITE_DATA);
|
|
2021
|
+
const key = `${symbol}:${strategyName}:${exchangeName}`;
|
|
2022
|
+
const isInitial = !this.getStrategyStorage.has(key);
|
|
2023
|
+
const instance = this.getStrategyStorage(symbol, strategyName, exchangeName);
|
|
2024
|
+
await instance.waitForInit(isInitial);
|
|
2025
|
+
return instance.writeStrategyData(strategyRow);
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Registers a custom IPersistStrategyInstance constructor.
|
|
2030
|
+
* Clears the memoization cache so subsequent calls use the new adapter.
|
|
2031
|
+
*
|
|
2032
|
+
* @param Ctor - Custom IPersistStrategyInstance constructor
|
|
2033
|
+
*/
|
|
2034
|
+
usePersistStrategyAdapter(Ctor) {
|
|
2035
|
+
LOGGER_SERVICE$9.info(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_PERSIST_STRATEGY_ADAPTER);
|
|
2036
|
+
this.PersistStrategyInstanceCtor = Ctor;
|
|
2037
|
+
this.getStrategyStorage.clear();
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Clears the memoized instance cache.
|
|
2041
|
+
* Call when process.cwd() changes between strategy iterations.
|
|
2042
|
+
*/
|
|
2043
|
+
clear() {
|
|
2044
|
+
LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_CLEAR);
|
|
2045
|
+
this.getStrategyStorage.clear();
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Switches to the default file-based PersistStrategyInstance.
|
|
2049
|
+
*/
|
|
2050
|
+
useJson() {
|
|
2051
|
+
LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_JSON);
|
|
2052
|
+
this.usePersistStrategyAdapter(PersistStrategyInstance);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Switches to PersistStrategyDummyInstance (all operations are no-ops).
|
|
2056
|
+
*/
|
|
2057
|
+
useDummy() {
|
|
2058
|
+
LOGGER_SERVICE$9.log(PERSIST_STRATEGY_UTILS_METHOD_NAME_USE_DUMMY);
|
|
2059
|
+
this.usePersistStrategyAdapter(PersistStrategyDummyInstance);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Global singleton instance of PersistStrategyUtils.
|
|
2064
|
+
* Used by ClientStrategy for deferred strategy state persistence.
|
|
2065
|
+
*
|
|
2066
|
+
* @example
|
|
2067
|
+
* ```typescript
|
|
2068
|
+
* // Custom adapter
|
|
2069
|
+
* PersistStrategyAdapter.usePersistStrategyAdapter(RedisPersist);
|
|
2070
|
+
*
|
|
2071
|
+
* // Read strategy state
|
|
2072
|
+
* const state = await PersistStrategyAdapter.readStrategyData("BTCUSDT", "my-strategy", "binance");
|
|
2073
|
+
*
|
|
2074
|
+
* // Write strategy state
|
|
2075
|
+
* await PersistStrategyAdapter.writeStrategyData(state, "BTCUSDT", "my-strategy", "binance");
|
|
2076
|
+
* ```
|
|
2077
|
+
*/
|
|
2078
|
+
const PersistStrategyAdapter = new PersistStrategyUtils();
|
|
1867
2079
|
/**
|
|
1868
2080
|
* Default file-based implementation of IPersistPartialInstance.
|
|
1869
2081
|
*
|
|
@@ -6625,6 +6837,119 @@ const validateScheduledSignal = (signal, currentPrice) => {
|
|
|
6625
6837
|
}
|
|
6626
6838
|
};
|
|
6627
6839
|
|
|
6840
|
+
/**
|
|
6841
|
+
* Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
|
|
6842
|
+
*
|
|
6843
|
+
* When priceOpen is provided:
|
|
6844
|
+
* - If currentPrice already reached priceOpen (shouldActivateImmediately) →
|
|
6845
|
+
* validates as pending: currentPrice must be between SL and TP
|
|
6846
|
+
* - Otherwise → validates as scheduled: priceOpen must be between SL and TP
|
|
6847
|
+
*
|
|
6848
|
+
* When priceOpen is absent:
|
|
6849
|
+
* - Validates as pending: currentPrice must be between SL and TP
|
|
6850
|
+
*
|
|
6851
|
+
* Checks:
|
|
6852
|
+
* - currentPrice is a finite positive number
|
|
6853
|
+
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
6854
|
+
* - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
|
|
6855
|
+
*
|
|
6856
|
+
* @param signal - Signal DTO returned by getSignal
|
|
6857
|
+
* @param currentPrice - Current market price at the moment of signal creation
|
|
6858
|
+
* @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
|
|
6859
|
+
*/
|
|
6860
|
+
const validateSignal = (signal, currentPrice) => {
|
|
6861
|
+
const errors = [];
|
|
6862
|
+
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
6863
|
+
{
|
|
6864
|
+
if (typeof currentPrice !== "number") {
|
|
6865
|
+
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
6866
|
+
}
|
|
6867
|
+
if (!isFinite(currentPrice)) {
|
|
6868
|
+
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
6869
|
+
}
|
|
6870
|
+
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
6871
|
+
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
if (errors.length > 0) {
|
|
6875
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6876
|
+
return false;
|
|
6877
|
+
}
|
|
6878
|
+
try {
|
|
6879
|
+
validateCommonSignal(signal);
|
|
6880
|
+
}
|
|
6881
|
+
catch (error) {
|
|
6882
|
+
console.error(functoolsKit.getErrorMessage(error));
|
|
6883
|
+
return false;
|
|
6884
|
+
}
|
|
6885
|
+
// Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
|
|
6886
|
+
// - нет priceOpen → pending (открывается по currentPrice)
|
|
6887
|
+
// - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
|
|
6888
|
+
// - priceOpen задан и ещё не достигнут → scheduled
|
|
6889
|
+
const hasPriceOpen = signal.priceOpen !== undefined;
|
|
6890
|
+
const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
|
|
6891
|
+
(signal.position === "short" && currentPrice >= signal.priceOpen));
|
|
6892
|
+
const isScheduled = hasPriceOpen && !shouldActivateImmediately;
|
|
6893
|
+
if (isScheduled) {
|
|
6894
|
+
// Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
|
|
6895
|
+
if (signal.position === "long") {
|
|
6896
|
+
if (isFinite(signal.priceOpen)) {
|
|
6897
|
+
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
6898
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6899
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
6900
|
+
}
|
|
6901
|
+
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
6902
|
+
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6903
|
+
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
6904
|
+
}
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
if (signal.position === "short") {
|
|
6908
|
+
if (isFinite(signal.priceOpen)) {
|
|
6909
|
+
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
6910
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6911
|
+
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
6912
|
+
}
|
|
6913
|
+
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
6914
|
+
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6915
|
+
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
else {
|
|
6921
|
+
// Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
|
|
6922
|
+
if (signal.position === "long") {
|
|
6923
|
+
if (isFinite(currentPrice)) {
|
|
6924
|
+
if (currentPrice <= signal.priceStopLoss) {
|
|
6925
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6926
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
6927
|
+
}
|
|
6928
|
+
if (currentPrice >= signal.priceTakeProfit) {
|
|
6929
|
+
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6930
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
if (signal.position === "short") {
|
|
6935
|
+
if (isFinite(currentPrice)) {
|
|
6936
|
+
if (currentPrice >= signal.priceStopLoss) {
|
|
6937
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
6938
|
+
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
6939
|
+
}
|
|
6940
|
+
if (currentPrice <= signal.priceTakeProfit) {
|
|
6941
|
+
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
6942
|
+
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
if (errors.length > 0) {
|
|
6948
|
+
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
6949
|
+
}
|
|
6950
|
+
return !errors.length;
|
|
6951
|
+
};
|
|
6952
|
+
|
|
6628
6953
|
const INTERVAL_MINUTES$8 = {
|
|
6629
6954
|
"1m": 1,
|
|
6630
6955
|
"3m": 3,
|
|
@@ -6791,6 +7116,9 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, currentPrice, timestamp) => {
|
|
|
6791
7116
|
{
|
|
6792
7117
|
self._commitQueue = [];
|
|
6793
7118
|
}
|
|
7119
|
+
// Persist the now-empty queue so a crash after draining does not replay commits
|
|
7120
|
+
// that were already forwarded to the broker on the next restart.
|
|
7121
|
+
await PERSIST_STRATEGY_FN(self);
|
|
6794
7122
|
if (!self._pendingSignal) {
|
|
6795
7123
|
return;
|
|
6796
7124
|
}
|
|
@@ -7088,11 +7416,27 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
|
|
|
7088
7416
|
self._lastSignalTimestamp = alignedTime;
|
|
7089
7417
|
}
|
|
7090
7418
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7419
|
+
// PRIORITY: a user-queued createPending/createScheduled DTO (set out of async-hooks
|
|
7420
|
+
// context) takes precedence over params.getSignal. When present it is consumed once
|
|
7421
|
+
// here — the slot is cleared and the snapshot rewritten — and then flows through the
|
|
7422
|
+
// exact same pipeline (risk check, priceOpen branching, onSignalSync on open) as a
|
|
7423
|
+
// signal returned by getSignal would.
|
|
7424
|
+
let signal;
|
|
7425
|
+
{
|
|
7426
|
+
if (!self._userSignal) {
|
|
7427
|
+
const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
|
|
7428
|
+
signal = await Promise.race([
|
|
7429
|
+
self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when, currentPrice),
|
|
7430
|
+
functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL$1),
|
|
7431
|
+
]);
|
|
7432
|
+
}
|
|
7433
|
+
if (self._userSignal) {
|
|
7434
|
+
signal = self._userSignal;
|
|
7435
|
+
self._userSignal = null;
|
|
7436
|
+
await PERSIST_STRATEGY_FN(self);
|
|
7437
|
+
}
|
|
7438
|
+
self._userSignal = null;
|
|
7439
|
+
}
|
|
7096
7440
|
if (typeof signal === "symbol") {
|
|
7097
7441
|
throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
|
|
7098
7442
|
}
|
|
@@ -7247,6 +7591,21 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7247
7591
|
self._lastPendingId = recentSignal.id;
|
|
7248
7592
|
}
|
|
7249
7593
|
}
|
|
7594
|
+
// Read deferred strategy state (commit queue + deferred user actions) so any
|
|
7595
|
+
// confirmed-but-not-yet-forwarded broker operation survives a live crash and is
|
|
7596
|
+
// re-drained on the next tick. Read here, before the pending restore which may
|
|
7597
|
+
// early-return on exchange/strategy mismatch.
|
|
7598
|
+
const strategyData = await PersistStrategyAdapter.readStrategyData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7599
|
+
if (strategyData) {
|
|
7600
|
+
// Deferred user actions are restored unconditionally: a deferred close belongs to
|
|
7601
|
+
// an already-cleared _pendingSignal, and cancel/activate belong to the scheduled
|
|
7602
|
+
// signal — none of them are tied to the currently-pending signal's id. A queued
|
|
7603
|
+
// createSignal is a not-yet-consumed signal source and likewise stands on its own.
|
|
7604
|
+
self._userSignal = strategyData.createSignal;
|
|
7605
|
+
self._closedSignal = strategyData.closedSignal;
|
|
7606
|
+
self._cancelledSignal = strategyData.cancelledSignal;
|
|
7607
|
+
self._activatedSignal = strategyData.activatedSignal;
|
|
7608
|
+
}
|
|
7250
7609
|
// Restore pending signal
|
|
7251
7610
|
const pendingSignal = await PersistSignalAdapter.readSignalData(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7252
7611
|
if (pendingSignal) {
|
|
@@ -7263,6 +7622,14 @@ const WAIT_FOR_INIT_FN$4 = async (self) => {
|
|
|
7263
7622
|
pendingSignal.minuteEstimatedTime = Infinity;
|
|
7264
7623
|
}
|
|
7265
7624
|
self._pendingSignal = pendingSignal;
|
|
7625
|
+
// Restore the commit queue only if the snapshot belongs to this exact pending
|
|
7626
|
+
// signal (pendingSignalId === restored id). The queue holds confirmed-but-not-yet
|
|
7627
|
+
// forwarded broker ops (average-buy / partial-* / trailing-* / breakeven) that are
|
|
7628
|
+
// always tied to the active position; a mismatch means the snapshot is stale and
|
|
7629
|
+
// the queue is dropped to avoid replaying ops against the wrong position.
|
|
7630
|
+
if (strategyData && strategyData.pendingSignalId === pendingSignal.id) {
|
|
7631
|
+
self._commitQueue = strategyData.commitQueue ?? [];
|
|
7632
|
+
}
|
|
7266
7633
|
// Call onActive callback for restored signal
|
|
7267
7634
|
const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
|
|
7268
7635
|
const currentTime = self.params.execution.context.when.getTime();
|
|
@@ -7296,6 +7663,30 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
|
|
|
7296
7663
|
self.params.logger.debug("ClientStrategy dispose");
|
|
7297
7664
|
await self.params.onDispose(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, self.params.execution.context.backtest);
|
|
7298
7665
|
};
|
|
7666
|
+
/**
|
|
7667
|
+
* Persists the deferred strategy state snapshot (commit queue + deferred user actions)
|
|
7668
|
+
* to disk in live mode.
|
|
7669
|
+
*
|
|
7670
|
+
* These fields carry confirmed-but-not-yet-forwarded broker operations and survive the
|
|
7671
|
+
* gap between ticks (drained at the start of the next tick). Without persistence a live
|
|
7672
|
+
* crash in that window silently loses the pending broker operation while _pendingSignal
|
|
7673
|
+
* (already mutated and saved) claims it happened. Skipped in backtest mode.
|
|
7674
|
+
*
|
|
7675
|
+
* @param self - ClientStrategy instance
|
|
7676
|
+
*/
|
|
7677
|
+
const PERSIST_STRATEGY_FN = async (self) => {
|
|
7678
|
+
if (self.params.backtest) {
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7681
|
+
await PersistStrategyAdapter.writeStrategyData({
|
|
7682
|
+
pendingSignalId: self._pendingSignal?.id ?? null,
|
|
7683
|
+
createSignal: self._userSignal,
|
|
7684
|
+
commitQueue: self._commitQueue,
|
|
7685
|
+
closedSignal: self._closedSignal,
|
|
7686
|
+
cancelledSignal: self._cancelledSignal,
|
|
7687
|
+
activatedSignal: self._activatedSignal,
|
|
7688
|
+
}, self.params.symbol, self.params.strategyName, self.params.exchangeName);
|
|
7689
|
+
};
|
|
7299
7690
|
const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
|
|
7300
7691
|
// Initialize partial array if not present
|
|
7301
7692
|
if (!signal._partial)
|
|
@@ -8920,14 +9311,15 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
|
|
|
8920
9311
|
const CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, closedSignal, averagePrice, closeTimestamp) => {
|
|
8921
9312
|
const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, averagePrice, "closed", closedSignal, self);
|
|
8922
9313
|
if (!syncCloseAllowed) {
|
|
8923
|
-
|
|
9314
|
+
// Sync close rejected (e.g. broker rejected the order) — keep _closedSignal intact
|
|
9315
|
+
// and return null so the candle loop re-attempts on the next candle. Mirrors live
|
|
9316
|
+
// tick, which keeps _closedSignal and returns idle on a rejected user close,
|
|
9317
|
+
// re-trying on the following tick.
|
|
9318
|
+
self.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry on next candle", {
|
|
8924
9319
|
symbol: self.params.execution.context.symbol,
|
|
8925
9320
|
signalId: closedSignal.id,
|
|
8926
9321
|
});
|
|
8927
|
-
|
|
8928
|
-
self._pendingSignal = closedSignal;
|
|
8929
|
-
throw new Error(`ClientStrategy backtest: signal close rejected by sync (signalId=${closedSignal.id}). ` +
|
|
8930
|
-
`Retry backtest() with new candle data.`);
|
|
9322
|
+
return null;
|
|
8931
9323
|
}
|
|
8932
9324
|
self._closedSignal = null;
|
|
8933
9325
|
const publicSignal = TO_PUBLIC_SIGNAL("pending", closedSignal, averagePrice);
|
|
@@ -9179,7 +9571,15 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
|
|
|
9179
9571
|
}
|
|
9180
9572
|
// КРИТИЧНО: Проверяем был ли сигнал закрыт пользователем через closePending()
|
|
9181
9573
|
if (self._closedSignal) {
|
|
9182
|
-
|
|
9574
|
+
const userCloseResult = await CLOSE_USER_PENDING_SIGNAL_IN_BACKTEST_FN(self, self._closedSignal, averagePrice, currentCandleTimestamp);
|
|
9575
|
+
// Sync close accepted — position closed, return. If rejected, _closedSignal is
|
|
9576
|
+
// kept and we skip this candle: live tick returns idle on a rejected user close
|
|
9577
|
+
// (it does NOT fall into the TP/SL or active-monitoring path), so the candle is
|
|
9578
|
+
// skipped here and the close is re-attempted on the next candle.
|
|
9579
|
+
if (userCloseResult) {
|
|
9580
|
+
return userCloseResult;
|
|
9581
|
+
}
|
|
9582
|
+
continue;
|
|
9183
9583
|
}
|
|
9184
9584
|
let shouldClose = false;
|
|
9185
9585
|
let closeReason;
|
|
@@ -9230,7 +9630,19 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles, frameEnd
|
|
|
9230
9630
|
else {
|
|
9231
9631
|
closePrice = averagePrice; // time_expired uses VWAP
|
|
9232
9632
|
}
|
|
9233
|
-
|
|
9633
|
+
const closeResult = await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
|
|
9634
|
+
// Sync close accepted — position closed, return.
|
|
9635
|
+
if (closeResult) {
|
|
9636
|
+
return closeResult;
|
|
9637
|
+
}
|
|
9638
|
+
// Sync close rejected (e.g. broker rejected the order) — _pendingSignal is left
|
|
9639
|
+
// intact (not cleared on rejection). Do NOT return/continue: fall through to the
|
|
9640
|
+
// active-monitoring block below so this candle is processed exactly like live
|
|
9641
|
+
// tick, which runs RETURN_PENDING_SIGNAL_ACTIVE_FN when CLOSE_PENDING_SIGNAL_FN
|
|
9642
|
+
// returns null (updates _peak/_fall, fires active ping / breakeven / partial
|
|
9643
|
+
// callbacks, drains the commit queue). The close is re-attempted on the next
|
|
9644
|
+
// candle; for time_expired this eventually reaches the loop-exhausted close
|
|
9645
|
+
// (which throws if still rejected).
|
|
9234
9646
|
}
|
|
9235
9647
|
// Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
|
|
9236
9648
|
// Calculate percentage of path to TP/SL
|
|
@@ -9404,6 +9816,13 @@ class ClientStrategy {
|
|
|
9404
9816
|
this._cancelledSignal = null;
|
|
9405
9817
|
this._closedSignal = null;
|
|
9406
9818
|
this._activatedSignal = null;
|
|
9819
|
+
/**
|
|
9820
|
+
* User-supplied signal DTO to be consumed by the next GET_SIGNAL_FN tick instead of
|
|
9821
|
+
* params.getSignal. Set via createSignal. When non-null, params.getSignal is NOT called
|
|
9822
|
+
* and the existing pipeline (priceOpen decides pending vs scheduled, onSignalSync on open)
|
|
9823
|
+
* is reused.
|
|
9824
|
+
*/
|
|
9825
|
+
this._userSignal = null;
|
|
9407
9826
|
/** Queue for commit events to be processed in tick()/backtest() with proper timestamp */
|
|
9408
9827
|
this._commitQueue = [];
|
|
9409
9828
|
/**
|
|
@@ -9652,7 +10071,7 @@ class ClientStrategy {
|
|
|
9652
10071
|
async getStopped(symbol) {
|
|
9653
10072
|
this.params.logger.debug("ClientStrategy getStopped", {
|
|
9654
10073
|
symbol,
|
|
9655
|
-
strategyName: this.params.
|
|
10074
|
+
strategyName: this.params.strategyName,
|
|
9656
10075
|
});
|
|
9657
10076
|
return this._isStopped;
|
|
9658
10077
|
}
|
|
@@ -10335,6 +10754,8 @@ class ClientStrategy {
|
|
|
10335
10754
|
if (this._cancelledSignal) {
|
|
10336
10755
|
const cancelledSignal = this._cancelledSignal;
|
|
10337
10756
|
this._cancelledSignal = null; // Clear after emitting
|
|
10757
|
+
// Persist the cleared deferred state so the drained flag is not replayed on restart
|
|
10758
|
+
await PERSIST_STRATEGY_FN(this);
|
|
10338
10759
|
this.params.logger.info("ClientStrategy tick: scheduled signal was cancelled", {
|
|
10339
10760
|
symbol: this.params.execution.context.symbol,
|
|
10340
10761
|
signalId: cancelledSignal.id,
|
|
@@ -10393,6 +10814,8 @@ class ClientStrategy {
|
|
|
10393
10814
|
return await RETURN_IDLE_FN(this, currentPrice);
|
|
10394
10815
|
}
|
|
10395
10816
|
this._closedSignal = null; // Clear only after sync confirmed
|
|
10817
|
+
// Persist the cleared deferred state so the drained flag is not replayed on restart
|
|
10818
|
+
await PERSIST_STRATEGY_FN(this);
|
|
10396
10819
|
this.params.logger.info("ClientStrategy tick: pending signal was closed", {
|
|
10397
10820
|
symbol: this.params.execution.context.symbol,
|
|
10398
10821
|
signalId: closedSignal.id,
|
|
@@ -10448,6 +10871,8 @@ class ClientStrategy {
|
|
|
10448
10871
|
const currentPrice = await this.params.exchange.getAveragePrice(this.params.execution.context.symbol);
|
|
10449
10872
|
const activatedSignal = this._activatedSignal;
|
|
10450
10873
|
this._activatedSignal = null; // Clear after emitting
|
|
10874
|
+
// Persist the cleared deferred state so the drained flag is not replayed on restart
|
|
10875
|
+
await PERSIST_STRATEGY_FN(this);
|
|
10451
10876
|
this.params.logger.info("ClientStrategy tick: scheduled signal was activated", {
|
|
10452
10877
|
symbol: this.params.execution.context.symbol,
|
|
10453
10878
|
signalId: activatedSignal.id,
|
|
@@ -10705,18 +11130,20 @@ class ClientStrategy {
|
|
|
10705
11130
|
const currentPrice = await this.params.exchange.getAveragePrice(symbol);
|
|
10706
11131
|
const closedSignal = this._closedSignal;
|
|
10707
11132
|
const closeTimestamp = this.params.execution.context.when.getTime();
|
|
10708
|
-
// Sync close: if external system rejects —
|
|
11133
|
+
// Sync close: if external system rejects — keep the close pending and re-attempt
|
|
11134
|
+
// it inside the candle loop below (PROCESS_PENDING_SIGNAL_CANDLES_FN handles
|
|
11135
|
+
// _closedSignal per candle). Mirrors live tick, which keeps _closedSignal and
|
|
11136
|
+
// retries on the next tick instead of failing.
|
|
10709
11137
|
const syncCloseAllowed = await CALL_SIGNAL_SYNC_CLOSE_FN(closeTimestamp, currentPrice, "closed", closedSignal, this);
|
|
10710
11138
|
if (!syncCloseAllowed) {
|
|
10711
|
-
this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry", {
|
|
11139
|
+
this.params.logger.info("ClientStrategy backtest: user-closed signal rejected by sync, will retry in candle loop", {
|
|
10712
11140
|
symbol: this.params.execution.context.symbol,
|
|
10713
11141
|
signalId: closedSignal.id,
|
|
10714
11142
|
});
|
|
10715
|
-
// Restore _pendingSignal so
|
|
10716
|
-
|
|
11143
|
+
// Restore _pendingSignal so the candle loop processes the position normally;
|
|
11144
|
+
// _closedSignal is kept so the loop re-attempts the close on each candle.
|
|
10717
11145
|
this._pendingSignal = closedSignal;
|
|
10718
|
-
|
|
10719
|
-
`Retry backtest() with new candle data.`);
|
|
11146
|
+
return await PROCESS_PENDING_SIGNAL_CANDLES_FN(this, this._pendingSignal, candles, frameEndTime);
|
|
10720
11147
|
}
|
|
10721
11148
|
this._closedSignal = null; // Clear only after sync confirmed
|
|
10722
11149
|
// Emit commit with correct timestamp from backtest context
|
|
@@ -10898,7 +11325,7 @@ class ClientStrategy {
|
|
|
10898
11325
|
if (backtest) {
|
|
10899
11326
|
return;
|
|
10900
11327
|
}
|
|
10901
|
-
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.
|
|
11328
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
10902
11329
|
}
|
|
10903
11330
|
/**
|
|
10904
11331
|
* Cancels the scheduled signal without stopping the strategy.
|
|
@@ -10942,7 +11369,9 @@ class ClientStrategy {
|
|
|
10942
11369
|
// Commit will be emitted in backtest() with correct candle timestamp
|
|
10943
11370
|
return;
|
|
10944
11371
|
}
|
|
10945
|
-
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.
|
|
11372
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
11373
|
+
// Persist deferred _cancelledSignal so a crash before the next tick does not lose it
|
|
11374
|
+
await PERSIST_STRATEGY_FN(this);
|
|
10946
11375
|
// Commit will be emitted in tick() with correct currentTime
|
|
10947
11376
|
}
|
|
10948
11377
|
/**
|
|
@@ -10993,7 +11422,9 @@ class ClientStrategy {
|
|
|
10993
11422
|
// Commit will be emitted AFTER successful risk check in PROCESS_SCHEDULED_SIGNAL_CANDLES_FN
|
|
10994
11423
|
return;
|
|
10995
11424
|
}
|
|
10996
|
-
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.
|
|
11425
|
+
await PersistScheduleAdapter.writeScheduleData(this._scheduledSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
11426
|
+
// Persist deferred _activatedSignal so a crash before the next tick does not lose it
|
|
11427
|
+
await PERSIST_STRATEGY_FN(this);
|
|
10997
11428
|
// Commit will be emitted AFTER successful risk check in tick()
|
|
10998
11429
|
}
|
|
10999
11430
|
/**
|
|
@@ -11038,8 +11469,92 @@ class ClientStrategy {
|
|
|
11038
11469
|
return;
|
|
11039
11470
|
}
|
|
11040
11471
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, symbol, this.params.strategyName, this.params.exchangeName);
|
|
11472
|
+
// Persist deferred _closedSignal so a crash before the next tick does not lose it
|
|
11473
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11041
11474
|
// Commit will be emitted in tick() with correct currentTime
|
|
11042
11475
|
}
|
|
11476
|
+
/**
|
|
11477
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of
|
|
11478
|
+
* params.getSignal.
|
|
11479
|
+
*
|
|
11480
|
+
* Works OUT of the async-hooks execution context (uses this.params.symbol directly).
|
|
11481
|
+
* priceOpen decides the outcome in the existing pipeline: when omitted the position opens
|
|
11482
|
+
* immediately at currentPrice; when provided the pipeline opens immediately if priceOpen is
|
|
11483
|
+
* already reached, otherwise registers a scheduled (priceOpen-awaiting) signal. On the next
|
|
11484
|
+
* tick GET_SIGNAL_FN consumes _userSignal and runs the normal pipeline, so onSignalSync
|
|
11485
|
+
* delivery is checked by OPEN_NEW_PENDING_SIGNAL_FN exactly as for a getSignal-produced signal.
|
|
11486
|
+
*
|
|
11487
|
+
* Validation (BEFORE any state mutation — a rejected call stores nothing):
|
|
11488
|
+
* - The DTO must pass the intrinsic shape/price checks.
|
|
11489
|
+
* - There must be NO signal already in flight: no active pending signal, no scheduled signal,
|
|
11490
|
+
* no already-queued createSignal, and no deferred activate/close/cancel awaiting drain.
|
|
11491
|
+
* Creating a new signal on top of an existing one is rejected.
|
|
11492
|
+
*
|
|
11493
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11494
|
+
* @param currentPrice - Current market price (priceOpen fallback for immediate signals)
|
|
11495
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
11496
|
+
* @returns Promise that resolves when the DTO is queued (and persisted in live mode)
|
|
11497
|
+
* @throws {Error} If the DTO is invalid or a signal/deferred action is already in flight
|
|
11498
|
+
*/
|
|
11499
|
+
async createSignal(symbol, currentPrice, dto) {
|
|
11500
|
+
this.params.logger.debug("ClientStrategy createSignal", { symbol, currentPrice });
|
|
11501
|
+
// Validate BEFORE mutating state — a bad DTO or a busy strategy must store nothing.
|
|
11502
|
+
// Reuse validateSignal (the canonical getSignal-output validator, branching
|
|
11503
|
+
// pending-vs-scheduled by the same rule as GET_SIGNAL_FN). It forwards to
|
|
11504
|
+
// validateCommonSignal which requires priceOpen and minuteEstimatedTime, so normalize the
|
|
11505
|
+
// DTO to the defaults GET_SIGNAL_FN would apply (priceOpen → currentPrice for an immediate
|
|
11506
|
+
// signal, minuteEstimatedTime → CC_MAX_SIGNAL_LIFETIME_MINUTES). The original dto stored in
|
|
11507
|
+
// _userSignal is left untouched so GET_SIGNAL_FN applies its own defaults on consume.
|
|
11508
|
+
if (!validateSignal({
|
|
11509
|
+
...dto,
|
|
11510
|
+
priceOpen: dto.priceOpen ?? currentPrice,
|
|
11511
|
+
minuteEstimatedTime: dto.minuteEstimatedTime ?? GLOBAL_CONFIG.CC_MAX_SIGNAL_LIFETIME_MINUTES,
|
|
11512
|
+
}, currentPrice)) {
|
|
11513
|
+
throw new Error(`ClientStrategy createSignal: invalid signal DTO for symbol=${symbol}`);
|
|
11514
|
+
}
|
|
11515
|
+
// Reject if any signal is already in flight or any deferred action is awaiting drain.
|
|
11516
|
+
if (this._pendingSignal) {
|
|
11517
|
+
throw new Error(`ClientStrategy createSignal: a pending signal already exists for symbol=${symbol}`);
|
|
11518
|
+
}
|
|
11519
|
+
if (this._scheduledSignal) {
|
|
11520
|
+
throw new Error(`ClientStrategy createSignal: a scheduled signal already exists for symbol=${symbol}`);
|
|
11521
|
+
}
|
|
11522
|
+
if (this._userSignal) {
|
|
11523
|
+
throw new Error(`ClientStrategy createSignal: a signal is already queued for creation for symbol=${symbol}`);
|
|
11524
|
+
}
|
|
11525
|
+
if (this._activatedSignal) {
|
|
11526
|
+
throw new Error(`ClientStrategy createSignal: a scheduled activation is pending for symbol=${symbol}`);
|
|
11527
|
+
}
|
|
11528
|
+
if (this._closedSignal) {
|
|
11529
|
+
throw new Error(`ClientStrategy createSignal: a pending close is awaiting for symbol=${symbol}`);
|
|
11530
|
+
}
|
|
11531
|
+
if (this._cancelledSignal) {
|
|
11532
|
+
throw new Error(`ClientStrategy createSignal: a scheduled cancel is awaiting for symbol=${symbol}`);
|
|
11533
|
+
}
|
|
11534
|
+
this._userSignal = dto;
|
|
11535
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11536
|
+
}
|
|
11537
|
+
/**
|
|
11538
|
+
* Returns the deferred strategy-state snapshot exactly as it would be written to persist on
|
|
11539
|
+
* this iteration: the in-memory _userSignal, _commitQueue and deferred user-action flags,
|
|
11540
|
+
* plus the current pending signal id.
|
|
11541
|
+
*
|
|
11542
|
+
* Synchronous in-memory read (no disk access), so it works OUT of the async-hooks context.
|
|
11543
|
+
*
|
|
11544
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
11545
|
+
* @returns The current StrategyData snapshot held in memory
|
|
11546
|
+
*/
|
|
11547
|
+
async getStatus(symbol) {
|
|
11548
|
+
this.params.logger.debug("ClientStrategy getStatus", { symbol });
|
|
11549
|
+
return {
|
|
11550
|
+
pendingSignalId: this._pendingSignal?.id ?? null,
|
|
11551
|
+
createSignal: this._userSignal,
|
|
11552
|
+
commitQueue: this._commitQueue,
|
|
11553
|
+
closedSignal: this._closedSignal,
|
|
11554
|
+
cancelledSignal: this._cancelledSignal,
|
|
11555
|
+
activatedSignal: this._activatedSignal,
|
|
11556
|
+
};
|
|
11557
|
+
}
|
|
11043
11558
|
/**
|
|
11044
11559
|
* Validates preconditions for partialProfit without mutating state.
|
|
11045
11560
|
*
|
|
@@ -11208,10 +11723,10 @@ class ClientStrategy {
|
|
|
11208
11723
|
});
|
|
11209
11724
|
// Call onWrite callback for testing persist storage
|
|
11210
11725
|
if (this.params.callbacks?.onWrite) {
|
|
11211
|
-
this.params.callbacks.onWrite(this.params.
|
|
11726
|
+
this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
|
|
11212
11727
|
}
|
|
11213
11728
|
if (!backtest) {
|
|
11214
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
11729
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
11215
11730
|
}
|
|
11216
11731
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
11217
11732
|
this._commitQueue.push({
|
|
@@ -11221,6 +11736,8 @@ class ClientStrategy {
|
|
|
11221
11736
|
percentToClose,
|
|
11222
11737
|
currentPrice,
|
|
11223
11738
|
});
|
|
11739
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
11740
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11224
11741
|
return true;
|
|
11225
11742
|
}
|
|
11226
11743
|
/**
|
|
@@ -11391,10 +11908,10 @@ class ClientStrategy {
|
|
|
11391
11908
|
});
|
|
11392
11909
|
// Call onWrite callback for testing persist storage
|
|
11393
11910
|
if (this.params.callbacks?.onWrite) {
|
|
11394
|
-
this.params.callbacks.onWrite(this.params.
|
|
11911
|
+
this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
|
|
11395
11912
|
}
|
|
11396
11913
|
if (!backtest) {
|
|
11397
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
11914
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
11398
11915
|
}
|
|
11399
11916
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
11400
11917
|
this._commitQueue.push({
|
|
@@ -11404,6 +11921,8 @@ class ClientStrategy {
|
|
|
11404
11921
|
percentToClose,
|
|
11405
11922
|
currentPrice,
|
|
11406
11923
|
});
|
|
11924
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
11925
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11407
11926
|
return true;
|
|
11408
11927
|
}
|
|
11409
11928
|
/**
|
|
@@ -11590,10 +12109,10 @@ class ClientStrategy {
|
|
|
11590
12109
|
// Call onWrite callback for testing persist storage
|
|
11591
12110
|
if (this.params.callbacks?.onWrite) {
|
|
11592
12111
|
const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
|
|
11593
|
-
this.params.callbacks.onWrite(this.params.
|
|
12112
|
+
this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
|
|
11594
12113
|
}
|
|
11595
12114
|
if (!backtest) {
|
|
11596
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
12115
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
11597
12116
|
}
|
|
11598
12117
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
11599
12118
|
this._commitQueue.push({
|
|
@@ -11602,6 +12121,8 @@ class ClientStrategy {
|
|
|
11602
12121
|
backtest,
|
|
11603
12122
|
currentPrice,
|
|
11604
12123
|
});
|
|
12124
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
12125
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11605
12126
|
return true;
|
|
11606
12127
|
}
|
|
11607
12128
|
/**
|
|
@@ -11839,10 +12360,10 @@ class ClientStrategy {
|
|
|
11839
12360
|
// Call onWrite callback for testing persist storage
|
|
11840
12361
|
if (this.params.callbacks?.onWrite) {
|
|
11841
12362
|
const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
|
|
11842
|
-
this.params.callbacks.onWrite(this.params.
|
|
12363
|
+
this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
|
|
11843
12364
|
}
|
|
11844
12365
|
if (!backtest) {
|
|
11845
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
12366
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
11846
12367
|
}
|
|
11847
12368
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
11848
12369
|
this._commitQueue.push({
|
|
@@ -11852,6 +12373,8 @@ class ClientStrategy {
|
|
|
11852
12373
|
percentShift,
|
|
11853
12374
|
currentPrice,
|
|
11854
12375
|
});
|
|
12376
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
12377
|
+
await PERSIST_STRATEGY_FN(this);
|
|
11855
12378
|
return true;
|
|
11856
12379
|
}
|
|
11857
12380
|
/**
|
|
@@ -12075,10 +12598,10 @@ class ClientStrategy {
|
|
|
12075
12598
|
// Call onWrite callback for testing persist storage
|
|
12076
12599
|
if (this.params.callbacks?.onWrite) {
|
|
12077
12600
|
const publicSignal = TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice);
|
|
12078
|
-
this.params.callbacks.onWrite(this.params.
|
|
12601
|
+
this.params.callbacks.onWrite(this.params.symbol, publicSignal, backtest);
|
|
12079
12602
|
}
|
|
12080
12603
|
if (!backtest) {
|
|
12081
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
12604
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
12082
12605
|
}
|
|
12083
12606
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
12084
12607
|
this._commitQueue.push({
|
|
@@ -12088,6 +12611,8 @@ class ClientStrategy {
|
|
|
12088
12611
|
percentShift,
|
|
12089
12612
|
currentPrice,
|
|
12090
12613
|
});
|
|
12614
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
12615
|
+
await PERSIST_STRATEGY_FN(this);
|
|
12091
12616
|
return true;
|
|
12092
12617
|
}
|
|
12093
12618
|
/**
|
|
@@ -12166,10 +12691,10 @@ class ClientStrategy {
|
|
|
12166
12691
|
});
|
|
12167
12692
|
// Call onWrite callback for testing persist storage
|
|
12168
12693
|
if (this.params.callbacks?.onWrite) {
|
|
12169
|
-
this.params.callbacks.onWrite(this.params.
|
|
12694
|
+
this.params.callbacks.onWrite(this.params.symbol, TO_PUBLIC_SIGNAL("pending", this._pendingSignal, currentPrice), backtest);
|
|
12170
12695
|
}
|
|
12171
12696
|
if (!backtest) {
|
|
12172
|
-
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.
|
|
12697
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.symbol, this.params.strategyName, this.params.exchangeName);
|
|
12173
12698
|
}
|
|
12174
12699
|
// Queue commit event for processing in tick()/backtest() with proper timestamp
|
|
12175
12700
|
this._commitQueue.push({
|
|
@@ -12180,6 +12705,8 @@ class ClientStrategy {
|
|
|
12180
12705
|
cost,
|
|
12181
12706
|
totalEntries: this._pendingSignal._entry?.length ?? 1,
|
|
12182
12707
|
});
|
|
12708
|
+
// Persist the queued commit so a crash before the next tick does not lose it
|
|
12709
|
+
await PERSIST_STRATEGY_FN(this);
|
|
12183
12710
|
return true;
|
|
12184
12711
|
}
|
|
12185
12712
|
}
|
|
@@ -12326,6 +12853,8 @@ class MergeRisk {
|
|
|
12326
12853
|
|
|
12327
12854
|
/** Default interval for strategies that do not specify one */
|
|
12328
12855
|
const STRATEGY_DEFAULT_INTERVAL = "1m";
|
|
12856
|
+
/** If not specified strategy will not open positions until createSignal is called manually */
|
|
12857
|
+
const STRATEGY_DEFAULT_SIGNAL = () => null;
|
|
12329
12858
|
/**
|
|
12330
12859
|
* If syncSubject listener or any registered action throws, it means the signal was not properly synchronized
|
|
12331
12860
|
* to the exchange (e.g. limit order failed to fill).
|
|
@@ -12759,7 +13288,7 @@ class StrategyConnectionService {
|
|
|
12759
13288
|
* @returns Configured ClientStrategy instance
|
|
12760
13289
|
*/
|
|
12761
13290
|
this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$A(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
|
|
12762
|
-
const { riskName = "", riskList = [], getSignal, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
13291
|
+
const { riskName = "", riskList = [], getSignal = STRATEGY_DEFAULT_SIGNAL, interval = STRATEGY_DEFAULT_INTERVAL, callbacks, } = this.strategySchemaService.get(strategyName);
|
|
12763
13292
|
return new ClientStrategy({
|
|
12764
13293
|
symbol,
|
|
12765
13294
|
interval,
|
|
@@ -13778,6 +14307,47 @@ class StrategyConnectionService {
|
|
|
13778
14307
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
13779
14308
|
await strategy.closePending(symbol, backtest, payload);
|
|
13780
14309
|
};
|
|
14310
|
+
/**
|
|
14311
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
|
|
14312
|
+
*
|
|
14313
|
+
* Delegates to ClientStrategy.createSignal(). Validated and rejected if a signal/deferred
|
|
14314
|
+
* action is already in flight. Works out of the async-hooks execution context.
|
|
14315
|
+
*
|
|
14316
|
+
* @param backtest - Whether running in backtest mode
|
|
14317
|
+
* @param symbol - Trading pair symbol
|
|
14318
|
+
* @param dto - Signal DTO to open
|
|
14319
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14320
|
+
* @returns Promise that resolves when the DTO is queued
|
|
14321
|
+
*/
|
|
14322
|
+
this.createSignal = async (backtest, symbol, dto, context) => {
|
|
14323
|
+
this.loggerService.log("strategyConnectionService createSignal", {
|
|
14324
|
+
symbol,
|
|
14325
|
+
context,
|
|
14326
|
+
backtest,
|
|
14327
|
+
});
|
|
14328
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14329
|
+
const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
|
|
14330
|
+
await strategy.createSignal(symbol, currentPrice, dto);
|
|
14331
|
+
};
|
|
14332
|
+
/**
|
|
14333
|
+
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
14334
|
+
*
|
|
14335
|
+
* Delegates to ClientStrategy.getStatus(). Synchronous in-memory read; works out of context.
|
|
14336
|
+
*
|
|
14337
|
+
* @param backtest - Whether running in backtest mode
|
|
14338
|
+
* @param symbol - Trading pair symbol
|
|
14339
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
14340
|
+
* @returns Promise resolving to the current StrategyData snapshot
|
|
14341
|
+
*/
|
|
14342
|
+
this.getStatus = async (backtest, symbol, context) => {
|
|
14343
|
+
this.loggerService.log("strategyConnectionService getStatus", {
|
|
14344
|
+
symbol,
|
|
14345
|
+
context,
|
|
14346
|
+
backtest,
|
|
14347
|
+
});
|
|
14348
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
14349
|
+
return await strategy.getStatus(symbol);
|
|
14350
|
+
};
|
|
13781
14351
|
/**
|
|
13782
14352
|
* Checks whether `partialProfit` would succeed without executing it.
|
|
13783
14353
|
* Delegates to `ClientStrategy.validatePartialProfit()` — no throws, pure boolean result.
|
|
@@ -17381,6 +17951,47 @@ class StrategyCoreService {
|
|
|
17381
17951
|
await this.validate(context);
|
|
17382
17952
|
return await this.strategyConnectionService.closePending(backtest, symbol, context, payload);
|
|
17383
17953
|
};
|
|
17954
|
+
/**
|
|
17955
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of getSignal.
|
|
17956
|
+
*
|
|
17957
|
+
* Validates the context, then delegates to StrategyConnectionService.createSignal().
|
|
17958
|
+
* Rejected if a signal or deferred action is already in flight. Does not require execution context.
|
|
17959
|
+
*
|
|
17960
|
+
* @param backtest - Whether running in backtest mode
|
|
17961
|
+
* @param symbol - Trading pair symbol
|
|
17962
|
+
* @param dto - Signal DTO to open
|
|
17963
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
17964
|
+
* @returns Promise that resolves when the DTO is queued
|
|
17965
|
+
*/
|
|
17966
|
+
this.createSignal = async (backtest, symbol, dto, context) => {
|
|
17967
|
+
this.loggerService.log("strategyCoreService createSignal", {
|
|
17968
|
+
symbol,
|
|
17969
|
+
context,
|
|
17970
|
+
backtest,
|
|
17971
|
+
});
|
|
17972
|
+
await this.validate(context);
|
|
17973
|
+
return await this.strategyConnectionService.createSignal(backtest, symbol, dto, context);
|
|
17974
|
+
};
|
|
17975
|
+
/**
|
|
17976
|
+
* Returns the in-memory deferred strategy-state snapshot for this iteration.
|
|
17977
|
+
*
|
|
17978
|
+
* Validates the context, then delegates to StrategyConnectionService.getStatus().
|
|
17979
|
+
* Does not require execution context.
|
|
17980
|
+
*
|
|
17981
|
+
* @param backtest - Whether running in backtest mode
|
|
17982
|
+
* @param symbol - Trading pair symbol
|
|
17983
|
+
* @param context - Context with strategyName, exchangeName, frameName
|
|
17984
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
17985
|
+
*/
|
|
17986
|
+
this.getStatus = async (backtest, symbol, context) => {
|
|
17987
|
+
this.loggerService.log("strategyCoreService getStatus", {
|
|
17988
|
+
symbol,
|
|
17989
|
+
context,
|
|
17990
|
+
backtest,
|
|
17991
|
+
});
|
|
17992
|
+
await this.validate(context);
|
|
17993
|
+
return await this.strategyConnectionService.getStatus(backtest, symbol, context);
|
|
17994
|
+
};
|
|
17384
17995
|
/**
|
|
17385
17996
|
* Disposes the ClientStrategy instance for the given context.
|
|
17386
17997
|
*
|
|
@@ -18905,7 +19516,7 @@ class StrategySchemaService {
|
|
|
18905
19516
|
if (strategySchema.interval && typeof strategySchema.interval !== "string") {
|
|
18906
19517
|
throw new Error(`strategy schema validation failed: invalid interval for strategyName=${strategySchema.strategyName}`);
|
|
18907
19518
|
}
|
|
18908
|
-
if (typeof strategySchema.getSignal !== "function") {
|
|
19519
|
+
if (strategySchema.getSignal && typeof strategySchema.getSignal !== "function") {
|
|
18909
19520
|
throw new Error(`strategy schema validation failed: missing getSignal for strategyName=${strategySchema.strategyName}`);
|
|
18910
19521
|
}
|
|
18911
19522
|
};
|
|
@@ -22920,32 +23531,32 @@ const heat_columns = [
|
|
|
22920
23531
|
{
|
|
22921
23532
|
key: "totalPnl",
|
|
22922
23533
|
label: "Total PNL",
|
|
22923
|
-
format: (data) => data.totalPnl !== null ?
|
|
23534
|
+
format: (data) => data.totalPnl !== null ? `${data.totalPnl.toFixed(2)}%` : "N/A",
|
|
22924
23535
|
isVisible: () => true,
|
|
22925
23536
|
},
|
|
22926
23537
|
{
|
|
22927
23538
|
key: "sharpeRatio",
|
|
22928
23539
|
label: "Sharpe",
|
|
22929
|
-
format: (data) => data.sharpeRatio !== null ?
|
|
23540
|
+
format: (data) => data.sharpeRatio !== null ? data.sharpeRatio.toFixed(3) : "N/A",
|
|
22930
23541
|
isVisible: () => true,
|
|
22931
23542
|
},
|
|
22932
23543
|
{
|
|
22933
23544
|
key: "annualizedSharpeRatio",
|
|
22934
23545
|
label: "Ann Sharpe",
|
|
22935
|
-
format: (data) => data.annualizedSharpeRatio !== null ?
|
|
23546
|
+
format: (data) => data.annualizedSharpeRatio !== null ? data.annualizedSharpeRatio.toFixed(3) : "N/A",
|
|
22936
23547
|
isVisible: () => true,
|
|
22937
23548
|
},
|
|
22938
23549
|
{
|
|
22939
23550
|
key: "certaintyRatio",
|
|
22940
23551
|
label: "Certainty",
|
|
22941
|
-
format: (data) => data.certaintyRatio !== null ?
|
|
23552
|
+
format: (data) => data.certaintyRatio !== null ? data.certaintyRatio.toFixed(3) : "N/A",
|
|
22942
23553
|
isVisible: () => true,
|
|
22943
23554
|
},
|
|
22944
23555
|
{
|
|
22945
23556
|
key: "expectedYearlyReturns",
|
|
22946
23557
|
label: "Exp Yearly",
|
|
22947
23558
|
format: (data) => data.expectedYearlyReturns !== null
|
|
22948
|
-
?
|
|
23559
|
+
? `${data.expectedYearlyReturns.toFixed(2)}%`
|
|
22949
23560
|
: "N/A",
|
|
22950
23561
|
isVisible: () => true,
|
|
22951
23562
|
},
|
|
@@ -22958,37 +23569,37 @@ const heat_columns = [
|
|
|
22958
23569
|
{
|
|
22959
23570
|
key: "profitFactor",
|
|
22960
23571
|
label: "PF",
|
|
22961
|
-
format: (data) => data.profitFactor !== null ?
|
|
23572
|
+
format: (data) => data.profitFactor !== null ? data.profitFactor.toFixed(3) : "N/A",
|
|
22962
23573
|
isVisible: () => true,
|
|
22963
23574
|
},
|
|
22964
23575
|
{
|
|
22965
23576
|
key: "expectancy",
|
|
22966
23577
|
label: "Expect",
|
|
22967
|
-
format: (data) => data.expectancy !== null ?
|
|
23578
|
+
format: (data) => data.expectancy !== null ? `${data.expectancy.toFixed(2)}%` : "N/A",
|
|
22968
23579
|
isVisible: () => true,
|
|
22969
23580
|
},
|
|
22970
23581
|
{
|
|
22971
23582
|
key: "winRate",
|
|
22972
23583
|
label: "WR",
|
|
22973
|
-
format: (data) => data.winRate !== null ?
|
|
23584
|
+
format: (data) => data.winRate !== null ? `${data.winRate.toFixed(2)}%` : "N/A",
|
|
22974
23585
|
isVisible: () => true,
|
|
22975
23586
|
},
|
|
22976
23587
|
{
|
|
22977
23588
|
key: "avgWin",
|
|
22978
23589
|
label: "Avg Win",
|
|
22979
|
-
format: (data) => data.avgWin !== null ?
|
|
23590
|
+
format: (data) => data.avgWin !== null ? `${data.avgWin.toFixed(2)}%` : "N/A",
|
|
22980
23591
|
isVisible: () => true,
|
|
22981
23592
|
},
|
|
22982
23593
|
{
|
|
22983
23594
|
key: "avgLoss",
|
|
22984
23595
|
label: "Avg Loss",
|
|
22985
|
-
format: (data) => data.avgLoss !== null ?
|
|
23596
|
+
format: (data) => data.avgLoss !== null ? `${data.avgLoss.toFixed(2)}%` : "N/A",
|
|
22986
23597
|
isVisible: () => true,
|
|
22987
23598
|
},
|
|
22988
23599
|
{
|
|
22989
23600
|
key: "maxDrawdown",
|
|
22990
23601
|
label: "Max DD",
|
|
22991
|
-
format: (data) => data.maxDrawdown !== null ?
|
|
23602
|
+
format: (data) => data.maxDrawdown !== null ? `${(-data.maxDrawdown).toFixed(2)}%` : "N/A",
|
|
22992
23603
|
isVisible: () => true,
|
|
22993
23604
|
},
|
|
22994
23605
|
{
|
|
@@ -23012,31 +23623,31 @@ const heat_columns = [
|
|
|
23012
23623
|
{
|
|
23013
23624
|
key: "avgPeakPnl",
|
|
23014
23625
|
label: "Avg Peak PNL",
|
|
23015
|
-
format: (data) => data.avgPeakPnl !== null ?
|
|
23626
|
+
format: (data) => data.avgPeakPnl !== null ? `${data.avgPeakPnl.toFixed(2)}%` : "N/A",
|
|
23016
23627
|
isVisible: () => true,
|
|
23017
23628
|
},
|
|
23018
23629
|
{
|
|
23019
23630
|
key: "avgFallPnl",
|
|
23020
23631
|
label: "Avg DD PNL",
|
|
23021
|
-
format: (data) => data.avgFallPnl !== null ?
|
|
23632
|
+
format: (data) => data.avgFallPnl !== null ? `${data.avgFallPnl.toFixed(2)}%` : "N/A",
|
|
23022
23633
|
isVisible: () => true,
|
|
23023
23634
|
},
|
|
23024
23635
|
{
|
|
23025
23636
|
key: "peakProfitPnl",
|
|
23026
23637
|
label: "Peak Profit PNL",
|
|
23027
|
-
format: (data) => data.peakProfitPnl !== null ?
|
|
23638
|
+
format: (data) => data.peakProfitPnl !== null ? `${data.peakProfitPnl.toFixed(2)}%` : "N/A",
|
|
23028
23639
|
isVisible: () => true,
|
|
23029
23640
|
},
|
|
23030
23641
|
{
|
|
23031
23642
|
key: "maxDrawdownPnl",
|
|
23032
23643
|
label: "Max DD PNL",
|
|
23033
|
-
format: (data) => data.maxDrawdownPnl !== null ?
|
|
23644
|
+
format: (data) => data.maxDrawdownPnl !== null ? `${data.maxDrawdownPnl.toFixed(2)}%` : "N/A",
|
|
23034
23645
|
isVisible: () => true,
|
|
23035
23646
|
},
|
|
23036
23647
|
{
|
|
23037
23648
|
key: "medianPnl",
|
|
23038
23649
|
label: "Median PNL",
|
|
23039
|
-
format: (data) => data.medianPnl !== null ?
|
|
23650
|
+
format: (data) => data.medianPnl !== null ? `${data.medianPnl.toFixed(2)}%` : "N/A",
|
|
23040
23651
|
isVisible: () => true,
|
|
23041
23652
|
},
|
|
23042
23653
|
{
|
|
@@ -23061,7 +23672,7 @@ const heat_columns = [
|
|
|
23061
23672
|
key: "avgConsecutiveWinPnl",
|
|
23062
23673
|
label: "Avg Win Streak PNL",
|
|
23063
23674
|
format: (data) => data.avgConsecutiveWinPnl !== null
|
|
23064
|
-
?
|
|
23675
|
+
? `${data.avgConsecutiveWinPnl.toFixed(2)}%`
|
|
23065
23676
|
: "N/A",
|
|
23066
23677
|
isVisible: () => true,
|
|
23067
23678
|
},
|
|
@@ -23069,7 +23680,7 @@ const heat_columns = [
|
|
|
23069
23680
|
key: "avgConsecutiveLossPnl",
|
|
23070
23681
|
label: "Avg Loss Streak PNL",
|
|
23071
23682
|
format: (data) => data.avgConsecutiveLossPnl !== null
|
|
23072
|
-
?
|
|
23683
|
+
? `${data.avgConsecutiveLossPnl.toFixed(2)}%`
|
|
23073
23684
|
: "N/A",
|
|
23074
23685
|
isVisible: () => true,
|
|
23075
23686
|
},
|
|
@@ -23082,7 +23693,7 @@ const heat_columns = [
|
|
|
23082
23693
|
{
|
|
23083
23694
|
key: "trendStrength",
|
|
23084
23695
|
label: "Trend %/d",
|
|
23085
|
-
format: (data) => data.trendStrength !== null ?
|
|
23696
|
+
format: (data) => data.trendStrength !== null ? `${data.trendStrength.toFixed(2)}%` : "N/A",
|
|
23086
23697
|
isVisible: () => true,
|
|
23087
23698
|
},
|
|
23088
23699
|
{
|
|
@@ -23135,25 +23746,25 @@ const heat_columns = [
|
|
|
23135
23746
|
{
|
|
23136
23747
|
key: "medianStepSize",
|
|
23137
23748
|
label: "Median Step",
|
|
23138
|
-
format: (data) => data.medianStepSize !== null ?
|
|
23749
|
+
format: (data) => data.medianStepSize !== null ? `${data.medianStepSize.toFixed(2)}%` : "N/A",
|
|
23139
23750
|
isVisible: () => true,
|
|
23140
23751
|
},
|
|
23141
23752
|
{
|
|
23142
23753
|
key: "sortinoRatio",
|
|
23143
23754
|
label: "Sortino",
|
|
23144
|
-
format: (data) => data.sortinoRatio !== null ?
|
|
23755
|
+
format: (data) => data.sortinoRatio !== null ? data.sortinoRatio.toFixed(3) : "N/A",
|
|
23145
23756
|
isVisible: () => true,
|
|
23146
23757
|
},
|
|
23147
23758
|
{
|
|
23148
23759
|
key: "calmarRatio",
|
|
23149
23760
|
label: "Calmar",
|
|
23150
|
-
format: (data) => data.calmarRatio !== null ?
|
|
23761
|
+
format: (data) => data.calmarRatio !== null ? data.calmarRatio.toFixed(3) : "N/A",
|
|
23151
23762
|
isVisible: () => true,
|
|
23152
23763
|
},
|
|
23153
23764
|
{
|
|
23154
23765
|
key: "recoveryFactor",
|
|
23155
23766
|
label: "Recovery",
|
|
23156
|
-
format: (data) => data.recoveryFactor !== null ?
|
|
23767
|
+
format: (data) => data.recoveryFactor !== null ? data.recoveryFactor.toFixed(3) : "N/A",
|
|
23157
23768
|
isVisible: () => true,
|
|
23158
23769
|
},
|
|
23159
23770
|
];
|
|
@@ -30329,28 +30940,28 @@ class HeatmapStorage {
|
|
|
30329
30940
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
30330
30941
|
"",
|
|
30331
30942
|
`**Total Symbols:** ${data.totalSymbols}`,
|
|
30332
|
-
`**Portfolio PNL:** ${data.portfolioTotalPnl !== null ?
|
|
30333
|
-
`**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ?
|
|
30334
|
-
`**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ?
|
|
30335
|
-
`**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ?
|
|
30336
|
-
`**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ?
|
|
30943
|
+
`**Portfolio PNL:** ${data.portfolioTotalPnl !== null ? `${data.portfolioTotalPnl.toFixed(2)} %` : "N/A"}`,
|
|
30944
|
+
`**Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? data.portfolioSharpeRatio.toFixed(3) : "N/A"}`,
|
|
30945
|
+
`**Annualized Sharpe:** ${data.portfolioAnnualizedSharpeRatio !== null ? data.portfolioAnnualizedSharpeRatio.toFixed(3) : "N/A"}`,
|
|
30946
|
+
`**Certainty Ratio:** ${data.portfolioCertaintyRatio !== null ? data.portfolioCertaintyRatio.toFixed(3) : "N/A"}`,
|
|
30947
|
+
`**Expected Yearly Returns:** ${data.portfolioExpectedYearlyReturns !== null ? `${data.portfolioExpectedYearlyReturns.toFixed(2)} %` : "N/A"}`,
|
|
30337
30948
|
`**Trades Per Year:** ${data.portfolioTradesPerYear !== null ? data.portfolioTradesPerYear.toFixed(1) : "N/A"}`,
|
|
30338
30949
|
`**Total Trades:** ${data.portfolioTotalTrades}`,
|
|
30339
|
-
`**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ?
|
|
30340
|
-
`**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ?
|
|
30341
|
-
`**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ?
|
|
30342
|
-
`**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ?
|
|
30343
|
-
`**Median PNL:** ${data.portfolioMedianPnl !== null ?
|
|
30950
|
+
`**Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? `${data.portfolioAvgPeakPnl.toFixed(2)} %` : "N/A"}`,
|
|
30951
|
+
`**Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? `${data.portfolioAvgFallPnl.toFixed(2)} %` : "N/A"}`,
|
|
30952
|
+
`**Peak Profit PNL:** ${data.portfolioPeakProfitPnl !== null ? `${data.portfolioPeakProfitPnl.toFixed(2)} %` : "N/A"}`,
|
|
30953
|
+
`**Max Drawdown PNL:** ${data.portfolioMaxDrawdownPnl !== null ? `${data.portfolioMaxDrawdownPnl.toFixed(2)} %` : "N/A"}`,
|
|
30954
|
+
`**Median PNL:** ${data.portfolioMedianPnl !== null ? `${data.portfolioMedianPnl.toFixed(2)} %` : "N/A"}`,
|
|
30344
30955
|
`**Avg Duration:** ${data.portfolioAvgDuration !== null ? `${data.portfolioAvgDuration.toFixed(1)} min` : "N/A"}`,
|
|
30345
30956
|
`**Avg Win Duration:** ${data.portfolioAvgWinDuration !== null ? `${data.portfolioAvgWinDuration.toFixed(1)} min` : "N/A"}`,
|
|
30346
30957
|
`**Avg Loss Duration:** ${data.portfolioAvgLossDuration !== null ? `${data.portfolioAvgLossDuration.toFixed(1)} min` : "N/A"}`,
|
|
30347
|
-
`**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ?
|
|
30348
|
-
`**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ?
|
|
30349
|
-
`**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ?
|
|
30350
|
-
`**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ?
|
|
30351
|
-
`**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ?
|
|
30352
|
-
`**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ?
|
|
30353
|
-
`**Expectancy:** ${data.portfolioExpectancy !== null ?
|
|
30958
|
+
`**Avg Consecutive Win PNL:** ${data.portfolioAvgConsecutiveWinPnl !== null ? `${data.portfolioAvgConsecutiveWinPnl.toFixed(2)} %` : "N/A"}`,
|
|
30959
|
+
`**Avg Consecutive Loss PNL:** ${data.portfolioAvgConsecutiveLossPnl !== null ? `${data.portfolioAvgConsecutiveLossPnl.toFixed(2)} %` : "N/A"}`,
|
|
30960
|
+
`**Standard Deviation Per Trade:** ${data.portfolioStdDev !== null ? `${data.portfolioStdDev.toFixed(2)} %` : "N/A"}`,
|
|
30961
|
+
`**Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? data.portfolioSortinoRatio.toFixed(3) : "N/A"}`,
|
|
30962
|
+
`**Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? data.portfolioCalmarRatio.toFixed(3) : "N/A"}`,
|
|
30963
|
+
`**Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? data.portfolioRecoveryFactor.toFixed(3) : "N/A"}`,
|
|
30964
|
+
`**Expectancy:** ${data.portfolioExpectancy !== null ? `${data.portfolioExpectancy.toFixed(2)} %` : "N/A"}`,
|
|
30354
30965
|
"",
|
|
30355
30966
|
table,
|
|
30356
30967
|
"",
|
|
@@ -42619,6 +43230,8 @@ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOve
|
|
|
42619
43230
|
const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
|
|
42620
43231
|
const HAS_NO_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.hasNoScheduledSignal";
|
|
42621
43232
|
const COMMIT_SIGNAL_NOTIFY_METHOD_NAME = "strategy.commitSignalNotify";
|
|
43233
|
+
const CREATE_SIGNAL_METHOD_NAME = "strategy.createSignal";
|
|
43234
|
+
const GET_STRATEGY_STATUS_METHOD_NAME = "strategy.getStrategyStatus";
|
|
42622
43235
|
/**
|
|
42623
43236
|
* Cancels the scheduled signal without stopping the strategy.
|
|
42624
43237
|
*
|
|
@@ -44683,6 +45296,69 @@ async function commitSignalNotify(symbol, payload = {}) {
|
|
|
44683
45296
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
44684
45297
|
await bt.notificationHelperService.commitSignalNotify(payload, symbol, currentPrice, { strategyName, exchangeName, frameName }, isBacktest);
|
|
44685
45298
|
}
|
|
45299
|
+
/**
|
|
45300
|
+
* Queues a user-supplied signal DTO to be consumed by the next tick instead of params.getSignal.
|
|
45301
|
+
*
|
|
45302
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
45303
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
45304
|
+
* scheduled (priceOpen-awaiting) signal. The DTO is validated (reusing validateSignal) and the
|
|
45305
|
+
* call is rejected if a signal or deferred action is already in flight.
|
|
45306
|
+
*
|
|
45307
|
+
* Automatically detects backtest/live mode from execution context.
|
|
45308
|
+
*
|
|
45309
|
+
* @param symbol - Trading pair symbol
|
|
45310
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
45311
|
+
* @returns Promise that resolves when the DTO is queued
|
|
45312
|
+
*
|
|
45313
|
+
* @example
|
|
45314
|
+
* ```typescript
|
|
45315
|
+
* import { createSignal } from "backtest-kit";
|
|
45316
|
+
*
|
|
45317
|
+
* // Open immediately at current price
|
|
45318
|
+
* await createSignal("BTCUSDT", { position: "long", priceTakeProfit: 110, priceStopLoss: 90 });
|
|
45319
|
+
* ```
|
|
45320
|
+
*/
|
|
45321
|
+
async function createSignal(symbol, dto) {
|
|
45322
|
+
bt.loggerService.info(CREATE_SIGNAL_METHOD_NAME, { symbol });
|
|
45323
|
+
if (!ExecutionContextService.hasContext()) {
|
|
45324
|
+
throw new Error("createSignal requires an execution context");
|
|
45325
|
+
}
|
|
45326
|
+
if (!MethodContextService.hasContext()) {
|
|
45327
|
+
throw new Error("createSignal requires a method context");
|
|
45328
|
+
}
|
|
45329
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
45330
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
45331
|
+
await bt.strategyCoreService.createSignal(isBacktest, symbol, dto, { exchangeName, frameName, strategyName });
|
|
45332
|
+
}
|
|
45333
|
+
/**
|
|
45334
|
+
* Returns the in-memory deferred strategy-state snapshot for the current iteration: the queued
|
|
45335
|
+
* createSignal, commit queue and deferred user-action flags, plus the current pending signal id.
|
|
45336
|
+
*
|
|
45337
|
+
* Automatically detects backtest/live mode from execution context.
|
|
45338
|
+
*
|
|
45339
|
+
* @param symbol - Trading pair symbol
|
|
45340
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
45341
|
+
*
|
|
45342
|
+
* @example
|
|
45343
|
+
* ```typescript
|
|
45344
|
+
* import { getStrategyStatus } from "backtest-kit";
|
|
45345
|
+
*
|
|
45346
|
+
* const status = await getStrategyStatus("BTCUSDT");
|
|
45347
|
+
* console.log(status.pendingSignalId, status.commitQueue.length);
|
|
45348
|
+
* ```
|
|
45349
|
+
*/
|
|
45350
|
+
async function getStrategyStatus(symbol) {
|
|
45351
|
+
bt.loggerService.info(GET_STRATEGY_STATUS_METHOD_NAME, { symbol });
|
|
45352
|
+
if (!ExecutionContextService.hasContext()) {
|
|
45353
|
+
throw new Error("getStrategyStatus requires an execution context");
|
|
45354
|
+
}
|
|
45355
|
+
if (!MethodContextService.hasContext()) {
|
|
45356
|
+
throw new Error("getStrategyStatus requires a method context");
|
|
45357
|
+
}
|
|
45358
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
45359
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
45360
|
+
return await bt.strategyCoreService.getStatus(isBacktest, symbol, { exchangeName, frameName, strategyName });
|
|
45361
|
+
}
|
|
44686
45362
|
|
|
44687
45363
|
const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
|
|
44688
45364
|
/**
|
|
@@ -46354,6 +47030,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPosi
|
|
|
46354
47030
|
const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
|
|
46355
47031
|
const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
|
|
46356
47032
|
const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
|
|
47033
|
+
const BACKTEST_METHOD_NAME_CREATE_SIGNAL = "Backtest.createSignal";
|
|
47034
|
+
const BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS = "Backtest.getStrategyStatus";
|
|
46357
47035
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
|
|
46358
47036
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
|
|
46359
47037
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
|
|
@@ -48058,6 +48736,53 @@ class BacktestUtils {
|
|
|
48058
48736
|
}
|
|
48059
48737
|
await bt.strategyCoreService.closePending(true, symbol, context, payload);
|
|
48060
48738
|
};
|
|
48739
|
+
/**
|
|
48740
|
+
* Queues a user-supplied signal DTO to be consumed by the next backtest tick instead of
|
|
48741
|
+
* params.getSignal.
|
|
48742
|
+
*
|
|
48743
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
48744
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
48745
|
+
* scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
|
|
48746
|
+
*
|
|
48747
|
+
* @param symbol - Trading pair symbol
|
|
48748
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
48749
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
48750
|
+
* @returns Promise that resolves when the DTO is queued
|
|
48751
|
+
*/
|
|
48752
|
+
this.createSignal = async (symbol, context, dto) => {
|
|
48753
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_CREATE_SIGNAL, {
|
|
48754
|
+
symbol,
|
|
48755
|
+
context,
|
|
48756
|
+
});
|
|
48757
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
|
|
48758
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
|
|
48759
|
+
{
|
|
48760
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
48761
|
+
riskName &&
|
|
48762
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL);
|
|
48763
|
+
riskList &&
|
|
48764
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
|
|
48765
|
+
actions &&
|
|
48766
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_CREATE_SIGNAL));
|
|
48767
|
+
}
|
|
48768
|
+
await bt.strategyCoreService.createSignal(true, symbol, dto, context);
|
|
48769
|
+
};
|
|
48770
|
+
/**
|
|
48771
|
+
* Returns the in-memory deferred strategy-state snapshot for the current backtest iteration.
|
|
48772
|
+
*
|
|
48773
|
+
* @param symbol - Trading pair symbol
|
|
48774
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
48775
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
48776
|
+
*/
|
|
48777
|
+
this.getStrategyStatus = async (symbol, context) => {
|
|
48778
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS, {
|
|
48779
|
+
symbol,
|
|
48780
|
+
context,
|
|
48781
|
+
});
|
|
48782
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
48783
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
48784
|
+
return await bt.strategyCoreService.getStatus(true, symbol, context);
|
|
48785
|
+
};
|
|
48061
48786
|
/**
|
|
48062
48787
|
* Executes partial close at profit level (moving toward TP).
|
|
48063
48788
|
*
|
|
@@ -49016,6 +49741,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPart
|
|
|
49016
49741
|
const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
|
|
49017
49742
|
const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
|
|
49018
49743
|
const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
|
|
49744
|
+
const LIVE_METHOD_NAME_CREATE_SIGNAL = "Live.createSignal";
|
|
49745
|
+
const LIVE_METHOD_NAME_GET_STRATEGY_STATUS = "Live.getStrategyStatus";
|
|
49019
49746
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
|
|
49020
49747
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
|
|
49021
49748
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
|
|
@@ -50893,6 +51620,61 @@ class LiveUtils {
|
|
|
50893
51620
|
frameName: "",
|
|
50894
51621
|
}, payload);
|
|
50895
51622
|
};
|
|
51623
|
+
/**
|
|
51624
|
+
* Queues a user-supplied signal DTO to be consumed by the next live tick instead of
|
|
51625
|
+
* params.getSignal.
|
|
51626
|
+
*
|
|
51627
|
+
* priceOpen decides the outcome via the existing pipeline: omitted → opens immediately at the
|
|
51628
|
+
* current price; provided → opens immediately if already reached, otherwise registers a
|
|
51629
|
+
* scheduled signal. Validated, and rejected if a signal or deferred action is already in flight.
|
|
51630
|
+
*
|
|
51631
|
+
* @param symbol - Trading pair symbol
|
|
51632
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
51633
|
+
* @param dto - Signal DTO to open (priceOpen optional)
|
|
51634
|
+
* @returns Promise that resolves when the DTO is queued
|
|
51635
|
+
*/
|
|
51636
|
+
this.createSignal = async (symbol, context, dto) => {
|
|
51637
|
+
bt.loggerService.info(LIVE_METHOD_NAME_CREATE_SIGNAL, {
|
|
51638
|
+
symbol,
|
|
51639
|
+
context,
|
|
51640
|
+
});
|
|
51641
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CREATE_SIGNAL);
|
|
51642
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CREATE_SIGNAL);
|
|
51643
|
+
{
|
|
51644
|
+
const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
|
|
51645
|
+
riskName &&
|
|
51646
|
+
bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL);
|
|
51647
|
+
riskList &&
|
|
51648
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CREATE_SIGNAL));
|
|
51649
|
+
actions &&
|
|
51650
|
+
actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_CREATE_SIGNAL));
|
|
51651
|
+
}
|
|
51652
|
+
await bt.strategyCoreService.createSignal(false, symbol, dto, {
|
|
51653
|
+
strategyName: context.strategyName,
|
|
51654
|
+
exchangeName: context.exchangeName,
|
|
51655
|
+
frameName: "",
|
|
51656
|
+
});
|
|
51657
|
+
};
|
|
51658
|
+
/**
|
|
51659
|
+
* Returns the in-memory deferred strategy-state snapshot for the current live iteration.
|
|
51660
|
+
*
|
|
51661
|
+
* @param symbol - Trading pair symbol
|
|
51662
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
51663
|
+
* @returns Promise resolving to the current StrategyStatus snapshot
|
|
51664
|
+
*/
|
|
51665
|
+
this.getStrategyStatus = async (symbol, context) => {
|
|
51666
|
+
bt.loggerService.info(LIVE_METHOD_NAME_GET_STRATEGY_STATUS, {
|
|
51667
|
+
symbol,
|
|
51668
|
+
context,
|
|
51669
|
+
});
|
|
51670
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
51671
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_STRATEGY_STATUS);
|
|
51672
|
+
return await bt.strategyCoreService.getStatus(false, symbol, {
|
|
51673
|
+
strategyName: context.strategyName,
|
|
51674
|
+
exchangeName: context.exchangeName,
|
|
51675
|
+
frameName: "",
|
|
51676
|
+
});
|
|
51677
|
+
};
|
|
50896
51678
|
/**
|
|
50897
51679
|
* Executes partial close at profit level (moving toward TP).
|
|
50898
51680
|
*
|
|
@@ -67539,119 +68321,6 @@ const percentValue = (yesterdayValue, todayValue) => {
|
|
|
67539
68321
|
return yesterdayValue / todayValue - 1;
|
|
67540
68322
|
};
|
|
67541
68323
|
|
|
67542
|
-
/**
|
|
67543
|
-
* Validates ISignalDto returned by getSignal, branching on the same logic as ClientStrategy GET_SIGNAL_FN.
|
|
67544
|
-
*
|
|
67545
|
-
* When priceOpen is provided:
|
|
67546
|
-
* - If currentPrice already reached priceOpen (shouldActivateImmediately) →
|
|
67547
|
-
* validates as pending: currentPrice must be between SL and TP
|
|
67548
|
-
* - Otherwise → validates as scheduled: priceOpen must be between SL and TP
|
|
67549
|
-
*
|
|
67550
|
-
* When priceOpen is absent:
|
|
67551
|
-
* - Validates as pending: currentPrice must be between SL and TP
|
|
67552
|
-
*
|
|
67553
|
-
* Checks:
|
|
67554
|
-
* - currentPrice is a finite positive number
|
|
67555
|
-
* - Common signal fields via validateCommonSignal (position, prices, TP/SL relationships, minuteEstimatedTime)
|
|
67556
|
-
* - Position-specific immediate-close protection (pending) or activation-close protection (scheduled)
|
|
67557
|
-
*
|
|
67558
|
-
* @param signal - Signal DTO returned by getSignal
|
|
67559
|
-
* @param currentPrice - Current market price at the moment of signal creation
|
|
67560
|
-
* @returns true if signal is valid, false if validation errors were found (errors logged to console.error)
|
|
67561
|
-
*/
|
|
67562
|
-
const validateSignal = (signal, currentPrice) => {
|
|
67563
|
-
const errors = [];
|
|
67564
|
-
// ЗАЩИТА ОТ NaN/Infinity: currentPrice должна быть конечным числом
|
|
67565
|
-
{
|
|
67566
|
-
if (typeof currentPrice !== "number") {
|
|
67567
|
-
errors.push(`currentPrice must be a number type, got ${currentPrice} (${typeof currentPrice})`);
|
|
67568
|
-
}
|
|
67569
|
-
if (!isFinite(currentPrice)) {
|
|
67570
|
-
errors.push(`currentPrice must be a finite number, got ${currentPrice} (${typeof currentPrice})`);
|
|
67571
|
-
}
|
|
67572
|
-
if (isFinite(currentPrice) && currentPrice <= 0) {
|
|
67573
|
-
errors.push(`currentPrice must be positive, got ${currentPrice}`);
|
|
67574
|
-
}
|
|
67575
|
-
}
|
|
67576
|
-
if (errors.length > 0) {
|
|
67577
|
-
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
67578
|
-
return false;
|
|
67579
|
-
}
|
|
67580
|
-
try {
|
|
67581
|
-
validateCommonSignal(signal);
|
|
67582
|
-
}
|
|
67583
|
-
catch (error) {
|
|
67584
|
-
console.error(functoolsKit.getErrorMessage(error));
|
|
67585
|
-
return false;
|
|
67586
|
-
}
|
|
67587
|
-
// Определяем режим валидации по той же логике что в GET_SIGNAL_FN:
|
|
67588
|
-
// - нет priceOpen → pending (открывается по currentPrice)
|
|
67589
|
-
// - priceOpen задан и уже достигнут (shouldActivateImmediately) → pending
|
|
67590
|
-
// - priceOpen задан и ещё не достигнут → scheduled
|
|
67591
|
-
const hasPriceOpen = signal.priceOpen !== undefined;
|
|
67592
|
-
const shouldActivateImmediately = hasPriceOpen && ((signal.position === "long" && currentPrice <= signal.priceOpen) ||
|
|
67593
|
-
(signal.position === "short" && currentPrice >= signal.priceOpen));
|
|
67594
|
-
const isScheduled = hasPriceOpen && !shouldActivateImmediately;
|
|
67595
|
-
if (isScheduled) {
|
|
67596
|
-
// Scheduled: priceOpen должен быть между SL и TP (активация не даст моментального закрытия)
|
|
67597
|
-
if (signal.position === "long") {
|
|
67598
|
-
if (isFinite(signal.priceOpen)) {
|
|
67599
|
-
if (signal.priceOpen <= signal.priceStopLoss) {
|
|
67600
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67601
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
67602
|
-
}
|
|
67603
|
-
if (signal.priceOpen >= signal.priceTakeProfit) {
|
|
67604
|
-
errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67605
|
-
`Signal would close immediately on activation. This is logically impossible for LONG position.`);
|
|
67606
|
-
}
|
|
67607
|
-
}
|
|
67608
|
-
}
|
|
67609
|
-
if (signal.position === "short") {
|
|
67610
|
-
if (isFinite(signal.priceOpen)) {
|
|
67611
|
-
if (signal.priceOpen >= signal.priceStopLoss) {
|
|
67612
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67613
|
-
`Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
|
|
67614
|
-
}
|
|
67615
|
-
if (signal.priceOpen <= signal.priceTakeProfit) {
|
|
67616
|
-
errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67617
|
-
`Signal would close immediately on activation. This is logically impossible for SHORT position.`);
|
|
67618
|
-
}
|
|
67619
|
-
}
|
|
67620
|
-
}
|
|
67621
|
-
}
|
|
67622
|
-
else {
|
|
67623
|
-
// Pending: currentPrice должна быть между SL и TP (позиция не закроется сразу после открытия)
|
|
67624
|
-
if (signal.position === "long") {
|
|
67625
|
-
if (isFinite(currentPrice)) {
|
|
67626
|
-
if (currentPrice <= signal.priceStopLoss) {
|
|
67627
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67628
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
67629
|
-
}
|
|
67630
|
-
if (currentPrice >= signal.priceTakeProfit) {
|
|
67631
|
-
errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67632
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
67633
|
-
}
|
|
67634
|
-
}
|
|
67635
|
-
}
|
|
67636
|
-
if (signal.position === "short") {
|
|
67637
|
-
if (isFinite(currentPrice)) {
|
|
67638
|
-
if (currentPrice >= signal.priceStopLoss) {
|
|
67639
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
|
|
67640
|
-
`Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
|
|
67641
|
-
}
|
|
67642
|
-
if (currentPrice <= signal.priceTakeProfit) {
|
|
67643
|
-
errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
|
|
67644
|
-
`Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
|
|
67645
|
-
}
|
|
67646
|
-
}
|
|
67647
|
-
}
|
|
67648
|
-
}
|
|
67649
|
-
if (errors.length > 0) {
|
|
67650
|
-
console.error(`Invalid signal for ${signal.position} position (${signal.symbol || "empty symbol"}):\n${errors.join("\n")}`);
|
|
67651
|
-
}
|
|
67652
|
-
return !errors.length;
|
|
67653
|
-
};
|
|
67654
|
-
|
|
67655
68324
|
exports.ActionBase = ActionBase;
|
|
67656
68325
|
exports.Backtest = Backtest;
|
|
67657
68326
|
exports.Breakeven = Breakeven;
|
|
@@ -67716,6 +68385,8 @@ exports.PersistStateAdapter = PersistStateAdapter;
|
|
|
67716
68385
|
exports.PersistStateInstance = PersistStateInstance;
|
|
67717
68386
|
exports.PersistStorageAdapter = PersistStorageAdapter;
|
|
67718
68387
|
exports.PersistStorageInstance = PersistStorageInstance;
|
|
68388
|
+
exports.PersistStrategyAdapter = PersistStrategyAdapter;
|
|
68389
|
+
exports.PersistStrategyInstance = PersistStrategyInstance;
|
|
67719
68390
|
exports.Position = Position;
|
|
67720
68391
|
exports.PositionSize = PositionSize;
|
|
67721
68392
|
exports.Recent = Recent;
|
|
@@ -67768,6 +68439,7 @@ exports.commitTrailingStop = commitTrailingStop;
|
|
|
67768
68439
|
exports.commitTrailingStopCost = commitTrailingStopCost;
|
|
67769
68440
|
exports.commitTrailingTake = commitTrailingTake;
|
|
67770
68441
|
exports.commitTrailingTakeCost = commitTrailingTakeCost;
|
|
68442
|
+
exports.createSignal = createSignal;
|
|
67771
68443
|
exports.createSignalState = createSignalState;
|
|
67772
68444
|
exports.dumpAgentAnswer = dumpAgentAnswer;
|
|
67773
68445
|
exports.dumpError = dumpError;
|
|
@@ -67842,6 +68514,7 @@ exports.getSessionData = getSessionData;
|
|
|
67842
68514
|
exports.getSignalState = getSignalState;
|
|
67843
68515
|
exports.getSizingSchema = getSizingSchema;
|
|
67844
68516
|
exports.getStrategySchema = getStrategySchema;
|
|
68517
|
+
exports.getStrategyStatus = getStrategyStatus;
|
|
67845
68518
|
exports.getSymbol = getSymbol;
|
|
67846
68519
|
exports.getTimestamp = getTimestamp;
|
|
67847
68520
|
exports.getTotalClosed = getTotalClosed;
|