backtest-kit 11.7.0 → 11.8.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 +1996 -1996
- package/build/index.cjs +377 -92
- package/build/index.mjs +377 -92
- package/package.json +86 -86
- package/types.d.ts +205 -48
package/build/index.mjs
CHANGED
|
@@ -74,6 +74,7 @@ const metaServices$1 = {
|
|
|
74
74
|
contextMetaService: Symbol('contextMetaService'),
|
|
75
75
|
priceMetaService: Symbol('priceMetaService'),
|
|
76
76
|
timeMetaService: Symbol('timeMetaService'),
|
|
77
|
+
runtimeMetaService: Symbol('runtimeMetaService'),
|
|
77
78
|
};
|
|
78
79
|
const globalServices$1 = {
|
|
79
80
|
sizingGlobalService: Symbol('sizingGlobalService'),
|
|
@@ -12356,6 +12357,7 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
|
|
|
12356
12357
|
symbol,
|
|
12357
12358
|
strategyName,
|
|
12358
12359
|
exchangeName,
|
|
12360
|
+
frameName: data.frameName,
|
|
12359
12361
|
currentPrice,
|
|
12360
12362
|
data,
|
|
12361
12363
|
backtest,
|
|
@@ -12423,6 +12425,7 @@ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyN
|
|
|
12423
12425
|
symbol,
|
|
12424
12426
|
strategyName,
|
|
12425
12427
|
exchangeName,
|
|
12428
|
+
frameName: data.frameName,
|
|
12426
12429
|
currentPrice,
|
|
12427
12430
|
data,
|
|
12428
12431
|
backtest,
|
|
@@ -13995,11 +13998,51 @@ class StrategyConnectionService {
|
|
|
13995
13998
|
}
|
|
13996
13999
|
}
|
|
13997
14000
|
|
|
14001
|
+
const MS_PER_MINUTE$6 = 60000;
|
|
14002
|
+
const INTERVAL_MINUTES$7 = {
|
|
14003
|
+
"1m": 1,
|
|
14004
|
+
"3m": 3,
|
|
14005
|
+
"5m": 5,
|
|
14006
|
+
"15m": 15,
|
|
14007
|
+
"30m": 30,
|
|
14008
|
+
"1h": 60,
|
|
14009
|
+
"2h": 120,
|
|
14010
|
+
"4h": 240,
|
|
14011
|
+
"6h": 360,
|
|
14012
|
+
"8h": 480,
|
|
14013
|
+
"1d": 1440,
|
|
14014
|
+
};
|
|
14015
|
+
/**
|
|
14016
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
14017
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
14018
|
+
*
|
|
14019
|
+
* Candle timestamp convention:
|
|
14020
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
14021
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
14022
|
+
*
|
|
14023
|
+
* Adapter contract:
|
|
14024
|
+
* - Adapter must return candles with timestamp = openTime
|
|
14025
|
+
* - First returned candle.timestamp must equal aligned since
|
|
14026
|
+
* - Adapter must return exactly `limit` candles
|
|
14027
|
+
*
|
|
14028
|
+
* @param date - Date to align
|
|
14029
|
+
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
14030
|
+
* @returns New Date aligned down to interval boundary
|
|
14031
|
+
*/
|
|
14032
|
+
const alignToInterval = (date, interval) => {
|
|
14033
|
+
const minutes = INTERVAL_MINUTES$7[interval];
|
|
14034
|
+
if (minutes === undefined) {
|
|
14035
|
+
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
14036
|
+
}
|
|
14037
|
+
const intervalMs = minutes * MS_PER_MINUTE$6;
|
|
14038
|
+
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
14039
|
+
};
|
|
14040
|
+
|
|
13998
14041
|
/**
|
|
13999
14042
|
* Maps FrameInterval to minutes for timestamp calculation.
|
|
14000
14043
|
* Used to generate timeframe arrays with proper spacing.
|
|
14001
14044
|
*/
|
|
14002
|
-
const INTERVAL_MINUTES$
|
|
14045
|
+
const INTERVAL_MINUTES$6 = {
|
|
14003
14046
|
"1m": 1,
|
|
14004
14047
|
"3m": 3,
|
|
14005
14048
|
"5m": 5,
|
|
@@ -14053,7 +14096,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
|
14053
14096
|
symbol,
|
|
14054
14097
|
});
|
|
14055
14098
|
const { interval, startDate, endDate } = self.params;
|
|
14056
|
-
const intervalMinutes = INTERVAL_MINUTES$
|
|
14099
|
+
const intervalMinutes = INTERVAL_MINUTES$6[interval];
|
|
14057
14100
|
if (!intervalMinutes) {
|
|
14058
14101
|
throw new Error(`ClientFrame unknown interval: ${interval}`);
|
|
14059
14102
|
}
|
|
@@ -14062,8 +14105,14 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
|
|
|
14062
14105
|
today.setUTCHours(0, 0, 0, 0);
|
|
14063
14106
|
// Ensure endDate doesn't go beyond today
|
|
14064
14107
|
const effectiveEndDate = endDate > today ? today : endDate;
|
|
14108
|
+
// Align the iteration start down to the 1-minute boundary so every generated
|
|
14109
|
+
// timestamp lands on a clean minute, matching live mode
|
|
14110
|
+
// (LiveLogicPrivateService aligns `when` via alignToInterval(new Date(), "1m")).
|
|
14111
|
+
// Without this, a startDate carrying sub-minute (or any non-aligned) offset
|
|
14112
|
+
// would propagate that offset to every tick `when` — and therefore to
|
|
14113
|
+
// IRuntimeInfo.when handed to Cron handlers — diverging from live behaviour.
|
|
14065
14114
|
const timeframes = [];
|
|
14066
|
-
let currentDate =
|
|
14115
|
+
let currentDate = alignToInterval(startDate, "1m");
|
|
14067
14116
|
while (currentDate <= effectiveEndDate) {
|
|
14068
14117
|
timeframes.push(new Date(currentDate));
|
|
14069
14118
|
currentDate = new Date(currentDate.getTime() + intervalMinutes * 60 * 1000);
|
|
@@ -19914,8 +19963,8 @@ class BacktestLogicPrivateService {
|
|
|
19914
19963
|
}
|
|
19915
19964
|
|
|
19916
19965
|
const EMITTER_CHECK_INTERVAL = 5000;
|
|
19917
|
-
const MS_PER_MINUTE$
|
|
19918
|
-
const INTERVAL_MINUTES$
|
|
19966
|
+
const MS_PER_MINUTE$5 = 60000;
|
|
19967
|
+
const INTERVAL_MINUTES$5 = {
|
|
19919
19968
|
"1m": 1,
|
|
19920
19969
|
"3m": 3,
|
|
19921
19970
|
"5m": 5,
|
|
@@ -19930,7 +19979,7 @@ const INTERVAL_MINUTES$6 = {
|
|
|
19930
19979
|
};
|
|
19931
19980
|
const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
|
|
19932
19981
|
const tickSubject = new Subject();
|
|
19933
|
-
const intervalMs = INTERVAL_MINUTES$
|
|
19982
|
+
const intervalMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$5;
|
|
19934
19983
|
{
|
|
19935
19984
|
let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
19936
19985
|
Source.fromInterval(EMITTER_CHECK_INTERVAL)
|
|
@@ -19957,46 +20006,6 @@ const waitForCandle = async (interval) => {
|
|
|
19957
20006
|
return emitter.toPromise();
|
|
19958
20007
|
};
|
|
19959
20008
|
|
|
19960
|
-
const MS_PER_MINUTE$5 = 60000;
|
|
19961
|
-
const INTERVAL_MINUTES$5 = {
|
|
19962
|
-
"1m": 1,
|
|
19963
|
-
"3m": 3,
|
|
19964
|
-
"5m": 5,
|
|
19965
|
-
"15m": 15,
|
|
19966
|
-
"30m": 30,
|
|
19967
|
-
"1h": 60,
|
|
19968
|
-
"2h": 120,
|
|
19969
|
-
"4h": 240,
|
|
19970
|
-
"6h": 360,
|
|
19971
|
-
"8h": 480,
|
|
19972
|
-
"1d": 1440,
|
|
19973
|
-
};
|
|
19974
|
-
/**
|
|
19975
|
-
* Aligns timestamp down to the nearest interval boundary.
|
|
19976
|
-
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
19977
|
-
*
|
|
19978
|
-
* Candle timestamp convention:
|
|
19979
|
-
* - Candle timestamp = openTime (when candle opens)
|
|
19980
|
-
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
19981
|
-
*
|
|
19982
|
-
* Adapter contract:
|
|
19983
|
-
* - Adapter must return candles with timestamp = openTime
|
|
19984
|
-
* - First returned candle.timestamp must equal aligned since
|
|
19985
|
-
* - Adapter must return exactly `limit` candles
|
|
19986
|
-
*
|
|
19987
|
-
* @param date - Date to align
|
|
19988
|
-
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
19989
|
-
* @returns New Date aligned down to interval boundary
|
|
19990
|
-
*/
|
|
19991
|
-
const alignToInterval = (date, interval) => {
|
|
19992
|
-
const minutes = INTERVAL_MINUTES$5[interval];
|
|
19993
|
-
if (minutes === undefined) {
|
|
19994
|
-
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
19995
|
-
}
|
|
19996
|
-
const intervalMs = minutes * MS_PER_MINUTE$5;
|
|
19997
|
-
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
19998
|
-
};
|
|
19999
|
-
|
|
20000
20009
|
/**
|
|
20001
20010
|
* Private service for live trading orchestration using async generators.
|
|
20002
20011
|
*
|
|
@@ -36135,6 +36144,21 @@ class PriceMetaService {
|
|
|
36135
36144
|
* Instances are cached until clear() is called.
|
|
36136
36145
|
*/
|
|
36137
36146
|
this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
|
|
36147
|
+
/**
|
|
36148
|
+
* Checks if a price exists for the given key and has emitted at least one value.
|
|
36149
|
+
*
|
|
36150
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
36151
|
+
* @param context - Strategy, exchange, and frame identifiers
|
|
36152
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
36153
|
+
* @returns True if a price exists and has emitted a value, false otherwise
|
|
36154
|
+
*/
|
|
36155
|
+
this.hasPrice = (symbol, context, backtest) => {
|
|
36156
|
+
const key = CREATE_KEY_FN$b(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
36157
|
+
if (!this.getSource.has(key)) {
|
|
36158
|
+
return false;
|
|
36159
|
+
}
|
|
36160
|
+
return !!this.getSource.get(key)?.data;
|
|
36161
|
+
};
|
|
36138
36162
|
/**
|
|
36139
36163
|
* Returns the current market price for the given symbol and context.
|
|
36140
36164
|
*
|
|
@@ -36797,6 +36821,129 @@ class NotificationHelperService {
|
|
|
36797
36821
|
}
|
|
36798
36822
|
}
|
|
36799
36823
|
|
|
36824
|
+
const GET_RANGE_FN = trycatch((self, context, backtest) => {
|
|
36825
|
+
if (!backtest) {
|
|
36826
|
+
return null;
|
|
36827
|
+
}
|
|
36828
|
+
const { startDate, endDate } = self.frameSchemaService.get(context.frameName);
|
|
36829
|
+
return {
|
|
36830
|
+
from: startDate,
|
|
36831
|
+
to: endDate,
|
|
36832
|
+
};
|
|
36833
|
+
}, {
|
|
36834
|
+
fallback: (error, self) => {
|
|
36835
|
+
const message = "RuntimeMetaService GET_RANGE_FN thrown";
|
|
36836
|
+
const payload = {
|
|
36837
|
+
error: errorData(error),
|
|
36838
|
+
message: getErrorMessage(error),
|
|
36839
|
+
};
|
|
36840
|
+
self.loggerService.warn(message, payload);
|
|
36841
|
+
console.error(message, payload);
|
|
36842
|
+
errorEmitter.next(error);
|
|
36843
|
+
},
|
|
36844
|
+
defaultValue: null,
|
|
36845
|
+
});
|
|
36846
|
+
const GET_INFO_FN = trycatch((self, context) => {
|
|
36847
|
+
const { info } = self.strategySchemaService.get(context.strategyName);
|
|
36848
|
+
return info || null;
|
|
36849
|
+
}, {
|
|
36850
|
+
fallback: (error, self) => {
|
|
36851
|
+
const message = "RuntimeMetaService GET_INFO_FN thrown";
|
|
36852
|
+
const payload = {
|
|
36853
|
+
error: errorData(error),
|
|
36854
|
+
message: getErrorMessage(error),
|
|
36855
|
+
};
|
|
36856
|
+
self.loggerService.warn(message, payload);
|
|
36857
|
+
console.error(message, payload);
|
|
36858
|
+
errorEmitter.next(error);
|
|
36859
|
+
},
|
|
36860
|
+
defaultValue: null,
|
|
36861
|
+
});
|
|
36862
|
+
const GET_PRICE_FN = trycatch(async (self, symbol, context, backtest) => {
|
|
36863
|
+
return await self.priceMetaService.getCurrentPrice(symbol, context, backtest);
|
|
36864
|
+
}, {
|
|
36865
|
+
fallback: (error, self) => {
|
|
36866
|
+
const message = "RuntimeMetaService GET_PRICE_FN thrown";
|
|
36867
|
+
const payload = {
|
|
36868
|
+
error: errorData(error),
|
|
36869
|
+
message: getErrorMessage(error),
|
|
36870
|
+
};
|
|
36871
|
+
self.loggerService.warn(message, payload);
|
|
36872
|
+
console.error(message, payload);
|
|
36873
|
+
errorEmitter.next(error);
|
|
36874
|
+
},
|
|
36875
|
+
defaultValue: null,
|
|
36876
|
+
});
|
|
36877
|
+
const RuntimeMetaService = singleton(class {
|
|
36878
|
+
constructor() {
|
|
36879
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
36880
|
+
this.timeMetaService = inject(TYPES.timeMetaService);
|
|
36881
|
+
this.priceMetaService = inject(TYPES.priceMetaService);
|
|
36882
|
+
this.frameSchemaService = inject(TYPES.frameSchemaService);
|
|
36883
|
+
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
36884
|
+
/**
|
|
36885
|
+
* Fetches the time range for the current strategy execution context.
|
|
36886
|
+
*
|
|
36887
|
+
* For backtest mode, it retrieves the start and end dates from the frame schema.
|
|
36888
|
+
* For live mode, it returns null since there is no predefined time range.
|
|
36889
|
+
*
|
|
36890
|
+
* This method is memoized to optimize performance, as the time range for a given context will not change during execution.
|
|
36891
|
+
*
|
|
36892
|
+
* @param context - Strategy, exchange, and frame identifiers
|
|
36893
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
36894
|
+
* @returns An object containing 'from' and 'to' Date objects for backtest mode, or null for live mode
|
|
36895
|
+
*/
|
|
36896
|
+
this._getRange = memoize(([context, backtest]) => `${context.frameName}:${backtest ? "backtest" : "live"}`, (context, backtest) => {
|
|
36897
|
+
return GET_RANGE_FN(this, context, backtest);
|
|
36898
|
+
});
|
|
36899
|
+
/**
|
|
36900
|
+
* Fetches strategy-defined runtime information for the current execution context.
|
|
36901
|
+
*
|
|
36902
|
+
* This method retrieves the 'info' object defined in the strategy schema, which can contain any custom data the strategy wants to track at runtime.
|
|
36903
|
+
* The content of this object is not defined by the system and can be used freely by strategy implementations for monitoring, reporting, or external logic.
|
|
36904
|
+
*
|
|
36905
|
+
* This method is memoized to optimize performance, as the strategy info for a given context will not change during execution.
|
|
36906
|
+
*
|
|
36907
|
+
* @param context - Strategy, exchange, and frame identifiers
|
|
36908
|
+
* @returns The 'info' object defined in the strategy schema for the given strategy, or null if not defined
|
|
36909
|
+
*/
|
|
36910
|
+
this._getInfo = memoize(([context]) => context.strategyName, (context) => {
|
|
36911
|
+
return GET_INFO_FN(this, context);
|
|
36912
|
+
});
|
|
36913
|
+
/**
|
|
36914
|
+
* Fetches comprehensive runtime information for a given symbol and strategy context, including current price, timestamp, and strategy-specific info.
|
|
36915
|
+
*
|
|
36916
|
+
* This method aggregates data from multiple sources (time, price, frame schema, strategy schema) to provide a complete picture of the current runtime state for a strategy tick.
|
|
36917
|
+
*
|
|
36918
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
36919
|
+
* @param context - Strategy, exchange, and frame identifiers
|
|
36920
|
+
* @param backtest - True if backtest mode, false if live mode
|
|
36921
|
+
* @returns An object containing symbol, time range, strategy-defined info, context, timestamp, current price, and backtest flag
|
|
36922
|
+
*/
|
|
36923
|
+
this.getRuntimeInfo = async (symbol, context, backtest) => {
|
|
36924
|
+
this.loggerService.log("runtimeMetaService getRuntimeInfo", {
|
|
36925
|
+
symbol,
|
|
36926
|
+
context,
|
|
36927
|
+
backtest,
|
|
36928
|
+
});
|
|
36929
|
+
const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
|
|
36930
|
+
const when = new Date(timestamp);
|
|
36931
|
+
const currentPrice = await GET_PRICE_FN(this, symbol, context, backtest);
|
|
36932
|
+
const range = this._getRange(context, backtest);
|
|
36933
|
+
const info = this._getInfo(context);
|
|
36934
|
+
return {
|
|
36935
|
+
symbol,
|
|
36936
|
+
range,
|
|
36937
|
+
info,
|
|
36938
|
+
context,
|
|
36939
|
+
backtest,
|
|
36940
|
+
when,
|
|
36941
|
+
currentPrice,
|
|
36942
|
+
};
|
|
36943
|
+
};
|
|
36944
|
+
}
|
|
36945
|
+
});
|
|
36946
|
+
|
|
36800
36947
|
{
|
|
36801
36948
|
provide(TYPES.loggerService, () => new LoggerService());
|
|
36802
36949
|
}
|
|
@@ -36833,6 +36980,7 @@ class NotificationHelperService {
|
|
|
36833
36980
|
provide(TYPES.contextMetaService, () => new ContextMetaService());
|
|
36834
36981
|
provide(TYPES.priceMetaService, () => new PriceMetaService());
|
|
36835
36982
|
provide(TYPES.timeMetaService, () => new TimeMetaService());
|
|
36983
|
+
provide(TYPES.runtimeMetaService, () => new RuntimeMetaService());
|
|
36836
36984
|
}
|
|
36837
36985
|
{
|
|
36838
36986
|
provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
|
|
@@ -36936,6 +37084,7 @@ const metaServices = {
|
|
|
36936
37084
|
timeMetaService: inject(TYPES.timeMetaService),
|
|
36937
37085
|
priceMetaService: inject(TYPES.priceMetaService),
|
|
36938
37086
|
contextMetaService: inject(TYPES.contextMetaService),
|
|
37087
|
+
runtimeMetaService: inject(TYPES.runtimeMetaService),
|
|
36939
37088
|
};
|
|
36940
37089
|
const globalServices = {
|
|
36941
37090
|
sizingGlobalService: inject(TYPES.sizingGlobalService),
|
|
@@ -64591,6 +64740,20 @@ const CRON_METHOD_NAME_TICK = "CronUtils._tick";
|
|
|
64591
64740
|
const CRON_METHOD_NAME_ENABLE = "CronUtils.enable";
|
|
64592
64741
|
const CRON_METHOD_NAME_DISABLE = "CronUtils.disable";
|
|
64593
64742
|
const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
|
|
64743
|
+
/**
|
|
64744
|
+
* Watchdog timeout (ms) for a single cron handler invocation.
|
|
64745
|
+
*
|
|
64746
|
+
* A handler that does not settle within this window is treated as failed:
|
|
64747
|
+
* `_runEntry` races `entry.handler(info)` against this `sleep` and, when the
|
|
64748
|
+
* timeout wins, throws into the same `catch` as any other handler error —
|
|
64749
|
+
* surfacing `failed = true`, logging a warning, and (for periodic entries)
|
|
64750
|
+
* rolling back the watermark so the boundary is retried on the next tick.
|
|
64751
|
+
*
|
|
64752
|
+
* This guards the `singlerun`-serialised tick pipeline against a handler that
|
|
64753
|
+
* never resolves (a lost `resolve`, a hung promise with no timeout of its
|
|
64754
|
+
* own): without it such a handler would stall every subsequent tick forever.
|
|
64755
|
+
*/
|
|
64756
|
+
const CRON_HANDLER_TIMEOUT = 120000;
|
|
64594
64757
|
/**
|
|
64595
64758
|
* Local logger instance.
|
|
64596
64759
|
*
|
|
@@ -64599,6 +64762,20 @@ const CRON_METHOD_NAME_DISPOSE = "CronUtils.dispose";
|
|
|
64599
64762
|
* being bootstrapped — `Cron` can be imported and used in isolation.
|
|
64600
64763
|
*/
|
|
64601
64764
|
const LOGGER_SERVICE$1 = new LoggerService();
|
|
64765
|
+
/**
|
|
64766
|
+
* Local runtime-meta-service instance.
|
|
64767
|
+
*
|
|
64768
|
+
* Like {@link LOGGER_SERVICE}, instantiated directly via `new` rather than
|
|
64769
|
+
* resolved from the DI container so `CronUtils` carries no compile-time
|
|
64770
|
+
* dependency on a bootstrapped framework. `RuntimeMetaService` is built with
|
|
64771
|
+
* the `singleton` HOF from `di-singleton`, so `new RuntimeMetaService()`
|
|
64772
|
+
* returns the one shared singleton proxy — the same instance the rest of the
|
|
64773
|
+
* framework injects — and resolves its own dependencies lazily on first use.
|
|
64774
|
+
*
|
|
64775
|
+
* Used by {@link CronUtils._runEntry} to assemble the {@link IRuntimeInfo}
|
|
64776
|
+
* snapshot handed to each cron handler.
|
|
64777
|
+
*/
|
|
64778
|
+
const RUNTIME_META_SERVICE = new RuntimeMetaService();
|
|
64602
64779
|
/**
|
|
64603
64780
|
* Utility class for registering periodic tasks that fire on candle-interval
|
|
64604
64781
|
* boundaries of the virtual time produced by parallel backtests.
|
|
@@ -64621,8 +64798,8 @@ const LOGGER_SERVICE$1 = new LoggerService();
|
|
|
64621
64798
|
* Cron.register({
|
|
64622
64799
|
* name: "tg-signal-parser",
|
|
64623
64800
|
* interval: "1h",
|
|
64624
|
-
* handler: async (
|
|
64625
|
-
* await parseTelegramSignalsToMongo(when);
|
|
64801
|
+
* handler: async (info) => {
|
|
64802
|
+
* await parseTelegramSignalsToMongo(info.when);
|
|
64626
64803
|
* },
|
|
64627
64804
|
* });
|
|
64628
64805
|
*
|
|
@@ -64664,12 +64841,15 @@ class CronUtils {
|
|
|
64664
64841
|
* - Fire-once global: `${name}:once:g${generation}`.
|
|
64665
64842
|
* - Fire-once fan-out: `${name}:once:${symbol}:g${generation}`.
|
|
64666
64843
|
*
|
|
64667
|
-
* Value is the shared in-flight handler promise.
|
|
64668
|
-
*
|
|
64669
|
-
*
|
|
64670
|
-
*
|
|
64671
|
-
*
|
|
64672
|
-
*
|
|
64844
|
+
* Value is the shared in-flight handler promise. It resolves to a `boolean`
|
|
64845
|
+
* "failed" flag (`true` when the handler — or the runtime-info assembly —
|
|
64846
|
+
* threw), which `_tick` uses to roll back the periodic watermark of the slot
|
|
64847
|
+
* it opened so a failed boundary is retried. Every parallel `tick` for the
|
|
64848
|
+
* same slot key awaits this exact promise (mutex semantics) and is released
|
|
64849
|
+
* together when it settles. `_inFlight` is owned exclusively by `_runEntry` —
|
|
64850
|
+
* `clear()` does **not** touch it, so the singleshot promise survives
|
|
64851
|
+
* concurrent `clear` calls and continues to coordinate parallel ticks until
|
|
64852
|
+
* it settles.
|
|
64673
64853
|
*/
|
|
64674
64854
|
this._inFlight = new Map();
|
|
64675
64855
|
/**
|
|
@@ -64713,9 +64893,12 @@ class CronUtils {
|
|
|
64713
64893
|
*
|
|
64714
64894
|
* Written synchronously in `_tick` at slot-open time (before the `await`),
|
|
64715
64895
|
* so a still-in-flight handler does not let a later tick re-open the same
|
|
64716
|
-
* (or an already-passed) boundary.
|
|
64717
|
-
*
|
|
64718
|
-
*
|
|
64896
|
+
* (or an already-passed) boundary. If that handler then **fails**, the
|
|
64897
|
+
* advance is rolled back after the slot settles — the prior value is restored
|
|
64898
|
+
* (or the key deleted if there was none) — so the failed boundary is retried
|
|
64899
|
+
* on the next tick, mirroring catch-up of a skipped boundary. Fire-once
|
|
64900
|
+
* entries never touch this map — they use `_firedOnce`. Pruned by
|
|
64901
|
+
* `_clearBoundaryFor` on `register`/`unregister` and wiped by `dispose`.
|
|
64719
64902
|
*/
|
|
64720
64903
|
this._lastBoundary = new Map();
|
|
64721
64904
|
/**
|
|
@@ -64735,7 +64918,7 @@ class CronUtils {
|
|
|
64735
64918
|
* name: "fetch-funding",
|
|
64736
64919
|
* interval: "8h",
|
|
64737
64920
|
* symbols: ["BTCUSDT", "ETHUSDT"],
|
|
64738
|
-
* handler: async (
|
|
64921
|
+
* handler: async (info) => { ... },
|
|
64739
64922
|
* });
|
|
64740
64923
|
* // Later:
|
|
64741
64924
|
* dispose();
|
|
@@ -64855,7 +65038,7 @@ class CronUtils {
|
|
|
64855
65038
|
* 4. **Fire-once** (`entry.interval === undefined`):
|
|
64856
65039
|
* - If the entry's fired-once key is already in `_firedOnce`, skip.
|
|
64857
65040
|
* - Slot key: `${name}:once` (+ scope) (+ gen).
|
|
64858
|
-
* - `
|
|
65041
|
+
* - `alignedMs` = the 1-minute-aligned `when` from step 0 (`ts`).
|
|
64859
65042
|
* 5. **Periodic** (`entry.interval` set):
|
|
64860
65043
|
* - Align `when` to the entry's interval via {@link alignToInterval} to
|
|
64861
65044
|
* get `alignedMs`, the boundary this tick belongs to.
|
|
@@ -64877,32 +65060,44 @@ class CronUtils {
|
|
|
64877
65060
|
* handler is still in flight.
|
|
64878
65061
|
* - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
|
|
64879
65062
|
* 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
|
|
64880
|
-
* already exists, `await` the same promise. Otherwise
|
|
64881
|
-
*
|
|
64882
|
-
*
|
|
64883
|
-
*
|
|
64884
|
-
*
|
|
65063
|
+
* already exists, `await` the same promise. Otherwise open the slot via
|
|
65064
|
+
* {@link _runEntry} — which assembles the {@link IRuntimeInfo} snapshot
|
|
65065
|
+
* (from `symbol`, `context`, `backtest`) and invokes `entry.handler(info)`
|
|
65066
|
+
* — store the promise, and `await` it. The slot is removed in `.finally()`
|
|
65067
|
+
* so the next boundary creates a fresh promise; for fire-once entries the
|
|
65068
|
+
* fired-once key is also added to `_firedOnce` on success so subsequent
|
|
65069
|
+
* ticks skip it.
|
|
65070
|
+
* 7. After `await Promise.all`, roll back the watermark for every **periodic**
|
|
65071
|
+
* slot this tick *opened* (not the ones whose in-flight promise it reused)
|
|
65072
|
+
* whose handler reported failure, so the next tick re-opens and re-runs
|
|
65073
|
+
* that boundary.
|
|
64885
65074
|
*
|
|
64886
65075
|
* Errors thrown by `handler` are caught, logged via `console.error`, and
|
|
64887
65076
|
* **not** rethrown — a failing handler must not break the per-symbol
|
|
64888
65077
|
* tick loop or unblock other parallel backtests with an unhandled
|
|
64889
65078
|
* rejection. A failed fire-once handler is **not** marked as fired and
|
|
64890
|
-
* will retry on the next tick.
|
|
65079
|
+
* will retry on the next tick. A failed **periodic** handler likewise
|
|
65080
|
+
* retries: the boundary watermark advanced at slot-open time is rolled back
|
|
65081
|
+
* after the slot settles (step 7), so the next tick re-opens that boundary.
|
|
64891
65082
|
*
|
|
64892
65083
|
* Requires active method context and execution context.
|
|
64893
65084
|
*
|
|
64894
65085
|
* @param symbol - Trading symbol from the current tick.
|
|
64895
65086
|
* @param when - Virtual time of the current tick.
|
|
64896
65087
|
* @param backtest - `true` for backtest ticks, `false` for live ticks.
|
|
64897
|
-
* Forwarded
|
|
64898
|
-
* from the tick that **opens** a given slot is observed by all
|
|
64899
|
-
* awaiters of that slot.
|
|
65088
|
+
* Forwarded to {@link _runEntry} and surfaced as `info.backtest`. Only the
|
|
65089
|
+
* value from the tick that **opens** a given slot is observed by all
|
|
65090
|
+
* parallel awaiters of that slot.
|
|
65091
|
+
* @param context - Strategy/exchange/frame identifiers from the originating
|
|
65092
|
+
* lifecycle event, forwarded to `RuntimeMetaService.getRuntimeInfo` to
|
|
65093
|
+
* build the {@link IRuntimeInfo} snapshot passed to the handler.
|
|
64900
65094
|
* @throws Error if method or execution context is missing.
|
|
64901
65095
|
*/
|
|
64902
|
-
this._tick = async (symbol, when, backtest) => {
|
|
65096
|
+
this._tick = async (symbol, when, backtest, context) => {
|
|
64903
65097
|
LOGGER_SERVICE$1.debug(CRON_METHOD_NAME_TICK, {
|
|
64904
65098
|
symbol,
|
|
64905
65099
|
when,
|
|
65100
|
+
context,
|
|
64906
65101
|
});
|
|
64907
65102
|
if (!MethodContextService.hasContext()) {
|
|
64908
65103
|
throw new Error("CronUtils _tick requires method context");
|
|
@@ -64912,6 +65107,10 @@ class CronUtils {
|
|
|
64912
65107
|
}
|
|
64913
65108
|
const ts = alignToInterval(when, "1m").getTime();
|
|
64914
65109
|
const taskList = [];
|
|
65110
|
+
// Periodic slots THIS tick actually opened (the `!pending` branch), tracked
|
|
65111
|
+
// for watermark rollback on failure. See {@link IOpenedSlot} for what is and
|
|
65112
|
+
// is not recorded here and why.
|
|
65113
|
+
const openedList = [];
|
|
64915
65114
|
for (const { entry, generation } of this._entries.values()) {
|
|
64916
65115
|
if (entry.symbols?.length && !entry.symbols.includes(symbol)) {
|
|
64917
65116
|
continue;
|
|
@@ -64919,7 +65118,6 @@ class CronUtils {
|
|
|
64919
65118
|
const perSymbol = !!entry.symbols?.length;
|
|
64920
65119
|
const scope = perSymbol ? `:${symbol}` : "";
|
|
64921
65120
|
const genSuffix = `:g${generation}`;
|
|
64922
|
-
let aligned;
|
|
64923
65121
|
let alignedMs;
|
|
64924
65122
|
let slotKey;
|
|
64925
65123
|
let firedKey;
|
|
@@ -64931,15 +65129,13 @@ class CronUtils {
|
|
|
64931
65129
|
if (this._firedOnce.has(onceKey)) {
|
|
64932
65130
|
continue;
|
|
64933
65131
|
}
|
|
64934
|
-
aligned = alignToInterval(when, "1m");
|
|
64935
65132
|
alignedMs = ts;
|
|
64936
65133
|
slotKey = `${entry.name}:once${scope}${genSuffix}`;
|
|
64937
65134
|
firedKey = onceKey;
|
|
64938
65135
|
boundaryKey = null;
|
|
64939
65136
|
}
|
|
64940
65137
|
else {
|
|
64941
|
-
|
|
64942
|
-
alignedMs = aligned.getTime();
|
|
65138
|
+
alignedMs = alignToInterval(when, entry.interval).getTime();
|
|
64943
65139
|
boundaryKey = `${entry.name}${scope}${genSuffix}`;
|
|
64944
65140
|
const lastBoundary = this._lastBoundary.get(boundaryKey);
|
|
64945
65141
|
// Fire when the tick's aligned boundary has advanced past the last one
|
|
@@ -64957,16 +65153,70 @@ class CronUtils {
|
|
|
64957
65153
|
// Advance the watermark synchronously at slot-open time, before the
|
|
64958
65154
|
// await below. Otherwise a later tick on the same (or an already
|
|
64959
65155
|
// crossed) boundary, arriving while this handler is still in flight,
|
|
64960
|
-
// would see the stale watermark and open a duplicate slot.
|
|
65156
|
+
// would see the stale watermark and open a duplicate slot. The advance
|
|
65157
|
+
// is rolled back after the slot settles if the handler failed (see the
|
|
65158
|
+
// post-await loop below), so a failed boundary is retried next tick.
|
|
64961
65159
|
if (boundaryKey !== null) {
|
|
65160
|
+
// Capture the pre-advance value so it can be restored verbatim on
|
|
65161
|
+
// failure (undefined => the boundary had never opened => delete the
|
|
65162
|
+
// key on rollback). Read fresh here rather than reusing `lastBoundary`
|
|
65163
|
+
// above to keep the value↔slot binding local and obvious; there is no
|
|
65164
|
+
// `await` between the two reads, so they are identical.
|
|
65165
|
+
const prevBoundary = this._lastBoundary.get(boundaryKey);
|
|
64962
65166
|
this._lastBoundary.set(boundaryKey, alignedMs);
|
|
65167
|
+
pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
|
|
65168
|
+
this._inFlight.set(slotKey, pending);
|
|
65169
|
+
openedList.push({ boundaryKey, prevBoundary, pending });
|
|
65170
|
+
}
|
|
65171
|
+
else {
|
|
65172
|
+
pending = this._runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context);
|
|
65173
|
+
this._inFlight.set(slotKey, pending);
|
|
64963
65174
|
}
|
|
64964
|
-
pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
|
|
64965
|
-
this._inFlight.set(slotKey, pending);
|
|
64966
65175
|
}
|
|
64967
65176
|
taskList.push(pending);
|
|
64968
65177
|
}
|
|
64969
|
-
|
|
65178
|
+
{
|
|
65179
|
+
// Watchdog: warn (do not interrupt) if the slots this tick is awaiting
|
|
65180
|
+
// have not settled within CRON_HANDLER_TIMEOUT. We deliberately keep
|
|
65181
|
+
// awaiting Promise.all so the singlerun pipeline stays serialised and no
|
|
65182
|
+
// duplicate/zombie slots are spawned — the timer only surfaces the stall.
|
|
65183
|
+
// Use a real setTimeout/clearTimeout (not sleep) so the alarm is cancelled
|
|
65184
|
+
// the instant Promise.all resolves, rather than lingering for the full
|
|
65185
|
+
// timeout on every fast tick.
|
|
65186
|
+
const timer = setTimeout(() => {
|
|
65187
|
+
const message = `${CRON_METHOD_NAME_TICK} timed out after ${CRON_HANDLER_TIMEOUT}ms`;
|
|
65188
|
+
const payload = { symbol, when, context };
|
|
65189
|
+
LOGGER_SERVICE$1.warn(message, payload);
|
|
65190
|
+
console.error(message, payload);
|
|
65191
|
+
errorEmitter.next(new Error(message));
|
|
65192
|
+
}, CRON_HANDLER_TIMEOUT);
|
|
65193
|
+
try {
|
|
65194
|
+
await Promise.all(taskList);
|
|
65195
|
+
}
|
|
65196
|
+
finally {
|
|
65197
|
+
clearTimeout(timer);
|
|
65198
|
+
}
|
|
65199
|
+
}
|
|
65200
|
+
// Roll back the watermark for any periodic slot THIS tick opened whose
|
|
65201
|
+
// handler failed, so the next tick re-opens the same boundary and retries
|
|
65202
|
+
// it — mirroring how a skipped boundary is later caught up. Restoring
|
|
65203
|
+
// `prevBoundary` (or deleting the key when it was `undefined`) re-arms the
|
|
65204
|
+
// strict-`>` gate without disturbing any earlier already-fired boundary.
|
|
65205
|
+
// `await pending` is cheap — every promise already settled in `Promise.all`
|
|
65206
|
+
// above; we re-await via `openedList` because its entries (opened slots
|
|
65207
|
+
// only) do not line up with `taskList` indices.
|
|
65208
|
+
for (const { boundaryKey, prevBoundary, pending } of openedList) {
|
|
65209
|
+
const failed = await pending;
|
|
65210
|
+
if (!failed) {
|
|
65211
|
+
continue;
|
|
65212
|
+
}
|
|
65213
|
+
if (prevBoundary === undefined) {
|
|
65214
|
+
this._lastBoundary.delete(boundaryKey);
|
|
65215
|
+
}
|
|
65216
|
+
else {
|
|
65217
|
+
this._lastBoundary.set(boundaryKey, prevBoundary);
|
|
65218
|
+
}
|
|
65219
|
+
}
|
|
64970
65220
|
};
|
|
64971
65221
|
/**
|
|
64972
65222
|
* Subscribe `Cron` to the engine's strategy lifecycle subjects so registered
|
|
@@ -64981,7 +65231,11 @@ class CronUtils {
|
|
|
64981
65231
|
*
|
|
64982
65232
|
* All four subjects are subscribed to a single `singlerun`-wrapped
|
|
64983
65233
|
* handler that builds `_tick(event.symbol, new Date(event.timestamp),
|
|
64984
|
-
* event.backtest
|
|
65234
|
+
* event.backtest, { strategyName, exchangeName, frameName })`. The context
|
|
65235
|
+
* object is read uniformly from the event — every contract carries
|
|
65236
|
+
* `strategyName`, `exchangeName` and `frameName` at the top level (Active /
|
|
65237
|
+
* Schedule contracts gained `frameName` for exactly this reason), so no
|
|
65238
|
+
* per-event branching is needed. `singlerun` merges the four streams into one serial
|
|
64985
65239
|
* queue: at most one `_tick` runs at a time, the next waits. This matters
|
|
64986
65240
|
* because the engine can emit `beforeStart` and an immediate `idlePing`
|
|
64987
65241
|
* on the very same minute, and concurrent `_tick`s on the same
|
|
@@ -65017,7 +65271,11 @@ class CronUtils {
|
|
|
65017
65271
|
this.enable = singleshot(() => {
|
|
65018
65272
|
LOGGER_SERVICE$1.info(CRON_METHOD_NAME_ENABLE);
|
|
65019
65273
|
const handleTick = singlerun(async (event) => {
|
|
65020
|
-
return await this._tick(event.symbol, new Date(event.timestamp), event.backtest
|
|
65274
|
+
return await this._tick(event.symbol, new Date(event.timestamp), event.backtest, {
|
|
65275
|
+
strategyName: event.strategyName,
|
|
65276
|
+
exchangeName: event.exchangeName,
|
|
65277
|
+
frameName: event.frameName,
|
|
65278
|
+
});
|
|
65021
65279
|
});
|
|
65022
65280
|
const unBeforeStart = beforeStartSubject.subscribe(handleTick);
|
|
65023
65281
|
const unIdlePing = idlePingSubject.subscribe(handleTick);
|
|
@@ -65120,25 +65378,51 @@ class CronUtils {
|
|
|
65120
65378
|
/**
|
|
65121
65379
|
* Build the singleshot promise for a single in-flight slot.
|
|
65122
65380
|
*
|
|
65123
|
-
*
|
|
65124
|
-
*
|
|
65125
|
-
*
|
|
65126
|
-
*
|
|
65127
|
-
*
|
|
65128
|
-
*
|
|
65381
|
+
* Assembles the {@link IRuntimeInfo} snapshot via
|
|
65382
|
+
* `RuntimeMetaService.getRuntimeInfo(symbol, context, backtest)` and invokes
|
|
65383
|
+
* `entry.handler(info)`. Logs any error via `console.error` and **returns** a
|
|
65384
|
+
* `failed` boolean (`true` when the handler — or the runtime-info assembly —
|
|
65385
|
+
* threw) so the caller (`_tick`) can roll back the periodic watermark of the
|
|
65386
|
+
* slot it opened and retry that boundary. The error is **not** rethrown, so a
|
|
65387
|
+
* failing handler never produces an unhandled rejection. Clears the
|
|
65388
|
+
* `_inFlight` slot in `.finally()` so the next boundary produces a fresh
|
|
65389
|
+
* promise. For fire-once entries `firedKey` is added to `_firedOnce` on
|
|
65390
|
+
* success so subsequent ticks skip it.
|
|
65391
|
+
*
|
|
65392
|
+
* `getRuntimeInfo` is the user-facing aggregator: its sub-fetches (range,
|
|
65393
|
+
* info, price) are individually wrapped in `trycatch` with `null` fallbacks,
|
|
65394
|
+
* so it almost never throws for missing data. Whatever does throw — the
|
|
65395
|
+
* handler, or in rare cases `getRuntimeInfo` — is caught here and reported via
|
|
65396
|
+
* the returned `failed` flag; the watermark rollback treats both identically.
|
|
65397
|
+
*
|
|
65398
|
+
* @param context - Strategy/exchange/frame identifiers from the originating
|
|
65399
|
+
* lifecycle event, forwarded to `getRuntimeInfo` to resolve `range`/`info`.
|
|
65129
65400
|
* @param firedKey - Key to add to `_firedOnce` on success, or `null` for
|
|
65130
65401
|
* periodic entries (which never populate `_firedOnce`).
|
|
65131
|
-
* @param backtest -
|
|
65132
|
-
* "winner" tick's flag is what all parallel awaiters
|
|
65402
|
+
* @param backtest - Forwarded to `getRuntimeInfo` and surfaced as
|
|
65403
|
+
* `info.backtest`; the "winner" tick's flag is what all parallel awaiters
|
|
65404
|
+
* of this slot see.
|
|
65405
|
+
* @returns `true` if the handler (or `getRuntimeInfo`) threw, `false` on
|
|
65406
|
+
* success. `_tick` uses this to decide whether to roll back the watermark.
|
|
65133
65407
|
*/
|
|
65134
|
-
async _runEntry(entry, symbol,
|
|
65408
|
+
async _runEntry(entry, symbol, alignedMs, slotKey, firedKey, backtest, context) {
|
|
65135
65409
|
let failed = false;
|
|
65136
65410
|
try {
|
|
65137
|
-
await
|
|
65411
|
+
const info = await RUNTIME_META_SERVICE.getRuntimeInfo(symbol, context, backtest);
|
|
65412
|
+
await entry.handler(info);
|
|
65138
65413
|
}
|
|
65139
|
-
catch (
|
|
65414
|
+
catch (error) {
|
|
65140
65415
|
failed = true;
|
|
65141
|
-
|
|
65416
|
+
const message = `${CRON_METHOD_NAME_TICK} entry "${entry.name}" failed`;
|
|
65417
|
+
const payload = {
|
|
65418
|
+
symbol,
|
|
65419
|
+
alignedMs,
|
|
65420
|
+
error: errorData(error),
|
|
65421
|
+
message: getErrorMessage(error),
|
|
65422
|
+
};
|
|
65423
|
+
LOGGER_SERVICE$1.warn(message, payload);
|
|
65424
|
+
console.error(message, payload);
|
|
65425
|
+
errorEmitter.next(error);
|
|
65142
65426
|
}
|
|
65143
65427
|
finally {
|
|
65144
65428
|
this._inFlight.delete(slotKey);
|
|
@@ -65146,6 +65430,7 @@ class CronUtils {
|
|
|
65146
65430
|
this._firedOnce.add(firedKey);
|
|
65147
65431
|
}
|
|
65148
65432
|
}
|
|
65433
|
+
return failed;
|
|
65149
65434
|
}
|
|
65150
65435
|
}
|
|
65151
65436
|
/**
|
|
@@ -65159,7 +65444,7 @@ class CronUtils {
|
|
|
65159
65444
|
* Cron.register({
|
|
65160
65445
|
* name: "tg-parser",
|
|
65161
65446
|
* interval: "1h",
|
|
65162
|
-
* handler: async (
|
|
65447
|
+
* handler: async (info) => { ... },
|
|
65163
65448
|
* });
|
|
65164
65449
|
* ```
|
|
65165
65450
|
*/
|