@tradejs/node 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/ai.d.mts +4 -1
- package/dist/ai.d.ts +4 -1
- package/dist/ai.js +601 -93
- package/dist/ai.mjs +5 -3
- package/dist/backtest.d.mts +26 -2
- package/dist/backtest.d.ts +26 -2
- package/dist/backtest.js +3367 -481
- package/dist/backtest.mjs +1845 -269
- package/dist/chunk-37VNDZVX.mjs +1040 -0
- package/dist/chunk-IUZML4RK.mjs +1136 -0
- package/dist/{chunk-WGOYR6AB.mjs → chunk-QVSMINLG.mjs} +1 -1
- package/dist/{chunk-JMDYEKIO.mjs → chunk-V3YMKE4I.mjs} +1 -1
- package/dist/{chunk-JU77QVJ3.mjs → chunk-WS5DYEVZ.mjs} +59 -5
- package/dist/cli.d.mts +13 -3
- package/dist/cli.d.ts +13 -3
- package/dist/cli.js +1016 -234
- package/dist/cli.mjs +338 -65
- package/dist/connectors.js +59 -5
- package/dist/connectors.mjs +2 -2
- package/dist/registry.js +59 -5
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +29 -8
- package/dist/strategies.d.ts +29 -8
- package/dist/strategies.js +3428 -1285
- package/dist/strategies.mjs +904 -112
- package/package.json +7 -7
- package/dist/chunk-2JKX3DM7.mjs +0 -619
- package/dist/chunk-JRRG3YQG.mjs +0 -154
package/dist/backtest.js
CHANGED
|
@@ -31,19 +31,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/backtest.ts
|
|
32
32
|
var backtest_exports = {};
|
|
33
33
|
__export(backtest_exports, {
|
|
34
|
+
canRunTestsInSharedCandleLoop: () => canRunTestsInSharedCandleLoop,
|
|
34
35
|
createTestConnector: () => createTestConnector,
|
|
36
|
+
releaseTestingSymbolCache: () => releaseTestingSymbolCache,
|
|
35
37
|
resetTestingKlineCache: () => resetTestingKlineCache,
|
|
36
|
-
|
|
38
|
+
resolveExecutionCosts: () => resolveExecutionCosts,
|
|
39
|
+
testing: () => testing,
|
|
40
|
+
testingGroupInSharedCandleLoop: () => testingGroupInSharedCandleLoop
|
|
37
41
|
});
|
|
38
42
|
module.exports = __toCommonJS(backtest_exports);
|
|
39
43
|
__reExport(backtest_exports, require("@tradejs/core/backtest"), module.exports);
|
|
40
44
|
|
|
41
45
|
// src/testing.ts
|
|
42
46
|
var import_indicators3 = require("@tradejs/core/indicators");
|
|
47
|
+
var import_constants4 = require("@tradejs/core/constants");
|
|
48
|
+
var import_strategies4 = require("@tradejs/core/strategies");
|
|
43
49
|
var import_time = require("@tradejs/core/time");
|
|
44
50
|
var import_ai2 = require("@tradejs/infra/ai");
|
|
45
51
|
var import_ml2 = require("@tradejs/infra/ml");
|
|
46
|
-
var
|
|
52
|
+
var import_logger7 = require("@tradejs/infra/logger");
|
|
47
53
|
|
|
48
54
|
// src/ai.ts
|
|
49
55
|
var import_aiLanguages = require("@tradejs/infra/aiLanguages");
|
|
@@ -52,6 +58,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
|
|
|
52
58
|
|
|
53
59
|
// src/aiShared.ts
|
|
54
60
|
var MAX_AI_SERIES_POINTS = 5;
|
|
61
|
+
var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
|
|
62
|
+
"tradejs.indicators.compactSnapshot"
|
|
63
|
+
);
|
|
64
|
+
var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
|
|
55
65
|
var trimSeriesDeep = (value) => {
|
|
56
66
|
if (Array.isArray(value)) {
|
|
57
67
|
const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
|
|
@@ -73,13 +83,15 @@ var trimSeriesDeep = (value) => {
|
|
|
73
83
|
}
|
|
74
84
|
return value;
|
|
75
85
|
};
|
|
86
|
+
var buildCompactAiIndicatorsSnapshot = (value) => {
|
|
87
|
+
const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
|
|
88
|
+
if (typeof compactSnapshot === "function") {
|
|
89
|
+
return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
|
|
90
|
+
}
|
|
91
|
+
return trimSeriesDeep(value);
|
|
92
|
+
};
|
|
76
93
|
|
|
77
94
|
// src/aiMarketContext.ts
|
|
78
|
-
var SESSION_WINDOWS = [
|
|
79
|
-
{ name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
|
|
80
|
-
{ name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
|
|
81
|
-
{ name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
|
|
82
|
-
];
|
|
83
95
|
var toRecord = (value) => {
|
|
84
96
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
85
97
|
return null;
|
|
@@ -90,92 +102,485 @@ var toFiniteNumber = (value) => {
|
|
|
90
102
|
const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
|
|
91
103
|
return Number.isFinite(numeric) ? numeric : null;
|
|
92
104
|
};
|
|
93
|
-
var getLastFiniteNumber = (value) => {
|
|
94
|
-
const numeric = toFiniteNumber(value);
|
|
95
|
-
if (numeric != null) {
|
|
96
|
-
return numeric;
|
|
97
|
-
}
|
|
98
|
-
if (!Array.isArray(value)) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
102
|
-
const nested = getLastFiniteNumber(value[i]);
|
|
103
|
-
if (nested != null) {
|
|
104
|
-
return nested;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
};
|
|
109
105
|
var roundTo = (value, decimals) => {
|
|
110
106
|
const factor = 10 ** decimals;
|
|
111
107
|
return Math.round(value * factor) / factor;
|
|
112
108
|
};
|
|
113
|
-
var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
|
|
114
|
-
var buildTradingSessionContext = (timestamp) => {
|
|
115
|
-
const date = new Date(timestamp);
|
|
116
|
-
const utcHour = date.getUTCHours();
|
|
117
|
-
const utcMinute = date.getUTCMinutes();
|
|
118
|
-
const minuteUtc = utcHour * 60 + utcMinute;
|
|
119
|
-
const activeSessions = SESSION_WINDOWS.filter(
|
|
120
|
-
(session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
|
|
121
|
-
).map((session) => session.name);
|
|
122
|
-
const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
|
|
123
|
-
return {
|
|
124
|
-
timezone: "UTC",
|
|
125
|
-
utcHour,
|
|
126
|
-
utcMinute,
|
|
127
|
-
primarySession,
|
|
128
|
-
activeSessions,
|
|
129
|
-
isOverlap: activeSessions.length > 1,
|
|
130
|
-
overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
109
|
var buildMissingSpreadContext = () => ({
|
|
134
|
-
source: "
|
|
135
|
-
indicatorKey: "payload.
|
|
110
|
+
source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
111
|
+
indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
136
112
|
available: false,
|
|
137
113
|
value: null,
|
|
114
|
+
zScore: null,
|
|
138
115
|
bps: null,
|
|
139
116
|
absBps: null,
|
|
140
117
|
bias: null,
|
|
141
118
|
severity: null
|
|
142
119
|
});
|
|
143
|
-
var
|
|
120
|
+
var buildSpreadContextFromSignal = (signal) => {
|
|
121
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
122
|
+
const relative = toRecord(baseContext?.relative);
|
|
123
|
+
const execution = toRecord(relative?.execution);
|
|
124
|
+
const spread = toFiniteNumber(execution?.venueSpread);
|
|
125
|
+
const zScore = toFiniteNumber(execution?.venueSpreadZScore);
|
|
126
|
+
if (spread == null) {
|
|
127
|
+
return buildMissingSpreadContext();
|
|
128
|
+
}
|
|
144
129
|
const value = roundTo(spread, 8);
|
|
145
130
|
const bps = roundTo(value * 1e4, 2);
|
|
146
131
|
const absBps = Math.abs(bps);
|
|
147
|
-
const bias =
|
|
132
|
+
const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
|
|
148
133
|
const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
|
|
149
134
|
return {
|
|
150
|
-
source: "
|
|
151
|
-
indicatorKey: "payload.
|
|
135
|
+
source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
136
|
+
indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
152
137
|
available: true,
|
|
153
138
|
value,
|
|
139
|
+
zScore,
|
|
154
140
|
bps,
|
|
155
141
|
absBps,
|
|
156
142
|
bias,
|
|
157
143
|
severity
|
|
158
144
|
};
|
|
159
145
|
};
|
|
160
|
-
var
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
146
|
+
var buildTrueDeltaContextFromSignal = (signal) => {
|
|
147
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
148
|
+
const participation = toRecord(baseContext?.participation);
|
|
149
|
+
const delta = toRecord(participation?.delta);
|
|
150
|
+
const source = String(delta?.source ?? "");
|
|
151
|
+
const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
|
|
152
|
+
if (!delta || !isTrueDeltaSource) {
|
|
153
|
+
return {
|
|
154
|
+
source: source || null,
|
|
155
|
+
available: false,
|
|
156
|
+
buyPressurePct: null,
|
|
157
|
+
buyVolume: null,
|
|
158
|
+
sellVolume: null,
|
|
159
|
+
netDelta: null,
|
|
160
|
+
deltaPct: null,
|
|
161
|
+
signedVolumeZScore: null
|
|
162
|
+
};
|
|
164
163
|
}
|
|
165
|
-
return
|
|
164
|
+
return {
|
|
165
|
+
source,
|
|
166
|
+
available: true,
|
|
167
|
+
buyPressurePct: toFiniteNumber(delta.buyPressurePct),
|
|
168
|
+
buyVolume: toFiniteNumber(delta.buyVolume),
|
|
169
|
+
sellVolume: toFiniteNumber(delta.sellVolume),
|
|
170
|
+
netDelta: toFiniteNumber(delta.netDelta),
|
|
171
|
+
deltaPct: toFiniteNumber(delta.deltaPct),
|
|
172
|
+
signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
|
|
173
|
+
};
|
|
166
174
|
};
|
|
167
|
-
var
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
var buildTradeFlowContextFromSignal = (signal) => {
|
|
176
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
177
|
+
const participation = toRecord(baseContext?.participation);
|
|
178
|
+
const tradeFlow = toRecord(participation?.tradeFlow);
|
|
179
|
+
if (!tradeFlow) {
|
|
180
|
+
return {
|
|
181
|
+
source: null,
|
|
182
|
+
available: false,
|
|
183
|
+
interval: null,
|
|
184
|
+
stale: null,
|
|
185
|
+
trades: null,
|
|
186
|
+
buyPressurePct: null,
|
|
187
|
+
netBaseDelta: null,
|
|
188
|
+
netQuoteDelta: null
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
source: String(tradeFlow.source ?? ""),
|
|
193
|
+
available: true,
|
|
194
|
+
interval: String(tradeFlow.interval ?? ""),
|
|
195
|
+
stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
|
|
196
|
+
trades: toFiniteNumber(tradeFlow.trades),
|
|
197
|
+
buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
|
|
198
|
+
netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
|
|
199
|
+
netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
var buildMarketBreadthContextFromSignal = (signal) => {
|
|
203
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
204
|
+
const relative = toRecord(baseContext?.relative);
|
|
205
|
+
const breadth = toRecord(relative?.marketBreadth);
|
|
206
|
+
if (!breadth) {
|
|
207
|
+
return {
|
|
208
|
+
source: null,
|
|
209
|
+
available: false,
|
|
210
|
+
universe: null,
|
|
211
|
+
interval: null,
|
|
212
|
+
stale: null,
|
|
213
|
+
symbolsCount: null,
|
|
214
|
+
advanceDeclineRatio: null,
|
|
215
|
+
pctAboveMa20: null,
|
|
216
|
+
pctAboveMa50: null,
|
|
217
|
+
equalWeightedReturn: null,
|
|
218
|
+
volumeWeightedReturn: null,
|
|
219
|
+
dispersion: null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
source: String(breadth.source ?? ""),
|
|
224
|
+
available: true,
|
|
225
|
+
universe: String(breadth.universe ?? ""),
|
|
226
|
+
interval: String(breadth.interval ?? ""),
|
|
227
|
+
stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
|
|
228
|
+
symbolsCount: toFiniteNumber(breadth.symbolsCount),
|
|
229
|
+
advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
|
|
230
|
+
pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
|
|
231
|
+
pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
|
|
232
|
+
equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
|
|
233
|
+
volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
|
|
234
|
+
dispersion: toFiniteNumber(breadth.dispersion)
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
var buildTargetVsBtcContextFromSignal = (signal) => {
|
|
238
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
239
|
+
const relative = toRecord(baseContext?.relative);
|
|
240
|
+
const targetVsBtc = toRecord(relative?.targetVsBtc);
|
|
241
|
+
if (!targetVsBtc) {
|
|
242
|
+
return {
|
|
243
|
+
source: null,
|
|
244
|
+
available: false,
|
|
245
|
+
ratioReturn1h: null,
|
|
246
|
+
ratioReturn4h: null,
|
|
247
|
+
ratioReturn24h: null,
|
|
248
|
+
alphaVsBtc1h: null,
|
|
249
|
+
alphaVsBtc4h: null,
|
|
250
|
+
alphaVsBtc24h: null,
|
|
251
|
+
betaToBtc20: null,
|
|
252
|
+
correlationToBtc20: null,
|
|
253
|
+
ratioTrend: null
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
source: String(targetVsBtc.source ?? ""),
|
|
258
|
+
available: true,
|
|
259
|
+
ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
|
|
260
|
+
ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
|
|
261
|
+
ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
|
|
262
|
+
alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
|
|
263
|
+
alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
|
|
264
|
+
alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
|
|
265
|
+
betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
|
|
266
|
+
correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
|
|
267
|
+
ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
var buildBtcAltRegimeContextFromSignal = (signal) => {
|
|
271
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
272
|
+
const relative = toRecord(baseContext?.relative);
|
|
273
|
+
const btcAltRegime = toRecord(relative?.btcAltRegime);
|
|
274
|
+
if (!btcAltRegime) {
|
|
275
|
+
return {
|
|
276
|
+
source: null,
|
|
277
|
+
available: false,
|
|
278
|
+
universe: null,
|
|
279
|
+
interval: null,
|
|
280
|
+
stale: null,
|
|
281
|
+
regime: null,
|
|
282
|
+
btcReturn24h: null,
|
|
283
|
+
altBasketReturn24h: null,
|
|
284
|
+
btcVsAltReturn24h: null,
|
|
285
|
+
btcTurnoverShare24h: null,
|
|
286
|
+
btcTurnoverShareChange24h: null,
|
|
287
|
+
altVolToBtcVol24h: null,
|
|
288
|
+
altDispersion24h: null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
source: String(btcAltRegime.source ?? ""),
|
|
293
|
+
available: true,
|
|
294
|
+
universe: String(btcAltRegime.universe ?? ""),
|
|
295
|
+
interval: String(btcAltRegime.interval ?? ""),
|
|
296
|
+
stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
|
|
297
|
+
regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
|
|
298
|
+
btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
|
|
299
|
+
altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
|
|
300
|
+
btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
|
|
301
|
+
btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
|
|
302
|
+
btcTurnoverShareChange24h: toFiniteNumber(
|
|
303
|
+
btcAltRegime.btcTurnoverShareChange24h
|
|
304
|
+
),
|
|
305
|
+
altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
|
|
306
|
+
altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
var buildCmcGlobalContextFromSignal = (signal) => {
|
|
310
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
311
|
+
const relative = toRecord(baseContext?.relative);
|
|
312
|
+
const cmcGlobal = toRecord(relative?.cmcGlobal);
|
|
313
|
+
if (!cmcGlobal) {
|
|
314
|
+
return {
|
|
315
|
+
source: null,
|
|
316
|
+
available: false,
|
|
317
|
+
interval: null,
|
|
318
|
+
asOfTs: null,
|
|
319
|
+
stale: null,
|
|
320
|
+
totalMarketCapUsd: null,
|
|
321
|
+
totalVolumeUsd: null,
|
|
322
|
+
totalVolumeReportedUsd: null,
|
|
323
|
+
altMarketCapUsd: null,
|
|
324
|
+
altVolumeUsd: null,
|
|
325
|
+
altVolumeReportedUsd: null,
|
|
326
|
+
btcDominancePct: null,
|
|
327
|
+
ethDominancePct: null,
|
|
328
|
+
btcDominanceChange24hPct: null,
|
|
329
|
+
ethDominanceChange24hPct: null,
|
|
330
|
+
altMarketCapChange24hPct: null,
|
|
331
|
+
altVolumeChange24hPct: null,
|
|
332
|
+
activeCryptocurrencies: null,
|
|
333
|
+
activeExchanges: null,
|
|
334
|
+
activeMarketPairs: null,
|
|
335
|
+
altLiquidityRegime: null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
source: String(cmcGlobal.source ?? ""),
|
|
340
|
+
available: true,
|
|
341
|
+
interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
|
|
342
|
+
asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
|
|
343
|
+
stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
|
|
344
|
+
totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
|
|
345
|
+
totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
|
|
346
|
+
totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
|
|
347
|
+
altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
|
|
348
|
+
altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
|
|
349
|
+
altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
|
|
350
|
+
btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
|
|
351
|
+
ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
|
|
352
|
+
btcDominanceChange24hPct: toFiniteNumber(
|
|
353
|
+
cmcGlobal.btcDominanceChange24hPct
|
|
354
|
+
),
|
|
355
|
+
ethDominanceChange24hPct: toFiniteNumber(
|
|
356
|
+
cmcGlobal.ethDominanceChange24hPct
|
|
357
|
+
),
|
|
358
|
+
altMarketCapChange24hPct: toFiniteNumber(
|
|
359
|
+
cmcGlobal.altMarketCapChange24hPct
|
|
360
|
+
),
|
|
361
|
+
altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
|
|
362
|
+
activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
|
|
363
|
+
activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
|
|
364
|
+
activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
|
|
365
|
+
altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
var buildCmcReferenceAssetsContextFromSignal = (signal) => {
|
|
369
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
370
|
+
const relative = toRecord(baseContext?.relative);
|
|
371
|
+
const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
|
|
372
|
+
if (!cmcReferenceAssets) {
|
|
373
|
+
return {
|
|
374
|
+
source: null,
|
|
375
|
+
available: false,
|
|
376
|
+
interval: null,
|
|
377
|
+
asOfTs: null,
|
|
378
|
+
stale: null,
|
|
379
|
+
btcMarketCapUsd: null,
|
|
380
|
+
ethMarketCapUsd: null,
|
|
381
|
+
btcVolumeUsd: null,
|
|
382
|
+
ethVolumeUsd: null,
|
|
383
|
+
btcVolumeToMarketCap: null,
|
|
384
|
+
ethVolumeToMarketCap: null,
|
|
385
|
+
ethBtcMarketCapRatio: null,
|
|
386
|
+
ethBtcMarketCapRatioChange24hPct: null,
|
|
387
|
+
ethVsBtcVolumeRatio: null,
|
|
388
|
+
referenceLiquidityRegime: null
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
source: String(cmcReferenceAssets.source ?? ""),
|
|
393
|
+
available: true,
|
|
394
|
+
interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
|
|
395
|
+
asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
|
|
396
|
+
stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
|
|
397
|
+
btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
|
|
398
|
+
ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
|
|
399
|
+
btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
|
|
400
|
+
ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
|
|
401
|
+
btcVolumeToMarketCap: toFiniteNumber(
|
|
402
|
+
cmcReferenceAssets.btcVolumeToMarketCap
|
|
403
|
+
),
|
|
404
|
+
ethVolumeToMarketCap: toFiniteNumber(
|
|
405
|
+
cmcReferenceAssets.ethVolumeToMarketCap
|
|
406
|
+
),
|
|
407
|
+
ethBtcMarketCapRatio: toFiniteNumber(
|
|
408
|
+
cmcReferenceAssets.ethBtcMarketCapRatio
|
|
409
|
+
),
|
|
410
|
+
ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
|
|
411
|
+
cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
|
|
412
|
+
),
|
|
413
|
+
ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
|
|
414
|
+
referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
|
|
418
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
419
|
+
const relative = toRecord(baseContext?.relative);
|
|
420
|
+
const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
|
|
421
|
+
if (!cmcExchangeLiquidity) {
|
|
422
|
+
return {
|
|
423
|
+
source: null,
|
|
424
|
+
available: false,
|
|
425
|
+
interval: null,
|
|
426
|
+
asOfTs: null,
|
|
427
|
+
stale: null,
|
|
428
|
+
exchangesCount: null,
|
|
429
|
+
totalVolumeUsd: null,
|
|
430
|
+
totalVolumeChange24hPct: null,
|
|
431
|
+
binanceVolumeUsd: null,
|
|
432
|
+
binanceVolumeShare: null,
|
|
433
|
+
topExchangeVolumeShare: null,
|
|
434
|
+
liquidityRegime: null
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
source: String(cmcExchangeLiquidity.source ?? ""),
|
|
439
|
+
available: true,
|
|
440
|
+
interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
|
|
441
|
+
asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
|
|
442
|
+
stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
|
|
443
|
+
exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
|
|
444
|
+
totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
|
|
445
|
+
totalVolumeChange24hPct: toFiniteNumber(
|
|
446
|
+
cmcExchangeLiquidity.totalVolumeChange24hPct
|
|
447
|
+
),
|
|
448
|
+
binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
|
|
449
|
+
binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
|
|
450
|
+
topExchangeVolumeShare: toFiniteNumber(
|
|
451
|
+
cmcExchangeLiquidity.topExchangeVolumeShare
|
|
452
|
+
),
|
|
453
|
+
liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
var buildCmcFearGreedContextFromSignal = (signal) => {
|
|
457
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
458
|
+
const relative = toRecord(baseContext?.relative);
|
|
459
|
+
const cmcFearGreed = toRecord(relative?.cmcFearGreed);
|
|
460
|
+
if (!cmcFearGreed) {
|
|
461
|
+
return {
|
|
462
|
+
source: null,
|
|
463
|
+
available: false,
|
|
464
|
+
interval: null,
|
|
465
|
+
asOfTs: null,
|
|
466
|
+
stale: null,
|
|
467
|
+
value: null,
|
|
468
|
+
valueChange24h: null,
|
|
469
|
+
valueChange7d: null,
|
|
470
|
+
classification: null,
|
|
471
|
+
sentimentRegime: null
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
source: String(cmcFearGreed.source ?? ""),
|
|
476
|
+
available: true,
|
|
477
|
+
interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
|
|
478
|
+
asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
|
|
479
|
+
stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
|
|
480
|
+
value: toFiniteNumber(cmcFearGreed.value),
|
|
481
|
+
valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
|
|
482
|
+
valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
|
|
483
|
+
classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
|
|
484
|
+
sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
var buildCmcIndexesContextFromSignal = (signal) => {
|
|
488
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
489
|
+
const relative = toRecord(baseContext?.relative);
|
|
490
|
+
const cmcIndexes = toRecord(relative?.cmcIndexes);
|
|
491
|
+
if (!cmcIndexes) {
|
|
492
|
+
return {
|
|
493
|
+
source: null,
|
|
494
|
+
available: false,
|
|
495
|
+
interval: null,
|
|
496
|
+
asOfTs: null,
|
|
497
|
+
stale: null,
|
|
498
|
+
cmc100Value: null,
|
|
499
|
+
cmc100Change24hPct: null,
|
|
500
|
+
cmc100TopConstituentSymbol: null,
|
|
501
|
+
cmc100TopConstituentWeightPct: null,
|
|
502
|
+
cmc20Value: null,
|
|
503
|
+
cmc20Change24hPct: null,
|
|
504
|
+
cmc20TopConstituentSymbol: null,
|
|
505
|
+
cmc20TopConstituentWeightPct: null,
|
|
506
|
+
cmc20ToCmc100Ratio: null,
|
|
507
|
+
cmc20ToCmc100RatioChange24hPct: null,
|
|
508
|
+
indexRegime: null
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
source: String(cmcIndexes.source ?? ""),
|
|
513
|
+
available: true,
|
|
514
|
+
interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
|
|
515
|
+
asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
|
|
516
|
+
stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
|
|
517
|
+
cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
|
|
518
|
+
cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
|
|
519
|
+
cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
|
|
520
|
+
cmc100TopConstituentWeightPct: toFiniteNumber(
|
|
521
|
+
cmcIndexes.cmc100TopConstituentWeightPct
|
|
522
|
+
),
|
|
523
|
+
cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
|
|
524
|
+
cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
|
|
525
|
+
cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
|
|
526
|
+
cmc20TopConstituentWeightPct: toFiniteNumber(
|
|
527
|
+
cmcIndexes.cmc20TopConstituentWeightPct
|
|
528
|
+
),
|
|
529
|
+
cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
|
|
530
|
+
cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
|
|
531
|
+
cmcIndexes.cmc20ToCmc100RatioChange24hPct
|
|
532
|
+
),
|
|
533
|
+
indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
|
|
534
|
+
};
|
|
535
|
+
};
|
|
536
|
+
var buildReferenceTradeFlowContextFromSignal = (signal) => {
|
|
537
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
538
|
+
const relative = toRecord(baseContext?.relative);
|
|
539
|
+
const refs = toRecord(relative?.referenceTradeFlow);
|
|
540
|
+
const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
|
|
541
|
+
const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
|
|
542
|
+
const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
|
|
543
|
+
if (!refs) {
|
|
544
|
+
return {
|
|
545
|
+
source: null,
|
|
546
|
+
available: false,
|
|
547
|
+
primaryReferenceSymbol: null,
|
|
548
|
+
referenceSymbols: [],
|
|
549
|
+
primaryTradeFlowBuyPressurePct: null,
|
|
550
|
+
primaryTradeFlowStale: null
|
|
551
|
+
};
|
|
552
|
+
}
|
|
173
553
|
return {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
554
|
+
source: String(refs.source ?? ""),
|
|
555
|
+
available: true,
|
|
556
|
+
primaryReferenceSymbol,
|
|
557
|
+
referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
|
|
558
|
+
primaryTradeFlowBuyPressurePct: toFiniteNumber(
|
|
559
|
+
primaryTradeFlow?.buyPressurePct
|
|
560
|
+
),
|
|
561
|
+
primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
|
|
177
562
|
};
|
|
178
563
|
};
|
|
564
|
+
var buildAiMarketContext = (signal) => ({
|
|
565
|
+
execution: {
|
|
566
|
+
binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
|
|
567
|
+
},
|
|
568
|
+
participation: {
|
|
569
|
+
trueDelta: buildTrueDeltaContextFromSignal(signal),
|
|
570
|
+
tradeFlow: buildTradeFlowContextFromSignal(signal)
|
|
571
|
+
},
|
|
572
|
+
relative: {
|
|
573
|
+
marketBreadth: buildMarketBreadthContextFromSignal(signal),
|
|
574
|
+
targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
|
|
575
|
+
btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
|
|
576
|
+
cmcGlobal: buildCmcGlobalContextFromSignal(signal),
|
|
577
|
+
cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
|
|
578
|
+
cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
|
|
579
|
+
cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
|
|
580
|
+
cmcIndexes: buildCmcIndexesContextFromSignal(signal),
|
|
581
|
+
referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
|
|
582
|
+
}
|
|
583
|
+
});
|
|
179
584
|
|
|
180
585
|
// src/strategy/manifests.ts
|
|
181
586
|
var import_indicators = require("@tradejs/core/indicators");
|
|
@@ -184,7 +589,6 @@ var import_logger2 = require("@tradejs/infra/logger");
|
|
|
184
589
|
// src/tradejsConfig.ts
|
|
185
590
|
var import_fs = __toESM(require("fs"));
|
|
186
591
|
var import_path = __toESM(require("path"));
|
|
187
|
-
var import_module = require("module");
|
|
188
592
|
var import_url = require("url");
|
|
189
593
|
var import_config = require("@tradejs/core/config");
|
|
190
594
|
var import_logger = require("@tradejs/infra/logger");
|
|
@@ -200,6 +604,7 @@ var cachedByCwd = /* @__PURE__ */ new Map();
|
|
|
200
604
|
var announcedConfigFile = /* @__PURE__ */ new Set();
|
|
201
605
|
var tsNodeRegistered = false;
|
|
202
606
|
var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
|
|
607
|
+
var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
|
|
203
608
|
var getTradejsProjectCwd = (cwd) => {
|
|
204
609
|
const explicit = String(cwd ?? "").trim();
|
|
205
610
|
if (explicit) {
|
|
@@ -229,7 +634,14 @@ var normalizeConfig = (rawConfig) => {
|
|
|
229
634
|
...hooks ? { hooks } : {}
|
|
230
635
|
};
|
|
231
636
|
};
|
|
232
|
-
var
|
|
637
|
+
var getNodeCreateRequire = () => {
|
|
638
|
+
const builtinModule = process.getBuiltinModule?.("module");
|
|
639
|
+
if (typeof builtinModule?.createRequire === "function") {
|
|
640
|
+
return builtinModule.createRequire;
|
|
641
|
+
}
|
|
642
|
+
throw new TypeError("module.createRequire is not available");
|
|
643
|
+
};
|
|
644
|
+
var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
|
|
233
645
|
var ensureTsNodeRegistered = async () => {
|
|
234
646
|
if (tsNodeRegistered) {
|
|
235
647
|
return;
|
|
@@ -239,8 +651,8 @@ var ensureTsNodeRegistered = async () => {
|
|
|
239
651
|
tsNode.register?.({
|
|
240
652
|
transpileOnly: true,
|
|
241
653
|
compilerOptions: {
|
|
242
|
-
module: "
|
|
243
|
-
moduleResolution: "
|
|
654
|
+
module: "Node16",
|
|
655
|
+
moduleResolution: "node16"
|
|
244
656
|
}
|
|
245
657
|
});
|
|
246
658
|
tsNodeRegistered = true;
|
|
@@ -267,6 +679,42 @@ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
|
|
|
267
679
|
});
|
|
268
680
|
tsconfigPathsRegisteredByCwd.add(projectRoot);
|
|
269
681
|
};
|
|
682
|
+
var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
|
|
683
|
+
const projectRoot = getTradejsProjectCwd(cwd);
|
|
684
|
+
const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
|
|
685
|
+
if (cachedMatcher) {
|
|
686
|
+
const resolved2 = cachedMatcher(moduleName);
|
|
687
|
+
return resolved2 || null;
|
|
688
|
+
}
|
|
689
|
+
const tsconfigPathsModule = await import("tsconfig-paths");
|
|
690
|
+
const loadConfig = tsconfigPathsModule.loadConfig;
|
|
691
|
+
const createMatchPath = tsconfigPathsModule.createMatchPath;
|
|
692
|
+
if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const loadedConfig = loadConfig(projectRoot);
|
|
696
|
+
if (loadedConfig.resultType !== "success") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const matchPath = createMatchPath(
|
|
700
|
+
loadedConfig.absoluteBaseUrl,
|
|
701
|
+
loadedConfig.paths
|
|
702
|
+
);
|
|
703
|
+
const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
|
|
704
|
+
".ts",
|
|
705
|
+
".tsx",
|
|
706
|
+
".mts",
|
|
707
|
+
".cts",
|
|
708
|
+
".js",
|
|
709
|
+
".jsx",
|
|
710
|
+
".mjs",
|
|
711
|
+
".cjs",
|
|
712
|
+
".json"
|
|
713
|
+
]) || "";
|
|
714
|
+
tsconfigPathMatchersByCwd.set(projectRoot, matcher);
|
|
715
|
+
const resolved = matcher(moduleName);
|
|
716
|
+
return resolved || null;
|
|
717
|
+
};
|
|
270
718
|
var toImportSpecifier = (moduleName) => {
|
|
271
719
|
if (moduleName.startsWith("file://")) {
|
|
272
720
|
return moduleName;
|
|
@@ -325,7 +773,18 @@ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
|
|
|
325
773
|
}
|
|
326
774
|
if (isBareModuleSpecifier(normalized)) {
|
|
327
775
|
await ensureTsconfigPathsRegistered(cwd);
|
|
328
|
-
|
|
776
|
+
try {
|
|
777
|
+
return requireFn(normalized);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const resolvedByTsconfig = await resolveTsconfigPathModule(
|
|
780
|
+
normalized,
|
|
781
|
+
cwd
|
|
782
|
+
);
|
|
783
|
+
if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
|
|
784
|
+
return requireFn(resolvedByTsconfig);
|
|
785
|
+
}
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
329
788
|
}
|
|
330
789
|
try {
|
|
331
790
|
return await import(
|
|
@@ -600,6 +1059,10 @@ var strategies = new Proxy(
|
|
|
600
1059
|
}
|
|
601
1060
|
);
|
|
602
1061
|
|
|
1062
|
+
// src/strategy/policyProfiles.ts
|
|
1063
|
+
var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
|
|
1064
|
+
var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
|
|
1065
|
+
|
|
603
1066
|
// src/strategyAdapters/ai.ts
|
|
604
1067
|
var toRecord2 = (value) => {
|
|
605
1068
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -627,13 +1090,13 @@ var buildBaseAiPayload = (signal) => {
|
|
|
627
1090
|
}
|
|
628
1091
|
},
|
|
629
1092
|
figures: trimSeriesDeep(signal.figures ?? {}),
|
|
630
|
-
indicators:
|
|
1093
|
+
indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
|
|
631
1094
|
additionalIndicators: trimSeriesDeep(additionalIndicators)
|
|
632
1095
|
};
|
|
633
1096
|
};
|
|
634
1097
|
var defaultAiAdapter = {};
|
|
635
|
-
var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)
|
|
636
|
-
var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
|
|
1098
|
+
var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
|
|
1099
|
+
var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
|
|
637
1100
|
var buildAiPayloadByStrategy = (signal) => {
|
|
638
1101
|
const basePayload = buildBaseAiPayload(signal);
|
|
639
1102
|
const adapter = getSignalAiAdapter(signal);
|
|
@@ -643,165 +1106,1034 @@ var buildAiPayloadByStrategy = (signal) => {
|
|
|
643
1106
|
// src/ai.ts
|
|
644
1107
|
var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
|
|
645
1108
|
|
|
646
|
-
// src/strategyHelpers/
|
|
647
|
-
var import_indicators2 = require("@tradejs/core/indicators");
|
|
648
|
-
var import_constants = require("@tradejs/core/constants");
|
|
1109
|
+
// src/strategyHelpers/binanceMarketContext.ts
|
|
649
1110
|
var import_timescale = require("@tradejs/infra/timescale");
|
|
650
1111
|
var import_logger3 = require("@tradejs/infra/logger");
|
|
651
|
-
var
|
|
652
|
-
var
|
|
653
|
-
|
|
1112
|
+
var import_strategies = require("@tradejs/core/strategies");
|
|
1113
|
+
var DEFAULT_MAX_AGE_BY_INTERVAL = {
|
|
1114
|
+
"1m": 3 * 6e4,
|
|
1115
|
+
"5m": 10 * 6e4,
|
|
1116
|
+
"15m": 30 * 6e4,
|
|
1117
|
+
"1h": 2 * 60 * 6e4
|
|
1118
|
+
};
|
|
1119
|
+
var binanceMarketContextUnavailable = false;
|
|
1120
|
+
var referenceRowsCache = /* @__PURE__ */ new Map();
|
|
1121
|
+
var breadthCache = /* @__PURE__ */ new Map();
|
|
654
1122
|
var parseEnabledFlag = (value, env) => {
|
|
655
1123
|
const normalized = String(value ?? "").trim().toLowerCase();
|
|
656
|
-
if (!normalized)
|
|
1124
|
+
if (!normalized)
|
|
1125
|
+
return env === "BACKTEST" || env === "CRON" || env === "PARITY";
|
|
657
1126
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
1127
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
658
1128
|
if (normalized === "backtest") return env === "BACKTEST";
|
|
659
1129
|
if (normalized === "live") return env !== "BACKTEST";
|
|
660
1130
|
return false;
|
|
661
1131
|
};
|
|
662
|
-
var
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
return normalizedHours * 60 * 60 * 1e3;
|
|
1132
|
+
var toFiniteNumberOrNull = (value) => {
|
|
1133
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
1134
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
666
1135
|
};
|
|
667
|
-
var
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
);
|
|
671
|
-
|
|
1136
|
+
var signalIntervalToMarketInterval = (value) => {
|
|
1137
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1138
|
+
if (normalized === "1" || normalized === "1m") return "1m";
|
|
1139
|
+
if (normalized === "5" || normalized === "5m") return "5m";
|
|
1140
|
+
if (normalized === "60" || normalized === "1h") return "1h";
|
|
1141
|
+
return "15m";
|
|
1142
|
+
};
|
|
1143
|
+
var resolveMarketInterval = (signal, override) => override ?? signalIntervalToMarketInterval(signal.interval);
|
|
1144
|
+
var resolveBreadthUniverse = () => (process.env.BINANCE_MARKET_CONTEXT_BREADTH_UNIVERSE || "binance_top30_usdt").trim().toLowerCase();
|
|
1145
|
+
var getReferenceSymbols = () => {
|
|
1146
|
+
const symbols = (process.env.BINANCE_MARKET_CONTEXT_REFERENCE_SYMBOLS || "BTCUSDT,ETHUSDT").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
|
|
1147
|
+
return symbols.length ? [...new Set(symbols)] : ["BTCUSDT", "ETHUSDT"];
|
|
672
1148
|
};
|
|
673
|
-
var getDerivativesContextReferenceSymbols = () => [
|
|
674
|
-
...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS
|
|
675
|
-
];
|
|
676
|
-
var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
|
|
677
1149
|
var resolvePrimaryReferenceSymbol = (signalSymbol) => {
|
|
678
|
-
const symbol =
|
|
679
|
-
const referenceSymbols =
|
|
680
|
-
return referenceSymbols.
|
|
1150
|
+
const symbol = signalSymbol.trim().toUpperCase();
|
|
1151
|
+
const referenceSymbols = getReferenceSymbols();
|
|
1152
|
+
return referenceSymbols.includes(symbol) ? symbol : referenceSymbols[0];
|
|
681
1153
|
};
|
|
682
|
-
var
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1154
|
+
var hasBaseContext = (signal) => Boolean(
|
|
1155
|
+
signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
|
|
1156
|
+
);
|
|
1157
|
+
var isBinanceMarketContextEnabled = (env) => parseEnabledFlag(process.env.BINANCE_MARKET_CONTEXT_ENABLED, env);
|
|
1158
|
+
var toTradeFlowContext = (row, interval) => row ? {
|
|
1159
|
+
source: "binance_agg_trades",
|
|
1160
|
+
interval,
|
|
1161
|
+
asOfTs: row.ts.getTime(),
|
|
1162
|
+
ageMs: row.ageMs,
|
|
1163
|
+
stale: row.stale,
|
|
1164
|
+
trades: toFiniteNumberOrNull(row.trades),
|
|
1165
|
+
buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
|
|
1166
|
+
buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
|
|
1167
|
+
sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
|
|
1168
|
+
buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
|
|
1169
|
+
sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
|
|
1170
|
+
netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
|
|
1171
|
+
netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
|
|
1172
|
+
} : null;
|
|
1173
|
+
var getCachedReferenceRows = ({
|
|
1174
|
+
referenceSymbols,
|
|
1175
|
+
interval,
|
|
1176
|
+
timestamp,
|
|
1177
|
+
maxAgeMs
|
|
1178
|
+
}) => {
|
|
1179
|
+
const key = `${referenceSymbols.join(",")}:${interval}:${timestamp}:${maxAgeMs}`;
|
|
1180
|
+
const cached = referenceRowsCache.get(key);
|
|
1181
|
+
if (cached) return cached;
|
|
1182
|
+
const promise = Promise.all(
|
|
1183
|
+
referenceSymbols.map(async (symbol) => {
|
|
1184
|
+
const tradeFlow = await (0, import_timescale.getLatestMarketTradeFlow)({
|
|
1185
|
+
symbol,
|
|
1186
|
+
interval,
|
|
1187
|
+
atMs: timestamp,
|
|
1188
|
+
maxAgeMs
|
|
1189
|
+
});
|
|
1190
|
+
return {
|
|
1191
|
+
symbol,
|
|
1192
|
+
tradeFlow: toTradeFlowContext(tradeFlow, interval)
|
|
1193
|
+
};
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
referenceRowsCache.set(key, promise);
|
|
1197
|
+
return promise;
|
|
695
1198
|
};
|
|
696
|
-
var
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1199
|
+
var getCachedBreadth = ({
|
|
1200
|
+
breadthUniverse,
|
|
1201
|
+
interval,
|
|
1202
|
+
timestamp,
|
|
1203
|
+
maxAgeMs
|
|
1204
|
+
}) => {
|
|
1205
|
+
const key = `${breadthUniverse}:${interval}:${timestamp}:${maxAgeMs}`;
|
|
1206
|
+
const cached = breadthCache.get(key);
|
|
1207
|
+
if (cached) return cached;
|
|
1208
|
+
const promise = (0, import_timescale.getLatestMarketBreadth)({
|
|
1209
|
+
universe: breadthUniverse,
|
|
1210
|
+
interval,
|
|
1211
|
+
atMs: timestamp,
|
|
1212
|
+
maxAgeMs
|
|
1213
|
+
});
|
|
1214
|
+
breadthCache.set(key, promise);
|
|
1215
|
+
return promise;
|
|
1216
|
+
};
|
|
1217
|
+
var enrichSignalWithBinanceMarketContext = async (params) => {
|
|
1218
|
+
const {
|
|
1219
|
+
signal,
|
|
1220
|
+
env,
|
|
1221
|
+
enabled = isBinanceMarketContextEnabled(env),
|
|
1222
|
+
interval = resolveMarketInterval(signal, params.interval),
|
|
1223
|
+
breadthUniverse = resolveBreadthUniverse(),
|
|
1224
|
+
maxAgeMs = DEFAULT_MAX_AGE_BY_INTERVAL[interval]
|
|
1225
|
+
} = params;
|
|
1226
|
+
if (signal.universe === "tradfi" || !enabled || binanceMarketContextUnavailable || !hasBaseContext(signal)) {
|
|
700
1227
|
return false;
|
|
701
1228
|
}
|
|
702
1229
|
try {
|
|
703
|
-
const
|
|
704
|
-
const
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
symbol,
|
|
718
|
-
direction: signal.direction,
|
|
719
|
-
timestamp: signal.timestamp,
|
|
720
|
-
rowsByInterval,
|
|
721
|
-
intervals
|
|
722
|
-
})
|
|
723
|
-
];
|
|
1230
|
+
const referenceSymbols = getReferenceSymbols();
|
|
1231
|
+
const primaryReferenceSymbol = resolvePrimaryReferenceSymbol(signal.symbol);
|
|
1232
|
+
const [referenceRows, breadth] = await Promise.all([
|
|
1233
|
+
getCachedReferenceRows({
|
|
1234
|
+
referenceSymbols,
|
|
1235
|
+
interval,
|
|
1236
|
+
timestamp: signal.timestamp,
|
|
1237
|
+
maxAgeMs
|
|
1238
|
+
}),
|
|
1239
|
+
getCachedBreadth({
|
|
1240
|
+
breadthUniverse,
|
|
1241
|
+
interval,
|
|
1242
|
+
timestamp: signal.timestamp,
|
|
1243
|
+
maxAgeMs
|
|
724
1244
|
})
|
|
1245
|
+
]);
|
|
1246
|
+
const tradeFlowBySymbol = Object.fromEntries(
|
|
1247
|
+
referenceRows.filter((row) => row.tradeFlow).map((row) => [row.symbol, row.tradeFlow])
|
|
725
1248
|
);
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1249
|
+
const targetReferenceSymbol = signal.symbol.trim().toUpperCase();
|
|
1250
|
+
const targetTradeFlow = tradeFlowBySymbol[targetReferenceSymbol];
|
|
1251
|
+
if (!Object.keys(tradeFlowBySymbol).length && !breadth) {
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
const baseContext = signal.additionalIndicators.baseContext;
|
|
732
1255
|
signal.additionalIndicators = {
|
|
733
|
-
...signal.additionalIndicators
|
|
734
|
-
|
|
1256
|
+
...signal.additionalIndicators,
|
|
1257
|
+
baseContext: {
|
|
1258
|
+
...baseContext,
|
|
1259
|
+
participation: {
|
|
1260
|
+
...baseContext.participation,
|
|
1261
|
+
...targetTradeFlow ? {
|
|
1262
|
+
tradeFlow: targetTradeFlow
|
|
1263
|
+
} : {}
|
|
1264
|
+
},
|
|
1265
|
+
relative: {
|
|
1266
|
+
...baseContext.relative,
|
|
1267
|
+
execution: {
|
|
1268
|
+
...baseContext.relative.execution
|
|
1269
|
+
},
|
|
1270
|
+
...Object.keys(tradeFlowBySymbol).length ? {
|
|
1271
|
+
referenceTradeFlow: {
|
|
1272
|
+
source: "binance_reference_market",
|
|
1273
|
+
primaryReferenceSymbol,
|
|
1274
|
+
referenceSymbols,
|
|
1275
|
+
tradeFlowBySymbol
|
|
1276
|
+
}
|
|
1277
|
+
} : {},
|
|
1278
|
+
...breadth ? {
|
|
1279
|
+
marketBreadth: {
|
|
1280
|
+
source: "binance_klines",
|
|
1281
|
+
universe: breadth.universe,
|
|
1282
|
+
interval,
|
|
1283
|
+
asOfTs: breadth.ts.getTime(),
|
|
1284
|
+
ageMs: breadth.ageMs,
|
|
1285
|
+
stale: breadth.stale,
|
|
1286
|
+
symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
|
|
1287
|
+
advancers: toFiniteNumberOrNull(breadth.advancers),
|
|
1288
|
+
decliners: toFiniteNumberOrNull(breadth.decliners),
|
|
1289
|
+
unchanged: toFiniteNumberOrNull(breadth.unchanged),
|
|
1290
|
+
advanceDeclineRatio: toFiniteNumberOrNull(
|
|
1291
|
+
breadth.advanceDeclineRatio
|
|
1292
|
+
),
|
|
1293
|
+
pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
|
|
1294
|
+
pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
|
|
1295
|
+
equalWeightedReturn: toFiniteNumberOrNull(
|
|
1296
|
+
breadth.equalWeightedReturn
|
|
1297
|
+
),
|
|
1298
|
+
volumeWeightedReturn: toFiniteNumberOrNull(
|
|
1299
|
+
breadth.volumeWeightedReturn
|
|
1300
|
+
),
|
|
1301
|
+
dispersion: toFiniteNumberOrNull(breadth.dispersion)
|
|
1302
|
+
},
|
|
1303
|
+
btcAltRegime: {
|
|
1304
|
+
source: "binance_klines",
|
|
1305
|
+
universe: breadth.universe,
|
|
1306
|
+
interval,
|
|
1307
|
+
asOfTs: breadth.ts.getTime(),
|
|
1308
|
+
ageMs: breadth.ageMs,
|
|
1309
|
+
stale: breadth.stale,
|
|
1310
|
+
btcReturn1h: toFiniteNumberOrNull(breadth.btcReturn1h),
|
|
1311
|
+
btcReturn4h: toFiniteNumberOrNull(breadth.btcReturn4h),
|
|
1312
|
+
btcReturn24h: toFiniteNumberOrNull(breadth.btcReturn24h),
|
|
1313
|
+
altBasketReturn1h: toFiniteNumberOrNull(
|
|
1314
|
+
breadth.altBasketReturn1h
|
|
1315
|
+
),
|
|
1316
|
+
altBasketReturn4h: toFiniteNumberOrNull(
|
|
1317
|
+
breadth.altBasketReturn4h
|
|
1318
|
+
),
|
|
1319
|
+
altBasketReturn24h: toFiniteNumberOrNull(
|
|
1320
|
+
breadth.altBasketReturn24h
|
|
1321
|
+
),
|
|
1322
|
+
btcVsAltReturn1h: toFiniteNumberOrNull(
|
|
1323
|
+
breadth.btcVsAltReturn1h
|
|
1324
|
+
),
|
|
1325
|
+
btcVsAltReturn4h: toFiniteNumberOrNull(
|
|
1326
|
+
breadth.btcVsAltReturn4h
|
|
1327
|
+
),
|
|
1328
|
+
btcVsAltReturn24h: toFiniteNumberOrNull(
|
|
1329
|
+
breadth.btcVsAltReturn24h
|
|
1330
|
+
),
|
|
1331
|
+
btcTurnoverShare1h: toFiniteNumberOrNull(
|
|
1332
|
+
breadth.btcTurnoverShare1h
|
|
1333
|
+
),
|
|
1334
|
+
btcTurnoverShare24h: toFiniteNumberOrNull(
|
|
1335
|
+
breadth.btcTurnoverShare24h
|
|
1336
|
+
),
|
|
1337
|
+
btcTurnoverShareChange24h: toFiniteNumberOrNull(
|
|
1338
|
+
breadth.btcTurnoverShareChange24h
|
|
1339
|
+
),
|
|
1340
|
+
altVolToBtcVol24h: toFiniteNumberOrNull(
|
|
1341
|
+
breadth.altVolToBtcVol24h
|
|
1342
|
+
),
|
|
1343
|
+
altDispersion24h: toFiniteNumberOrNull(
|
|
1344
|
+
breadth.altDispersion24h
|
|
1345
|
+
),
|
|
1346
|
+
regime: breadth.btcAltRegime ?? "unknown"
|
|
1347
|
+
}
|
|
1348
|
+
} : {}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
735
1351
|
};
|
|
1352
|
+
(0, import_strategies.refreshSignalBaseContextGateFeatures)(signal);
|
|
736
1353
|
return true;
|
|
737
1354
|
} catch (error) {
|
|
738
|
-
|
|
1355
|
+
binanceMarketContextUnavailable = true;
|
|
739
1356
|
import_logger3.logger.warn(
|
|
740
|
-
"
|
|
1357
|
+
"Binance market context disabled after Timescale read failure: %s",
|
|
741
1358
|
String(error)
|
|
742
1359
|
);
|
|
743
1360
|
return false;
|
|
744
1361
|
}
|
|
745
1362
|
};
|
|
746
1363
|
|
|
747
|
-
// src/
|
|
748
|
-
var
|
|
749
|
-
|
|
1364
|
+
// src/strategyHelpers/coinMarketCapContext.ts
|
|
1365
|
+
var import_strategies2 = require("@tradejs/core/strategies");
|
|
1366
|
+
var import_logger4 = require("@tradejs/infra/logger");
|
|
1367
|
+
var import_timescale2 = require("@tradejs/infra/timescale");
|
|
1368
|
+
var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
|
|
1369
|
+
var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
|
|
1370
|
+
var SOURCE_REFERENCE = "coinmarketcap_reference_asset";
|
|
1371
|
+
var SOURCE_EXCHANGE_LIQUIDITY = "coinmarketcap_exchange_liquidity";
|
|
1372
|
+
var SOURCE_FEAR_GREED = "coinmarketcap_fear_greed";
|
|
1373
|
+
var SOURCE_INDEX = "coinmarketcap_index";
|
|
1374
|
+
var DAY_MS = 864e5;
|
|
1375
|
+
var coinMarketCapContextUnavailable = false;
|
|
1376
|
+
var globalContextCache = /* @__PURE__ */ new Map();
|
|
1377
|
+
var referenceContextCache = /* @__PURE__ */ new Map();
|
|
1378
|
+
var exchangeLiquidityContextCache = /* @__PURE__ */ new Map();
|
|
1379
|
+
var fearGreedContextCache = /* @__PURE__ */ new Map();
|
|
1380
|
+
var indexContextCache = /* @__PURE__ */ new Map();
|
|
1381
|
+
var parseEnabledFlag2 = (value, env) => {
|
|
1382
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1383
|
+
if (!normalized) {
|
|
1384
|
+
return env === "BACKTEST" || env === "PARITY" || env === "CRON";
|
|
1385
|
+
}
|
|
1386
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
1387
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
1388
|
+
if (normalized === "backtest") return env === "BACKTEST";
|
|
1389
|
+
if (normalized === "live") return env !== "BACKTEST";
|
|
1390
|
+
return false;
|
|
750
1391
|
};
|
|
751
|
-
var
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
return {
|
|
755
|
-
...defaultMlAdapter,
|
|
756
|
-
...strategyAdapter
|
|
757
|
-
};
|
|
1392
|
+
var asInt = (value, fallback) => {
|
|
1393
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
1394
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
758
1395
|
};
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return getStrategyMlAdapter(strategyName).normalizeStrategyConfig?.(
|
|
763
|
-
strategyConfig
|
|
764
|
-
);
|
|
1396
|
+
var toFiniteNumberOrNull2 = (value) => {
|
|
1397
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
|
|
1398
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
765
1399
|
};
|
|
766
|
-
var
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
1400
|
+
var safeDivide = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
|
|
1401
|
+
var hasBaseContext2 = (signal) => Boolean(
|
|
1402
|
+
signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
|
|
1403
|
+
);
|
|
1404
|
+
var resolveMaxAgeMs = () => asInt(process.env.COINMARKETCAP_CONTEXT_MAX_AGE_MS, DEFAULT_MAX_AGE_MS);
|
|
1405
|
+
var toAltLiquidityRegime = ({
|
|
1406
|
+
stale,
|
|
1407
|
+
btcDominanceChange24hPct,
|
|
1408
|
+
altMarketCapChange24hPct,
|
|
1409
|
+
altVolumeChange24hPct
|
|
1410
|
+
}) => {
|
|
1411
|
+
if (stale) return "unknown";
|
|
1412
|
+
if (altMarketCapChange24hPct != null && altMarketCapChange24hPct <= -0.03 || altVolumeChange24hPct != null && altVolumeChange24hPct <= -0.15) {
|
|
1413
|
+
return "risk_off";
|
|
1414
|
+
}
|
|
1415
|
+
if (btcDominanceChange24hPct != null && btcDominanceChange24hPct >= 0.25) {
|
|
1416
|
+
return "btc_favored";
|
|
1417
|
+
}
|
|
1418
|
+
if (btcDominanceChange24hPct != null && btcDominanceChange24hPct <= -0.25 && (altMarketCapChange24hPct == null || altMarketCapChange24hPct >= 0)) {
|
|
1419
|
+
return "alt_friendly";
|
|
1420
|
+
}
|
|
1421
|
+
return "neutral";
|
|
787
1422
|
};
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
var getConnectorRegistryState = (cwd = getTradejsProjectCwd()) => {
|
|
798
|
-
const projectRoot = getTradejsProjectCwd(cwd);
|
|
799
|
-
let state = registryStateByProjectRoot2.get(projectRoot);
|
|
800
|
-
if (!state) {
|
|
801
|
-
state = createConnectorRegistryState();
|
|
802
|
-
registryStateByProjectRoot2.set(projectRoot, state);
|
|
1423
|
+
var toReferenceLiquidityRegime = ({
|
|
1424
|
+
stale,
|
|
1425
|
+
ethBtcMarketCapRatioChange24hPct,
|
|
1426
|
+
ethVsBtcVolumeRatio
|
|
1427
|
+
}) => {
|
|
1428
|
+
if (stale) return "unknown";
|
|
1429
|
+
if (ethVsBtcVolumeRatio != null && ethVsBtcVolumeRatio < 0.15) return "thin";
|
|
1430
|
+
if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct >= 0.01) {
|
|
1431
|
+
return "eth_led";
|
|
803
1432
|
}
|
|
804
|
-
|
|
1433
|
+
if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct <= -0.01) {
|
|
1434
|
+
return "btc_led";
|
|
1435
|
+
}
|
|
1436
|
+
return "balanced";
|
|
1437
|
+
};
|
|
1438
|
+
var toExchangeLiquidityRegime = ({
|
|
1439
|
+
stale,
|
|
1440
|
+
totalVolumeChange24hPct,
|
|
1441
|
+
fallback
|
|
1442
|
+
}) => {
|
|
1443
|
+
if (stale) return "unknown";
|
|
1444
|
+
if (totalVolumeChange24hPct != null && totalVolumeChange24hPct >= 0.15) {
|
|
1445
|
+
return "expanding";
|
|
1446
|
+
}
|
|
1447
|
+
if (totalVolumeChange24hPct != null && totalVolumeChange24hPct <= -0.15) {
|
|
1448
|
+
return "contracting";
|
|
1449
|
+
}
|
|
1450
|
+
return fallback;
|
|
1451
|
+
};
|
|
1452
|
+
var toIndexRegime = ({
|
|
1453
|
+
stale,
|
|
1454
|
+
cmc100Change24hPct,
|
|
1455
|
+
cmc20Change24hPct,
|
|
1456
|
+
cmc20ToCmc100RatioChange24hPct
|
|
1457
|
+
}) => {
|
|
1458
|
+
if (stale) return "unknown";
|
|
1459
|
+
if (cmc100Change24hPct == null && cmc20Change24hPct == null) {
|
|
1460
|
+
return "unknown";
|
|
1461
|
+
}
|
|
1462
|
+
if ((cmc100Change24hPct ?? 0) <= -0.02 && (cmc20Change24hPct ?? 0) <= -0.02) {
|
|
1463
|
+
return "risk_off";
|
|
1464
|
+
}
|
|
1465
|
+
if ((cmc20ToCmc100RatioChange24hPct ?? 0) >= 5e-3) {
|
|
1466
|
+
return "top20_led";
|
|
1467
|
+
}
|
|
1468
|
+
if ((cmc20ToCmc100RatioChange24hPct ?? 0) <= -5e-3) {
|
|
1469
|
+
return "large_cap_led";
|
|
1470
|
+
}
|
|
1471
|
+
return "balanced";
|
|
1472
|
+
};
|
|
1473
|
+
var getCachedGlobalContext = ({
|
|
1474
|
+
timestamp,
|
|
1475
|
+
maxAgeMs
|
|
1476
|
+
}) => {
|
|
1477
|
+
const key = `${SOURCE_GLOBAL_DAILY}:${timestamp}:${maxAgeMs}`;
|
|
1478
|
+
const cached = globalContextCache.get(key);
|
|
1479
|
+
if (cached) return cached;
|
|
1480
|
+
const promise = (0, import_timescale2.getLatestMarketGlobalContext)({
|
|
1481
|
+
source: SOURCE_GLOBAL_DAILY,
|
|
1482
|
+
atMs: timestamp,
|
|
1483
|
+
maxAgeMs
|
|
1484
|
+
});
|
|
1485
|
+
globalContextCache.set(key, promise);
|
|
1486
|
+
return promise;
|
|
1487
|
+
};
|
|
1488
|
+
var getCachedReferenceContexts = ({
|
|
1489
|
+
timestamp,
|
|
1490
|
+
maxAgeMs
|
|
1491
|
+
}) => {
|
|
1492
|
+
const key = `${SOURCE_REFERENCE}:1d:${timestamp}:${maxAgeMs}`;
|
|
1493
|
+
const cached = referenceContextCache.get(key);
|
|
1494
|
+
if (cached) return cached;
|
|
1495
|
+
const promise = (0, import_timescale2.getLatestMarketReferenceAssetContexts)({
|
|
1496
|
+
source: SOURCE_REFERENCE,
|
|
1497
|
+
symbols: ["BTCUSDT", "ETHUSDT"],
|
|
1498
|
+
interval: "1d",
|
|
1499
|
+
atMs: timestamp,
|
|
1500
|
+
maxAgeMs
|
|
1501
|
+
});
|
|
1502
|
+
referenceContextCache.set(key, promise);
|
|
1503
|
+
return promise;
|
|
1504
|
+
};
|
|
1505
|
+
var getCachedExchangeLiquidityContext = ({
|
|
1506
|
+
timestamp,
|
|
1507
|
+
maxAgeMs
|
|
1508
|
+
}) => {
|
|
1509
|
+
const key = `${SOURCE_EXCHANGE_LIQUIDITY}:1d:${timestamp}:${maxAgeMs}`;
|
|
1510
|
+
const cached = exchangeLiquidityContextCache.get(key);
|
|
1511
|
+
if (cached) return cached;
|
|
1512
|
+
const promise = (0, import_timescale2.getLatestMarketCmcExchangeLiquidityContext)({
|
|
1513
|
+
source: SOURCE_EXCHANGE_LIQUIDITY,
|
|
1514
|
+
interval: "1d",
|
|
1515
|
+
atMs: timestamp,
|
|
1516
|
+
maxAgeMs
|
|
1517
|
+
});
|
|
1518
|
+
exchangeLiquidityContextCache.set(key, promise);
|
|
1519
|
+
return promise;
|
|
1520
|
+
};
|
|
1521
|
+
var getCachedFearGreedContext = ({
|
|
1522
|
+
timestamp,
|
|
1523
|
+
maxAgeMs
|
|
1524
|
+
}) => {
|
|
1525
|
+
const key = `${SOURCE_FEAR_GREED}:1d:${timestamp}:${maxAgeMs}`;
|
|
1526
|
+
const cached = fearGreedContextCache.get(key);
|
|
1527
|
+
if (cached) return cached;
|
|
1528
|
+
const promise = (0, import_timescale2.getLatestMarketCmcFearGreedContext)({
|
|
1529
|
+
source: SOURCE_FEAR_GREED,
|
|
1530
|
+
interval: "1d",
|
|
1531
|
+
atMs: timestamp,
|
|
1532
|
+
maxAgeMs
|
|
1533
|
+
});
|
|
1534
|
+
fearGreedContextCache.set(key, promise);
|
|
1535
|
+
return promise;
|
|
1536
|
+
};
|
|
1537
|
+
var getCachedIndexContexts = ({
|
|
1538
|
+
timestamp,
|
|
1539
|
+
maxAgeMs
|
|
1540
|
+
}) => {
|
|
1541
|
+
const key = `${SOURCE_INDEX}:1d:${timestamp}:${maxAgeMs}`;
|
|
1542
|
+
const cached = indexContextCache.get(key);
|
|
1543
|
+
if (cached) return cached;
|
|
1544
|
+
const promise = (0, import_timescale2.getLatestMarketCmcIndexContexts)({
|
|
1545
|
+
source: SOURCE_INDEX,
|
|
1546
|
+
indexSlugs: ["cmc100", "cmc20"],
|
|
1547
|
+
interval: "1d",
|
|
1548
|
+
atMs: timestamp,
|
|
1549
|
+
maxAgeMs
|
|
1550
|
+
});
|
|
1551
|
+
indexContextCache.set(key, promise);
|
|
1552
|
+
return promise;
|
|
1553
|
+
};
|
|
1554
|
+
var isCoinMarketCapContextEnabled = (env) => parseEnabledFlag2(process.env.COINMARKETCAP_CONTEXT_ENABLED, env);
|
|
1555
|
+
var enrichSignalWithCoinMarketCapContext = async (params) => {
|
|
1556
|
+
const {
|
|
1557
|
+
signal,
|
|
1558
|
+
env,
|
|
1559
|
+
enabled = isCoinMarketCapContextEnabled(env),
|
|
1560
|
+
maxAgeMs = resolveMaxAgeMs()
|
|
1561
|
+
} = params;
|
|
1562
|
+
if (signal.universe === "tradfi" || !enabled || coinMarketCapContextUnavailable || !hasBaseContext2(signal)) {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const [
|
|
1567
|
+
globalDailyRow,
|
|
1568
|
+
dailyReferences,
|
|
1569
|
+
previousDailyReferences,
|
|
1570
|
+
exchangeLiquidityRow,
|
|
1571
|
+
fearGreedRow,
|
|
1572
|
+
indexRows
|
|
1573
|
+
] = await Promise.all([
|
|
1574
|
+
getCachedGlobalContext({
|
|
1575
|
+
timestamp: signal.timestamp,
|
|
1576
|
+
maxAgeMs
|
|
1577
|
+
}),
|
|
1578
|
+
getCachedReferenceContexts({
|
|
1579
|
+
timestamp: signal.timestamp,
|
|
1580
|
+
maxAgeMs
|
|
1581
|
+
}),
|
|
1582
|
+
getCachedReferenceContexts({
|
|
1583
|
+
timestamp: signal.timestamp - DAY_MS,
|
|
1584
|
+
maxAgeMs: maxAgeMs + DAY_MS
|
|
1585
|
+
}),
|
|
1586
|
+
getCachedExchangeLiquidityContext({
|
|
1587
|
+
timestamp: signal.timestamp,
|
|
1588
|
+
maxAgeMs
|
|
1589
|
+
}),
|
|
1590
|
+
getCachedFearGreedContext({
|
|
1591
|
+
timestamp: signal.timestamp,
|
|
1592
|
+
maxAgeMs
|
|
1593
|
+
}),
|
|
1594
|
+
getCachedIndexContexts({
|
|
1595
|
+
timestamp: signal.timestamp,
|
|
1596
|
+
maxAgeMs
|
|
1597
|
+
})
|
|
1598
|
+
]);
|
|
1599
|
+
const globalRow = globalDailyRow;
|
|
1600
|
+
const references = dailyReferences;
|
|
1601
|
+
const previousReferences = previousDailyReferences;
|
|
1602
|
+
if (!globalRow && !references.size && !exchangeLiquidityRow && !fearGreedRow && !indexRows.size) {
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
const btcRow = references.get("BTCUSDT") ?? null;
|
|
1606
|
+
const ethRow = references.get("ETHUSDT") ?? null;
|
|
1607
|
+
const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
|
|
1608
|
+
const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
|
|
1609
|
+
const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
|
|
1610
|
+
const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
|
|
1611
|
+
const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
|
|
1612
|
+
previousBtcRow?.marketCapUsd
|
|
1613
|
+
);
|
|
1614
|
+
const previousEthMarketCapUsd = toFiniteNumberOrNull2(
|
|
1615
|
+
previousEthRow?.marketCapUsd
|
|
1616
|
+
);
|
|
1617
|
+
const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
|
|
1618
|
+
const previousEthBtcMarketCapRatio = safeDivide(
|
|
1619
|
+
previousEthMarketCapUsd,
|
|
1620
|
+
previousBtcMarketCapUsd
|
|
1621
|
+
);
|
|
1622
|
+
const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
|
|
1623
|
+
const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
|
|
1624
|
+
const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
|
|
1625
|
+
const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
|
|
1626
|
+
const btcDominanceChange24hPct = toFiniteNumberOrNull2(
|
|
1627
|
+
globalRow?.btcDominanceChange24hPct
|
|
1628
|
+
);
|
|
1629
|
+
const altMarketCapChange24hPct = toFiniteNumberOrNull2(
|
|
1630
|
+
globalRow?.altMarketCapChange24hPct
|
|
1631
|
+
);
|
|
1632
|
+
const altVolumeChange24hPct = toFiniteNumberOrNull2(
|
|
1633
|
+
globalRow?.altVolumeChange24hPct
|
|
1634
|
+
);
|
|
1635
|
+
const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
|
|
1636
|
+
stale: globalRow.stale,
|
|
1637
|
+
btcDominanceChange24hPct,
|
|
1638
|
+
altMarketCapChange24hPct,
|
|
1639
|
+
altVolumeChange24hPct
|
|
1640
|
+
}) : "unknown";
|
|
1641
|
+
const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
|
|
1642
|
+
stale: exchangeLiquidityRow.stale,
|
|
1643
|
+
totalVolumeChange24hPct: toFiniteNumberOrNull2(
|
|
1644
|
+
exchangeLiquidityRow.totalVolumeChange24hPct
|
|
1645
|
+
),
|
|
1646
|
+
fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
|
|
1647
|
+
}) : "unknown";
|
|
1648
|
+
const cmc100Row = indexRows.get("cmc100") ?? null;
|
|
1649
|
+
const cmc20Row = indexRows.get("cmc20") ?? null;
|
|
1650
|
+
const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
|
|
1651
|
+
const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
|
|
1652
|
+
const cmc100Change24hPct = toFiniteNumberOrNull2(
|
|
1653
|
+
cmc100Row?.valueChange24hPct
|
|
1654
|
+
);
|
|
1655
|
+
const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
|
|
1656
|
+
const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
|
|
1657
|
+
const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
|
|
1658
|
+
const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
|
|
1659
|
+
const indexRegime = toIndexRegime({
|
|
1660
|
+
stale: indexStale,
|
|
1661
|
+
cmc100Change24hPct,
|
|
1662
|
+
cmc20Change24hPct,
|
|
1663
|
+
cmc20ToCmc100RatioChange24hPct
|
|
1664
|
+
});
|
|
1665
|
+
const baseContext = signal.additionalIndicators.baseContext;
|
|
1666
|
+
signal.additionalIndicators = {
|
|
1667
|
+
...signal.additionalIndicators,
|
|
1668
|
+
baseContext: {
|
|
1669
|
+
...baseContext,
|
|
1670
|
+
relative: {
|
|
1671
|
+
...baseContext.relative,
|
|
1672
|
+
...globalRow ? {
|
|
1673
|
+
cmcGlobal: {
|
|
1674
|
+
source: globalRow.source,
|
|
1675
|
+
interval: "1d",
|
|
1676
|
+
asOfTs: globalRow.ts.getTime(),
|
|
1677
|
+
ageMs: globalRow.ageMs,
|
|
1678
|
+
stale: globalRow.stale,
|
|
1679
|
+
totalMarketCapUsd: toFiniteNumberOrNull2(
|
|
1680
|
+
globalRow.totalMarketCapUsd
|
|
1681
|
+
),
|
|
1682
|
+
totalVolumeUsd: toFiniteNumberOrNull2(
|
|
1683
|
+
globalRow.totalVolumeUsd
|
|
1684
|
+
),
|
|
1685
|
+
totalVolumeReportedUsd: toFiniteNumberOrNull2(
|
|
1686
|
+
globalRow.totalVolumeReportedUsd
|
|
1687
|
+
),
|
|
1688
|
+
altMarketCapUsd: toFiniteNumberOrNull2(
|
|
1689
|
+
globalRow.altMarketCapUsd
|
|
1690
|
+
),
|
|
1691
|
+
altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
|
|
1692
|
+
altVolumeReportedUsd: toFiniteNumberOrNull2(
|
|
1693
|
+
globalRow.altVolumeReportedUsd
|
|
1694
|
+
),
|
|
1695
|
+
btcDominancePct: toFiniteNumberOrNull2(
|
|
1696
|
+
globalRow.btcDominancePct
|
|
1697
|
+
),
|
|
1698
|
+
ethDominancePct: toFiniteNumberOrNull2(
|
|
1699
|
+
globalRow.ethDominancePct
|
|
1700
|
+
),
|
|
1701
|
+
btcDominanceChange24hPct,
|
|
1702
|
+
ethDominanceChange24hPct: toFiniteNumberOrNull2(
|
|
1703
|
+
globalRow.ethDominanceChange24hPct
|
|
1704
|
+
),
|
|
1705
|
+
altMarketCapChange24hPct,
|
|
1706
|
+
altVolumeChange24hPct,
|
|
1707
|
+
activeCryptocurrencies: toFiniteNumberOrNull2(
|
|
1708
|
+
globalRow.activeCryptocurrencies
|
|
1709
|
+
),
|
|
1710
|
+
activeExchanges: toFiniteNumberOrNull2(
|
|
1711
|
+
globalRow.activeExchanges
|
|
1712
|
+
),
|
|
1713
|
+
activeMarketPairs: toFiniteNumberOrNull2(
|
|
1714
|
+
globalRow.activeMarketPairs
|
|
1715
|
+
),
|
|
1716
|
+
altLiquidityRegime
|
|
1717
|
+
}
|
|
1718
|
+
} : {},
|
|
1719
|
+
...btcRow || ethRow ? {
|
|
1720
|
+
cmcReferenceAssets: {
|
|
1721
|
+
source: SOURCE_REFERENCE,
|
|
1722
|
+
interval: "1d",
|
|
1723
|
+
asOfTs: Math.max(
|
|
1724
|
+
btcRow?.ts.getTime() ?? 0,
|
|
1725
|
+
ethRow?.ts.getTime() ?? 0
|
|
1726
|
+
),
|
|
1727
|
+
ageMs: btcRow?.ageMs != null && ethRow?.ageMs != null ? Math.max(btcRow.ageMs, ethRow.ageMs) : btcRow?.ageMs ?? ethRow?.ageMs ?? null,
|
|
1728
|
+
stale: referenceStale,
|
|
1729
|
+
btcMarketCapUsd,
|
|
1730
|
+
ethMarketCapUsd,
|
|
1731
|
+
btcVolumeUsd,
|
|
1732
|
+
ethVolumeUsd,
|
|
1733
|
+
btcVolumeToMarketCap: safeDivide(
|
|
1734
|
+
btcVolumeUsd,
|
|
1735
|
+
btcMarketCapUsd
|
|
1736
|
+
),
|
|
1737
|
+
ethVolumeToMarketCap: safeDivide(
|
|
1738
|
+
ethVolumeUsd,
|
|
1739
|
+
ethMarketCapUsd
|
|
1740
|
+
),
|
|
1741
|
+
ethBtcMarketCapRatio,
|
|
1742
|
+
ethBtcMarketCapRatioChange24hPct,
|
|
1743
|
+
ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd),
|
|
1744
|
+
referenceLiquidityRegime: toReferenceLiquidityRegime({
|
|
1745
|
+
stale: referenceStale,
|
|
1746
|
+
ethBtcMarketCapRatioChange24hPct,
|
|
1747
|
+
ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd)
|
|
1748
|
+
})
|
|
1749
|
+
}
|
|
1750
|
+
} : {},
|
|
1751
|
+
...exchangeLiquidityRow ? {
|
|
1752
|
+
cmcExchangeLiquidity: {
|
|
1753
|
+
source: SOURCE_EXCHANGE_LIQUIDITY,
|
|
1754
|
+
interval: exchangeLiquidityRow.interval,
|
|
1755
|
+
asOfTs: exchangeLiquidityRow.ts.getTime(),
|
|
1756
|
+
ageMs: exchangeLiquidityRow.ageMs,
|
|
1757
|
+
stale: exchangeLiquidityRow.stale,
|
|
1758
|
+
exchangesCount: toFiniteNumberOrNull2(
|
|
1759
|
+
exchangeLiquidityRow.exchangesCount
|
|
1760
|
+
),
|
|
1761
|
+
totalVolumeUsd: toFiniteNumberOrNull2(
|
|
1762
|
+
exchangeLiquidityRow.totalVolumeUsd
|
|
1763
|
+
),
|
|
1764
|
+
totalVolumeChange24hPct: toFiniteNumberOrNull2(
|
|
1765
|
+
exchangeLiquidityRow.totalVolumeChange24hPct
|
|
1766
|
+
),
|
|
1767
|
+
binanceVolumeUsd: toFiniteNumberOrNull2(
|
|
1768
|
+
exchangeLiquidityRow.binanceVolumeUsd
|
|
1769
|
+
),
|
|
1770
|
+
binanceVolumeShare: toFiniteNumberOrNull2(
|
|
1771
|
+
exchangeLiquidityRow.binanceVolumeShare
|
|
1772
|
+
),
|
|
1773
|
+
topExchangeVolumeShare: toFiniteNumberOrNull2(
|
|
1774
|
+
exchangeLiquidityRow.topExchangeVolumeShare
|
|
1775
|
+
),
|
|
1776
|
+
liquidityRegime: exchangeLiquidityRegime
|
|
1777
|
+
}
|
|
1778
|
+
} : {},
|
|
1779
|
+
...fearGreedRow ? {
|
|
1780
|
+
cmcFearGreed: {
|
|
1781
|
+
source: SOURCE_FEAR_GREED,
|
|
1782
|
+
interval: "1d",
|
|
1783
|
+
asOfTs: fearGreedRow.ts.getTime(),
|
|
1784
|
+
ageMs: fearGreedRow.ageMs,
|
|
1785
|
+
stale: fearGreedRow.stale,
|
|
1786
|
+
value: toFiniteNumberOrNull2(fearGreedRow.value),
|
|
1787
|
+
valueChange24h: toFiniteNumberOrNull2(
|
|
1788
|
+
fearGreedRow.valueChange24h
|
|
1789
|
+
),
|
|
1790
|
+
valueChange7d: toFiniteNumberOrNull2(
|
|
1791
|
+
fearGreedRow.valueChange7d
|
|
1792
|
+
),
|
|
1793
|
+
classification: fearGreedRow.classification ?? "Unknown",
|
|
1794
|
+
sentimentRegime: fearGreedRow.sentimentRegime ?? "unknown"
|
|
1795
|
+
}
|
|
1796
|
+
} : {},
|
|
1797
|
+
...cmc100Row || cmc20Row ? {
|
|
1798
|
+
cmcIndexes: {
|
|
1799
|
+
source: SOURCE_INDEX,
|
|
1800
|
+
interval: "1d",
|
|
1801
|
+
asOfTs: Math.max(
|
|
1802
|
+
cmc100Row?.ts.getTime() ?? 0,
|
|
1803
|
+
cmc20Row?.ts.getTime() ?? 0
|
|
1804
|
+
),
|
|
1805
|
+
ageMs: cmc100Row?.ageMs != null && cmc20Row?.ageMs != null ? Math.max(cmc100Row.ageMs, cmc20Row.ageMs) : cmc100Row?.ageMs ?? cmc20Row?.ageMs ?? null,
|
|
1806
|
+
stale: indexStale,
|
|
1807
|
+
cmc100Value,
|
|
1808
|
+
cmc100Change24hPct,
|
|
1809
|
+
cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
|
|
1810
|
+
cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
|
|
1811
|
+
cmc100Row?.topConstituentWeightPct
|
|
1812
|
+
),
|
|
1813
|
+
cmc20Value,
|
|
1814
|
+
cmc20Change24hPct,
|
|
1815
|
+
cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
|
|
1816
|
+
cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
|
|
1817
|
+
cmc20Row?.topConstituentWeightPct
|
|
1818
|
+
),
|
|
1819
|
+
cmc20ToCmc100Ratio,
|
|
1820
|
+
cmc20ToCmc100RatioChange24hPct,
|
|
1821
|
+
indexRegime
|
|
1822
|
+
}
|
|
1823
|
+
} : {}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
(0, import_strategies2.refreshSignalBaseContextGateFeatures)(signal);
|
|
1828
|
+
return true;
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
coinMarketCapContextUnavailable = true;
|
|
1831
|
+
import_logger4.logger.warn(
|
|
1832
|
+
"CoinMarketCap context disabled after Timescale read failure: %s",
|
|
1833
|
+
String(error)
|
|
1834
|
+
);
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
// src/strategyHelpers/derivativesContext.ts
|
|
1840
|
+
var import_indicators2 = require("@tradejs/core/indicators");
|
|
1841
|
+
var import_strategies3 = require("@tradejs/core/strategies");
|
|
1842
|
+
var import_constants = require("@tradejs/core/constants");
|
|
1843
|
+
var import_timescale3 = require("@tradejs/infra/timescale");
|
|
1844
|
+
var import_logger5 = require("@tradejs/infra/logger");
|
|
1845
|
+
var DEFAULT_INTERVALS = ["15m", "1h"];
|
|
1846
|
+
var DEFAULT_LOOKBACK_HOURS = 48;
|
|
1847
|
+
var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
|
|
1848
|
+
var SECONDARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[1];
|
|
1849
|
+
var derivativesContextUnavailable = false;
|
|
1850
|
+
var parseEnabledFlag3 = (value, env) => {
|
|
1851
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1852
|
+
if (!normalized) return true;
|
|
1853
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
1854
|
+
if (normalized === "backtest") return env === "BACKTEST";
|
|
1855
|
+
if (normalized === "live") return env !== "BACKTEST";
|
|
1856
|
+
return false;
|
|
1857
|
+
};
|
|
1858
|
+
var parseBooleanFlag = (value, fallback = false) => {
|
|
1859
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
1860
|
+
if (!normalized) return fallback;
|
|
1861
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
1862
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
1863
|
+
return fallback;
|
|
1864
|
+
};
|
|
1865
|
+
var parseLookbackMs = () => {
|
|
1866
|
+
const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
|
|
1867
|
+
const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
|
|
1868
|
+
return normalizedHours * 60 * 60 * 1e3;
|
|
1869
|
+
};
|
|
1870
|
+
var parseIntervals = () => {
|
|
1871
|
+
const fromEnv = (0, import_indicators2.normalizeDerivativesIntervals)(
|
|
1872
|
+
process.env.DERIVATIVES_CONTEXT_INTERVALS
|
|
1873
|
+
);
|
|
1874
|
+
return fromEnv.length ? fromEnv : DEFAULT_INTERVALS;
|
|
1875
|
+
};
|
|
1876
|
+
var getDerivativesContextReferenceSymbols = () => [
|
|
1877
|
+
...(0, import_constants.resolveDerivativesContextReferenceSymbols)(
|
|
1878
|
+
process.env.DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS
|
|
1879
|
+
)
|
|
1880
|
+
];
|
|
1881
|
+
var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
|
|
1882
|
+
var getSignalPriceChangePct1h = (signal) => {
|
|
1883
|
+
const baseContext = signal.additionalIndicators?.baseContext;
|
|
1884
|
+
if (!baseContext || typeof baseContext !== "object" || Array.isArray(baseContext)) {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
const raw = typeof baseContext.raw === "object" && baseContext.raw && !Array.isArray(baseContext.raw) ? baseContext.raw : null;
|
|
1888
|
+
const price = raw && typeof raw.price === "object" && raw.price && !Array.isArray(raw.price) ? raw.price : null;
|
|
1889
|
+
const value = price?.price1hPct;
|
|
1890
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
1891
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
1892
|
+
};
|
|
1893
|
+
var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
|
|
1894
|
+
var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
|
|
1895
|
+
var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
|
|
1896
|
+
var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
|
|
1897
|
+
var toFiniteNumberOrNull3 = (value) => {
|
|
1898
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1899
|
+
if (typeof value === "string" && value.trim()) {
|
|
1900
|
+
const parsed = Number(value);
|
|
1901
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
};
|
|
1905
|
+
var roundNullable = (value, digits = 4) => {
|
|
1906
|
+
if (value == null || !Number.isFinite(value)) return null;
|
|
1907
|
+
const multiplier = 10 ** digits;
|
|
1908
|
+
return Math.round(value * multiplier) / multiplier;
|
|
1909
|
+
};
|
|
1910
|
+
var deltaNullable = (targetValue, referenceValue) => {
|
|
1911
|
+
const target = toFiniteNumberOrNull3(targetValue);
|
|
1912
|
+
const reference = toFiniteNumberOrNull3(referenceValue);
|
|
1913
|
+
return target == null || reference == null ? null : roundNullable(target - reference);
|
|
1914
|
+
};
|
|
1915
|
+
var buildTargetDerivedContext = (params) => {
|
|
1916
|
+
const { targetContext, primaryReferenceContext } = params;
|
|
1917
|
+
const targetPrimary = getPrimaryIntervalContext(targetContext);
|
|
1918
|
+
const referencePrimary = getPrimaryIntervalContext(primaryReferenceContext);
|
|
1919
|
+
const targetDirectionAligned = targetContext.summary.directionAligned;
|
|
1920
|
+
const referenceDirectionAligned = primaryReferenceContext?.summary.directionAligned ?? null;
|
|
1921
|
+
return {
|
|
1922
|
+
available: hasDerivativesSymbolData(targetContext),
|
|
1923
|
+
stale: targetContext.summary.riskFlags.includes("stale_derivatives") || targetPrimary?.stale === true ? true : targetPrimary == null ? null : false,
|
|
1924
|
+
sourceSymbol: targetContext.symbol,
|
|
1925
|
+
referenceSymbol: primaryReferenceContext?.symbol ?? null,
|
|
1926
|
+
directionAligned: targetDirectionAligned,
|
|
1927
|
+
referenceDirectionAligned,
|
|
1928
|
+
pressure: targetContext.summary.pressure ?? null,
|
|
1929
|
+
referencePressure: primaryReferenceContext?.summary.pressure ?? null,
|
|
1930
|
+
riskFlags: targetContext.summary.riskFlags,
|
|
1931
|
+
oiChangePct1h: targetPrimary?.oiChangePct1h ?? null,
|
|
1932
|
+
oiAcceleration: targetContext.summary.oiAcceleration ?? null,
|
|
1933
|
+
fundingRate: targetPrimary?.fundingRate ?? null,
|
|
1934
|
+
fundingZScore: targetPrimary?.fundingZScore ?? null,
|
|
1935
|
+
fundingChange1h: targetContext.summary.fundingChange1h ?? null,
|
|
1936
|
+
liqSpikeRatio: targetPrimary?.liqSpikeRatio ?? null,
|
|
1937
|
+
liqImbalance: targetPrimary?.liqImbalance ?? null,
|
|
1938
|
+
targetVsPrimaryOiChangePct1hDelta: deltaNullable(
|
|
1939
|
+
targetPrimary?.oiChangePct1h,
|
|
1940
|
+
referencePrimary?.oiChangePct1h
|
|
1941
|
+
),
|
|
1942
|
+
targetVsPrimaryFundingZScoreDelta: deltaNullable(
|
|
1943
|
+
targetPrimary?.fundingZScore,
|
|
1944
|
+
referencePrimary?.fundingZScore
|
|
1945
|
+
),
|
|
1946
|
+
targetReferenceConflict: targetDirectionAligned == null || referenceDirectionAligned == null ? null : targetDirectionAligned !== referenceDirectionAligned
|
|
1947
|
+
};
|
|
1948
|
+
};
|
|
1949
|
+
var buildReferenceDerivativesContext = (params) => {
|
|
1950
|
+
const {
|
|
1951
|
+
targetSymbol,
|
|
1952
|
+
primaryReferenceSymbol,
|
|
1953
|
+
secondaryReferenceSymbol,
|
|
1954
|
+
referenceSymbols,
|
|
1955
|
+
referenceContexts,
|
|
1956
|
+
targetContext
|
|
1957
|
+
} = params;
|
|
1958
|
+
const primaryContext = referenceContexts[primaryReferenceSymbol] ?? referenceContexts[referenceSymbols[0]];
|
|
1959
|
+
if (!primaryContext) {
|
|
1960
|
+
throw new Error("No derivatives reference contexts built");
|
|
1961
|
+
}
|
|
1962
|
+
const referenceSymbolsMetadata = [
|
|
1963
|
+
.../* @__PURE__ */ new Set([
|
|
1964
|
+
primaryReferenceSymbol,
|
|
1965
|
+
secondaryReferenceSymbol,
|
|
1966
|
+
...referenceSymbols,
|
|
1967
|
+
...Object.keys(referenceContexts)
|
|
1968
|
+
])
|
|
1969
|
+
];
|
|
1970
|
+
const targetDerived = targetContext && hasDerivativesSymbolData(targetContext) ? buildTargetDerivedContext({
|
|
1971
|
+
targetContext,
|
|
1972
|
+
primaryReferenceContext: primaryContext
|
|
1973
|
+
}) : void 0;
|
|
1974
|
+
return {
|
|
1975
|
+
...primaryContext,
|
|
1976
|
+
targetSymbol,
|
|
1977
|
+
primaryReferenceSymbol: primaryContext.symbol,
|
|
1978
|
+
secondaryReferenceSymbol: referenceContexts[secondaryReferenceSymbol]?.symbol ?? secondaryReferenceSymbol,
|
|
1979
|
+
referenceSymbols: referenceSymbolsMetadata,
|
|
1980
|
+
referenceContexts,
|
|
1981
|
+
...targetContext && targetDerived ? {
|
|
1982
|
+
targetContext,
|
|
1983
|
+
targetDerived
|
|
1984
|
+
} : {}
|
|
1985
|
+
};
|
|
1986
|
+
};
|
|
1987
|
+
var isDerivativesContextEnabled = (env) => parseEnabledFlag3(process.env.DERIVATIVES_CONTEXT_ENABLED, env);
|
|
1988
|
+
var isDerivativesTargetContextEnabled = () => parseBooleanFlag(process.env.DERIVATIVES_CONTEXT_TARGET_ENABLED, false);
|
|
1989
|
+
var enrichSignalWithDerivativesContext = async (params) => {
|
|
1990
|
+
const { signal, env, enabled = isDerivativesContextEnabled(env) } = params;
|
|
1991
|
+
if (signal.universe === "tradfi" || !enabled || derivativesContextUnavailable) {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1994
|
+
try {
|
|
1995
|
+
const intervals = parseIntervals();
|
|
1996
|
+
const referenceSymbols = getDerivativesContextReferenceSymbols();
|
|
1997
|
+
const targetSymbol = normalizeSymbol(signal.symbol);
|
|
1998
|
+
const lookbackMs = parseLookbackMs();
|
|
1999
|
+
const contexts = await Promise.all(
|
|
2000
|
+
referenceSymbols.map(async (symbol) => {
|
|
2001
|
+
const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
|
|
2002
|
+
symbol,
|
|
2003
|
+
intervals,
|
|
2004
|
+
endMs: signal.timestamp,
|
|
2005
|
+
lookbackMs
|
|
2006
|
+
});
|
|
2007
|
+
return [
|
|
2008
|
+
symbol,
|
|
2009
|
+
(0, import_indicators2.buildDerivativesContext)({
|
|
2010
|
+
symbol,
|
|
2011
|
+
direction: signal.direction,
|
|
2012
|
+
timestamp: signal.timestamp,
|
|
2013
|
+
rowsByInterval,
|
|
2014
|
+
priceChangePct1h: getSignalPriceChangePct1h(signal),
|
|
2015
|
+
intervals
|
|
2016
|
+
})
|
|
2017
|
+
];
|
|
2018
|
+
})
|
|
2019
|
+
);
|
|
2020
|
+
const referenceContexts = Object.fromEntries(contexts);
|
|
2021
|
+
const primaryReferenceSymbol = resolvePrimaryReferenceSymbol2();
|
|
2022
|
+
const secondaryReferenceSymbol = resolveSecondaryReferenceSymbol();
|
|
2023
|
+
const targetContextEnabled = isDerivativesTargetContextEnabled();
|
|
2024
|
+
const referenceTargetContext = targetContextEnabled && targetSymbol !== primaryReferenceSymbol ? referenceContexts[targetSymbol] : void 0;
|
|
2025
|
+
const shouldFetchTargetContext = targetContextEnabled && targetSymbol.length > 0 && !referenceSymbols.some(
|
|
2026
|
+
(referenceSymbol) => referenceSymbol === targetSymbol
|
|
2027
|
+
);
|
|
2028
|
+
const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
|
|
2029
|
+
const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
|
|
2030
|
+
symbol: targetSymbol,
|
|
2031
|
+
intervals,
|
|
2032
|
+
endMs: signal.timestamp,
|
|
2033
|
+
lookbackMs
|
|
2034
|
+
});
|
|
2035
|
+
const context = (0, import_indicators2.buildDerivativesContext)({
|
|
2036
|
+
symbol: targetSymbol,
|
|
2037
|
+
direction: signal.direction,
|
|
2038
|
+
timestamp: signal.timestamp,
|
|
2039
|
+
rowsByInterval,
|
|
2040
|
+
priceChangePct1h: getSignalPriceChangePct1h(signal),
|
|
2041
|
+
intervals
|
|
2042
|
+
});
|
|
2043
|
+
return hasDerivativesSymbolData(context) ? context : void 0;
|
|
2044
|
+
})() : void 0;
|
|
2045
|
+
const targetContext = referenceTargetContext && hasDerivativesSymbolData(referenceTargetContext) ? referenceTargetContext : fetchedTargetContext;
|
|
2046
|
+
const derivativesContext = buildReferenceDerivativesContext({
|
|
2047
|
+
targetSymbol: targetSymbol || signal.symbol,
|
|
2048
|
+
primaryReferenceSymbol,
|
|
2049
|
+
secondaryReferenceSymbol,
|
|
2050
|
+
referenceSymbols,
|
|
2051
|
+
referenceContexts,
|
|
2052
|
+
targetContext
|
|
2053
|
+
});
|
|
2054
|
+
signal.additionalIndicators = {
|
|
2055
|
+
...signal.additionalIndicators ?? {},
|
|
2056
|
+
baseContext: signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext) ? {
|
|
2057
|
+
...signal.additionalIndicators.baseContext,
|
|
2058
|
+
derivatives: derivativesContext
|
|
2059
|
+
} : signal.additionalIndicators?.baseContext
|
|
2060
|
+
};
|
|
2061
|
+
(0, import_strategies3.refreshSignalBaseContextGateFeatures)(signal);
|
|
2062
|
+
return true;
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
derivativesContextUnavailable = true;
|
|
2065
|
+
import_logger5.logger.warn(
|
|
2066
|
+
"Derivatives context disabled after Timescale read failure: %s",
|
|
2067
|
+
String(error)
|
|
2068
|
+
);
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
// src/strategyAdapters/ml.ts
|
|
2074
|
+
var defaultMlAdapter = {
|
|
2075
|
+
normalizeStrategyConfig: (strategyConfig) => strategyConfig
|
|
2076
|
+
};
|
|
2077
|
+
var getStrategyMlAdapter = (strategy, profileId) => {
|
|
2078
|
+
const strategyAdapter = getStrategyProfileMlAdapter(
|
|
2079
|
+
getStrategyManifest(strategy),
|
|
2080
|
+
profileId
|
|
2081
|
+
);
|
|
2082
|
+
if (!strategyAdapter) return defaultMlAdapter;
|
|
2083
|
+
return {
|
|
2084
|
+
...defaultMlAdapter,
|
|
2085
|
+
...strategyAdapter
|
|
2086
|
+
};
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
// src/mlPayload.ts
|
|
2090
|
+
var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
|
|
2091
|
+
return getStrategyMlAdapter(
|
|
2092
|
+
strategyName,
|
|
2093
|
+
profileId
|
|
2094
|
+
).normalizeStrategyConfig?.(strategyConfig);
|
|
2095
|
+
};
|
|
2096
|
+
var buildMlPayload = (payload) => {
|
|
2097
|
+
const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
|
|
2098
|
+
const profileId = payload.signal?.policyProfileId;
|
|
2099
|
+
const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
|
|
2100
|
+
const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
|
|
2101
|
+
const nextSignal = {
|
|
2102
|
+
...normalizedSignal,
|
|
2103
|
+
indicators: {
|
|
2104
|
+
...normalizedSignal?.indicators ?? {}
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
const nextContext = payload.context ? {
|
|
2108
|
+
...payload.context,
|
|
2109
|
+
strategyConfig: normalizeStrategyConfig(
|
|
2110
|
+
payload.context.strategyConfig,
|
|
2111
|
+
strategyName,
|
|
2112
|
+
profileId
|
|
2113
|
+
)
|
|
2114
|
+
} : void 0;
|
|
2115
|
+
return {
|
|
2116
|
+
signal: nextSignal,
|
|
2117
|
+
context: nextContext
|
|
2118
|
+
};
|
|
2119
|
+
};
|
|
2120
|
+
|
|
2121
|
+
// src/connectorsRegistry.ts
|
|
2122
|
+
var import_logger6 = require("@tradejs/infra/logger");
|
|
2123
|
+
var createConnectorRegistryState = () => ({
|
|
2124
|
+
connectorCreators: /* @__PURE__ */ new Map(),
|
|
2125
|
+
providerToConnectorName: /* @__PURE__ */ new Map(),
|
|
2126
|
+
pluginsLoadPromise: null
|
|
2127
|
+
});
|
|
2128
|
+
var registryStateByProjectRoot2 = /* @__PURE__ */ new Map();
|
|
2129
|
+
var getConnectorRegistryState = (cwd = getTradejsProjectCwd()) => {
|
|
2130
|
+
const projectRoot = getTradejsProjectCwd(cwd);
|
|
2131
|
+
let state = registryStateByProjectRoot2.get(projectRoot);
|
|
2132
|
+
if (!state) {
|
|
2133
|
+
state = createConnectorRegistryState();
|
|
2134
|
+
registryStateByProjectRoot2.set(projectRoot, state);
|
|
2135
|
+
}
|
|
2136
|
+
return {
|
|
805
2137
|
projectRoot,
|
|
806
2138
|
state
|
|
807
2139
|
};
|
|
@@ -839,7 +2171,7 @@ var normalizeProviders = (providers, connectorName) => {
|
|
|
839
2171
|
var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
|
|
840
2172
|
const existing = providerToConnectorName.get(provider);
|
|
841
2173
|
if (existing && existing !== connectorName) {
|
|
842
|
-
|
|
2174
|
+
import_logger6.logger.warn(
|
|
843
2175
|
'Skip duplicate connector provider "%s" from %s: already mapped to %s',
|
|
844
2176
|
provider,
|
|
845
2177
|
source,
|
|
@@ -852,11 +2184,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
|
|
|
852
2184
|
var registerEntry = (entry, source, state) => {
|
|
853
2185
|
const connectorName = String(entry?.name ?? "").trim();
|
|
854
2186
|
if (!connectorName) {
|
|
855
|
-
|
|
2187
|
+
import_logger6.logger.warn("Skip connector entry without name from %s", source);
|
|
856
2188
|
return;
|
|
857
2189
|
}
|
|
858
2190
|
if (typeof entry.creator !== "function") {
|
|
859
|
-
|
|
2191
|
+
import_logger6.logger.warn(
|
|
860
2192
|
'Skip connector entry "%s" from %s: creator must be a function',
|
|
861
2193
|
connectorName,
|
|
862
2194
|
source
|
|
@@ -868,7 +2200,7 @@ var registerEntry = (entry, source, state) => {
|
|
|
868
2200
|
state.connectorCreators
|
|
869
2201
|
);
|
|
870
2202
|
if (existingByName) {
|
|
871
|
-
|
|
2203
|
+
import_logger6.logger.warn(
|
|
872
2204
|
'Skip duplicate connector "%s" from %s: already registered as %s',
|
|
873
2205
|
connectorName,
|
|
874
2206
|
source,
|
|
@@ -940,7 +2272,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
|
|
|
940
2272
|
);
|
|
941
2273
|
const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
|
|
942
2274
|
if (!pluginDefinition) {
|
|
943
|
-
|
|
2275
|
+
import_logger6.logger.warn(
|
|
944
2276
|
'Skip connector plugin "%s": export { connectorEntries } is missing',
|
|
945
2277
|
moduleName
|
|
946
2278
|
);
|
|
@@ -948,7 +2280,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
|
|
|
948
2280
|
}
|
|
949
2281
|
registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
|
|
950
2282
|
} catch (error) {
|
|
951
|
-
|
|
2283
|
+
import_logger6.logger.warn(
|
|
952
2284
|
'Failed to load connector plugin "%s": %s',
|
|
953
2285
|
moduleName,
|
|
954
2286
|
String(error)
|
|
@@ -980,19 +2312,31 @@ var DEFAULT_CONNECTOR_NAME = BUILTIN_CONNECTOR_NAMES.ByBit;
|
|
|
980
2312
|
|
|
981
2313
|
// src/testConnector.ts
|
|
982
2314
|
var import_node_crypto = require("crypto");
|
|
2315
|
+
var import_constants2 = require("@tradejs/core/constants");
|
|
2316
|
+
var import_backtest = require("@tradejs/core/backtest");
|
|
2317
|
+
var import_trade = require("@tradejs/core/trade");
|
|
983
2318
|
var import_math = require("@tradejs/core/math");
|
|
984
|
-
var
|
|
985
|
-
var INITIAL_AMOUNT = 100;
|
|
2319
|
+
var PRICE_PRECISION = 8;
|
|
986
2320
|
var createTestConnector = (connector, context) => {
|
|
987
2321
|
let state = {};
|
|
988
2322
|
const orderLog = [];
|
|
989
2323
|
const positionLog = [];
|
|
2324
|
+
const fastMode = Boolean(context?.fastMode);
|
|
2325
|
+
const executionCostModel = context?.executionCostModel;
|
|
2326
|
+
const makerFeeRate = executionCostModel?.fees.makerRate ?? import_constants2.FEE_PERCENT;
|
|
2327
|
+
const takerFeeRate = executionCostModel?.fees.takerRate ?? import_constants2.FEE_PERCENT;
|
|
2328
|
+
const fundingRates = [...context?.fundingRates ?? []].sort(
|
|
2329
|
+
(left, right) => left.timestamp - right.timestamp
|
|
2330
|
+
);
|
|
2331
|
+
const processedFundingTimestamps = /* @__PURE__ */ new Set();
|
|
990
2332
|
let currentPosition = null;
|
|
991
|
-
let amount =
|
|
2333
|
+
let amount = import_constants2.INITIAL_BACKTEST_AMOUNT;
|
|
992
2334
|
let originalQty = 0;
|
|
993
2335
|
let currentPositionProfit = 0;
|
|
2336
|
+
let currentSignalId = null;
|
|
994
2337
|
let takeProfits = [];
|
|
995
2338
|
let stopLossPrice = null;
|
|
2339
|
+
let currentTradeResult = null;
|
|
996
2340
|
const closedSignalResults = [];
|
|
997
2341
|
const logOrder = (data) => {
|
|
998
2342
|
const nextEntry = {
|
|
@@ -1003,10 +2347,157 @@ var createTestConnector = (connector, context) => {
|
|
|
1003
2347
|
index: orderLog.length
|
|
1004
2348
|
};
|
|
1005
2349
|
if (nextEntry.signal) {
|
|
1006
|
-
const {
|
|
1007
|
-
|
|
2350
|
+
const {
|
|
2351
|
+
additionalIndicators: _additionalIndicators,
|
|
2352
|
+
indicators: _indicators,
|
|
2353
|
+
...signalWithoutHeavyContext
|
|
2354
|
+
} = nextEntry.signal;
|
|
2355
|
+
nextEntry.signal = signalWithoutHeavyContext;
|
|
2356
|
+
}
|
|
2357
|
+
if (!fastMode) {
|
|
2358
|
+
orderLog.push(nextEntry);
|
|
1008
2359
|
}
|
|
1009
|
-
|
|
2360
|
+
};
|
|
2361
|
+
const roundNullable2 = (value) => value == null ? null : (0, import_math.round)(value);
|
|
2362
|
+
const roundPrice = (value) => (0, import_math.round)(value, PRICE_PRECISION);
|
|
2363
|
+
const roundNullablePrice = (value) => value == null ? null : roundPrice(value);
|
|
2364
|
+
const getSlippageCost = ({
|
|
2365
|
+
requestedPrice,
|
|
2366
|
+
executionPrice,
|
|
2367
|
+
direction,
|
|
2368
|
+
stage,
|
|
2369
|
+
qty
|
|
2370
|
+
}) => {
|
|
2371
|
+
if (direction === "LONG") {
|
|
2372
|
+
return stage === "entry" ? Math.max(0, executionPrice - requestedPrice) * qty : Math.max(0, requestedPrice - executionPrice) * qty;
|
|
2373
|
+
}
|
|
2374
|
+
return stage === "entry" ? Math.max(0, requestedPrice - executionPrice) * qty : Math.max(0, executionPrice - requestedPrice) * qty;
|
|
2375
|
+
};
|
|
2376
|
+
const getSlippageBps = (requestedPrice, executionPrice) => requestedPrice ? (executionPrice - requestedPrice) / requestedPrice * 1e4 : 0;
|
|
2377
|
+
const getWeightedAverage = (previousValue, previousQty, nextValue, nextQty) => {
|
|
2378
|
+
if (previousValue == null || previousQty <= 0) {
|
|
2379
|
+
return nextValue;
|
|
2380
|
+
}
|
|
2381
|
+
return (previousValue * previousQty + nextValue * nextQty) / (previousQty + nextQty);
|
|
2382
|
+
};
|
|
2383
|
+
const finalizeTradeResult = (tradeResult, timestamp) => {
|
|
2384
|
+
if (!tradeResult.exitReason) {
|
|
2385
|
+
return void 0;
|
|
2386
|
+
}
|
|
2387
|
+
return {
|
|
2388
|
+
...tradeResult,
|
|
2389
|
+
exitTimestamp: tradeResult.exitTimestamp ?? timestamp,
|
|
2390
|
+
exitReason: tradeResult.exitReason,
|
|
2391
|
+
requestedEntryPrice: roundPrice(tradeResult.requestedEntryPrice),
|
|
2392
|
+
entryPrice: roundPrice(tradeResult.entryPrice),
|
|
2393
|
+
requestedExitPrice: roundNullablePrice(tradeResult.requestedExitPrice),
|
|
2394
|
+
exitPrice: roundNullablePrice(tradeResult.exitPrice),
|
|
2395
|
+
grossProfit: (0, import_math.round)(tradeResult.grossProfit),
|
|
2396
|
+
netProfit: (0, import_math.round)(tradeResult.netProfit),
|
|
2397
|
+
openFee: (0, import_math.round)(tradeResult.openFee),
|
|
2398
|
+
closeFee: (0, import_math.round)(tradeResult.closeFee),
|
|
2399
|
+
fundingFee: roundNullable2(tradeResult.fundingFee),
|
|
2400
|
+
totalFee: (0, import_math.round)(tradeResult.totalFee),
|
|
2401
|
+
entrySlippagePrice: (0, import_math.round)(tradeResult.entrySlippagePrice),
|
|
2402
|
+
entrySlippageBps: (0, import_math.round)(tradeResult.entrySlippageBps),
|
|
2403
|
+
entryBaseSlippageBps: (0, import_math.round)(tradeResult.entryBaseSlippageBps),
|
|
2404
|
+
entrySpreadBps: (0, import_math.round)(tradeResult.entrySpreadBps),
|
|
2405
|
+
entrySpreadSlippageBps: (0, import_math.round)(tradeResult.entrySpreadSlippageBps),
|
|
2406
|
+
entryMarketImpactBps: (0, import_math.round)(tradeResult.entryMarketImpactBps),
|
|
2407
|
+
entryDelayRiskBps: roundNullable2(tradeResult.entryDelayRiskBps),
|
|
2408
|
+
entrySlippageCost: (0, import_math.round)(tradeResult.entrySlippageCost),
|
|
2409
|
+
exitSlippagePrice: roundNullable2(tradeResult.exitSlippagePrice),
|
|
2410
|
+
exitSlippageBps: roundNullable2(tradeResult.exitSlippageBps),
|
|
2411
|
+
exitBaseSlippageBps: roundNullable2(tradeResult.exitBaseSlippageBps),
|
|
2412
|
+
exitSpreadBps: roundNullable2(tradeResult.exitSpreadBps),
|
|
2413
|
+
exitSpreadSlippageBps: roundNullable2(tradeResult.exitSpreadSlippageBps),
|
|
2414
|
+
exitMarketImpactBps: roundNullable2(tradeResult.exitMarketImpactBps),
|
|
2415
|
+
exitDelayRiskBps: roundNullable2(tradeResult.exitDelayRiskBps),
|
|
2416
|
+
exitSlippageCost: (0, import_math.round)(tradeResult.exitSlippageCost),
|
|
2417
|
+
totalSlippageCost: (0, import_math.round)(tradeResult.totalSlippageCost),
|
|
2418
|
+
qty: (0, import_math.round)(tradeResult.qty),
|
|
2419
|
+
closedQty: (0, import_math.round)(tradeResult.closedQty)
|
|
2420
|
+
};
|
|
2421
|
+
};
|
|
2422
|
+
const recordExitResult = ({
|
|
2423
|
+
timestamp,
|
|
2424
|
+
reason,
|
|
2425
|
+
requestedPrice,
|
|
2426
|
+
executionPrice,
|
|
2427
|
+
qty,
|
|
2428
|
+
grossProfit,
|
|
2429
|
+
fee,
|
|
2430
|
+
slippageBreakdown
|
|
2431
|
+
}) => {
|
|
2432
|
+
if (!currentTradeResult || !currentPosition) {
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const previousClosedQty = currentTradeResult.closedQty;
|
|
2436
|
+
const requestedExitPrice = getWeightedAverage(
|
|
2437
|
+
currentTradeResult.requestedExitPrice,
|
|
2438
|
+
previousClosedQty,
|
|
2439
|
+
requestedPrice,
|
|
2440
|
+
qty
|
|
2441
|
+
);
|
|
2442
|
+
const exitPrice = getWeightedAverage(
|
|
2443
|
+
currentTradeResult.exitPrice,
|
|
2444
|
+
previousClosedQty,
|
|
2445
|
+
executionPrice,
|
|
2446
|
+
qty
|
|
2447
|
+
);
|
|
2448
|
+
const exitBaseSlippageBps = getWeightedAverage(
|
|
2449
|
+
currentTradeResult.exitBaseSlippageBps,
|
|
2450
|
+
previousClosedQty,
|
|
2451
|
+
slippageBreakdown.baseSlippageBps,
|
|
2452
|
+
qty
|
|
2453
|
+
);
|
|
2454
|
+
const exitSpreadBps = getWeightedAverage(
|
|
2455
|
+
currentTradeResult.exitSpreadBps,
|
|
2456
|
+
previousClosedQty,
|
|
2457
|
+
slippageBreakdown.spreadBps,
|
|
2458
|
+
qty
|
|
2459
|
+
);
|
|
2460
|
+
const exitSpreadSlippageBps = getWeightedAverage(
|
|
2461
|
+
currentTradeResult.exitSpreadSlippageBps,
|
|
2462
|
+
previousClosedQty,
|
|
2463
|
+
slippageBreakdown.spreadSlippageBps,
|
|
2464
|
+
qty
|
|
2465
|
+
);
|
|
2466
|
+
const exitMarketImpactBps = getWeightedAverage(
|
|
2467
|
+
currentTradeResult.exitMarketImpactBps,
|
|
2468
|
+
previousClosedQty,
|
|
2469
|
+
slippageBreakdown.marketImpactBps,
|
|
2470
|
+
qty
|
|
2471
|
+
);
|
|
2472
|
+
const exitDelayRiskBps = null;
|
|
2473
|
+
const exitSlippageCost = currentTradeResult.exitSlippageCost + getSlippageCost({
|
|
2474
|
+
requestedPrice,
|
|
2475
|
+
executionPrice,
|
|
2476
|
+
direction: currentPosition.direction,
|
|
2477
|
+
stage: "exit",
|
|
2478
|
+
qty
|
|
2479
|
+
});
|
|
2480
|
+
currentTradeResult = {
|
|
2481
|
+
...currentTradeResult,
|
|
2482
|
+
closedQty: previousClosedQty + qty,
|
|
2483
|
+
exitTimestamp: timestamp,
|
|
2484
|
+
exitReason: reason,
|
|
2485
|
+
requestedExitPrice,
|
|
2486
|
+
exitPrice,
|
|
2487
|
+
grossProfit: currentTradeResult.grossProfit + grossProfit,
|
|
2488
|
+
netProfit: currentTradeResult.netProfit + grossProfit - fee,
|
|
2489
|
+
closeFee: currentTradeResult.closeFee + fee,
|
|
2490
|
+
totalFee: currentTradeResult.openFee + currentTradeResult.closeFee + fee + (currentTradeResult.fundingFee ?? 0),
|
|
2491
|
+
exitSlippagePrice: exitPrice - requestedExitPrice,
|
|
2492
|
+
exitSlippageBps: getSlippageBps(requestedExitPrice, exitPrice),
|
|
2493
|
+
exitBaseSlippageBps,
|
|
2494
|
+
exitSpreadBps,
|
|
2495
|
+
exitSpreadSlippageBps,
|
|
2496
|
+
exitMarketImpactBps,
|
|
2497
|
+
exitDelayRiskBps,
|
|
2498
|
+
exitSlippageCost,
|
|
2499
|
+
totalSlippageCost: currentTradeResult.entrySlippageCost + exitSlippageCost
|
|
2500
|
+
};
|
|
1010
2501
|
};
|
|
1011
2502
|
const clearPosition = (timestamp) => {
|
|
1012
2503
|
takeProfits = [];
|
|
@@ -1016,11 +2507,12 @@ var createTestConnector = (connector, context) => {
|
|
|
1016
2507
|
return;
|
|
1017
2508
|
}
|
|
1018
2509
|
if (context?.mlEnabled || context?.aiEnabled) {
|
|
1019
|
-
|
|
1020
|
-
|
|
2510
|
+
if (currentSignalId) {
|
|
2511
|
+
const tradeResult = currentTradeResult ? finalizeTradeResult(currentTradeResult, timestamp) : void 0;
|
|
1021
2512
|
closedSignalResults.push({
|
|
1022
|
-
signalId,
|
|
1023
|
-
profit: currentPositionProfit
|
|
2513
|
+
signalId: currentSignalId,
|
|
2514
|
+
profit: (0, import_math.round)(currentPositionProfit),
|
|
2515
|
+
...tradeResult ? { tradeResult } : {}
|
|
1024
2516
|
});
|
|
1025
2517
|
}
|
|
1026
2518
|
}
|
|
@@ -1036,29 +2528,117 @@ var createTestConnector = (connector, context) => {
|
|
|
1036
2528
|
}
|
|
1037
2529
|
});
|
|
1038
2530
|
currentPosition = null;
|
|
2531
|
+
currentSignalId = null;
|
|
2532
|
+
currentTradeResult = null;
|
|
1039
2533
|
currentPositionProfit = 0;
|
|
1040
2534
|
};
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2535
|
+
const getNetProfit = ({
|
|
2536
|
+
grossProfit,
|
|
2537
|
+
price,
|
|
2538
|
+
qty,
|
|
2539
|
+
feeRate = takerFeeRate
|
|
2540
|
+
}) => {
|
|
2541
|
+
const fee = price * qty * feeRate;
|
|
2542
|
+
return {
|
|
2543
|
+
fee,
|
|
2544
|
+
profit: grossProfit - fee
|
|
2545
|
+
};
|
|
2546
|
+
};
|
|
2547
|
+
const applyFunding = (candle) => {
|
|
2548
|
+
if (!executionCostModel?.funding.enabled || !currentPosition) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
for (const point of fundingRates) {
|
|
2552
|
+
if (processedFundingTimestamps.has(point.timestamp) || point.symbol.toUpperCase() !== currentPosition.symbol.toUpperCase() || point.timestamp <= currentPosition.timestamp || point.timestamp > candle.timestamp) {
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
processedFundingTimestamps.add(point.timestamp);
|
|
2556
|
+
const notional = candle.close * currentPosition.qty;
|
|
2557
|
+
const fundingCost = notional * point.rate * (currentPosition.direction === "LONG" ? 1 : -1);
|
|
2558
|
+
amount -= fundingCost;
|
|
2559
|
+
currentPositionProfit -= fundingCost;
|
|
2560
|
+
if (currentTradeResult) {
|
|
2561
|
+
currentTradeResult.fundingFee = (currentTradeResult.fundingFee ?? 0) + fundingCost;
|
|
2562
|
+
currentTradeResult.netProfit -= fundingCost;
|
|
2563
|
+
currentTradeResult.totalFee += fundingCost;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
const getExitTimestamp = (candle) => currentPosition ? Math.max(candle.timestamp, currentPosition.timestamp) : candle.timestamp;
|
|
2568
|
+
const applyExecutionSlippage = ({
|
|
2569
|
+
price,
|
|
2570
|
+
direction,
|
|
2571
|
+
stage,
|
|
2572
|
+
signal
|
|
2573
|
+
}) => {
|
|
2574
|
+
const modelParams = {
|
|
2575
|
+
baseSlippageBps: executionCostModel?.slippage.baseBps,
|
|
2576
|
+
spreadBps: (0, import_trade.extractExecutionSpreadBps)(signal),
|
|
2577
|
+
spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
|
|
2578
|
+
marketImpactBps: (0, import_trade.extractExecutionMarketImpactBps)(signal) ?? executionCostModel?.slippage.marketImpactBps,
|
|
2579
|
+
delayRiskBps: stage === "entry" ? ((0, import_trade.extractExecutionDelayRiskBps)(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
|
|
2580
|
+
};
|
|
2581
|
+
return (0, import_trade.applyExecutionSlippage)({
|
|
2582
|
+
price,
|
|
2583
|
+
direction,
|
|
2584
|
+
stage,
|
|
2585
|
+
...modelParams
|
|
2586
|
+
});
|
|
2587
|
+
};
|
|
2588
|
+
const getExecutionSlippageBreakdown = ({
|
|
2589
|
+
stage,
|
|
2590
|
+
signal
|
|
2591
|
+
}) => (0, import_trade.calculateExecutionSlippageBreakdown)({
|
|
2592
|
+
baseSlippageBps: executionCostModel?.slippage.baseBps,
|
|
2593
|
+
spreadBps: (0, import_trade.extractExecutionSpreadBps)(signal),
|
|
2594
|
+
spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
|
|
2595
|
+
marketImpactBps: (0, import_trade.extractExecutionMarketImpactBps)(signal) ?? executionCostModel?.slippage.marketImpactBps,
|
|
2596
|
+
delayRiskBps: stage === "entry" ? ((0, import_trade.extractExecutionDelayRiskBps)(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
|
|
2597
|
+
});
|
|
2598
|
+
const getExecutionSlippageLogData = (slippageBreakdown, stage) => ({
|
|
2599
|
+
executionSlippageStage: stage,
|
|
2600
|
+
executionSlippageBps: (0, import_math.round)(slippageBreakdown.effectiveSlippageBps),
|
|
2601
|
+
executionBaseSlippageBps: (0, import_math.round)(slippageBreakdown.baseSlippageBps),
|
|
2602
|
+
executionSpreadBps: (0, import_math.round)(slippageBreakdown.spreadBps),
|
|
2603
|
+
executionSpreadSlippageBps: (0, import_math.round)(slippageBreakdown.spreadSlippageBps),
|
|
2604
|
+
executionMarketImpactBps: (0, import_math.round)(slippageBreakdown.marketImpactBps),
|
|
2605
|
+
executionDelayRiskBps: stage === "entry" ? (0, import_math.round)(slippageBreakdown.delayRiskBps) : null
|
|
2606
|
+
});
|
|
2607
|
+
return {
|
|
2608
|
+
__tradejsTestConnector: true,
|
|
2609
|
+
capabilities: connector.capabilities,
|
|
2610
|
+
universe: connector.universe,
|
|
2611
|
+
accountId: connector.accountId,
|
|
2612
|
+
deploymentId: connector.deploymentId,
|
|
2613
|
+
listInstruments: (query) => connector.listInstruments(query),
|
|
2614
|
+
getFundingRateHistory: connector.getFundingRateHistory ? (request) => connector.getFundingRateHistory(request) : void 0,
|
|
2615
|
+
getTradingFeeRate: connector.getTradingFeeRate ? (symbol) => connector.getTradingFeeRate(symbol) : void 0,
|
|
2616
|
+
getState: async () => state,
|
|
2617
|
+
setState: async (newState) => {
|
|
2618
|
+
state = {
|
|
2619
|
+
...state,
|
|
2620
|
+
...newState
|
|
2621
|
+
};
|
|
2622
|
+
},
|
|
2623
|
+
kline: async (options) => connector.kline(options),
|
|
2624
|
+
getResult: async () => {
|
|
2625
|
+
const orderLogId = (0, import_node_crypto.randomUUID)().slice(-12);
|
|
2626
|
+
const fullStat = fastMode ? (0, import_backtest.calculateStatsFull)(positionLog) : null;
|
|
1053
2627
|
return {
|
|
1054
|
-
stat: {
|
|
1055
|
-
|
|
1056
|
-
profit:
|
|
2628
|
+
stat: fullStat ? {
|
|
2629
|
+
...fullStat,
|
|
2630
|
+
profit: fullStat.netProfit
|
|
2631
|
+
} : {
|
|
2632
|
+
amount: (0, import_math.round)(amount),
|
|
2633
|
+
profit: (0, import_math.round)(amount - import_constants2.INITIAL_BACKTEST_AMOUNT),
|
|
1057
2634
|
orders: positionLog.length
|
|
1058
2635
|
},
|
|
1059
2636
|
orderLogId,
|
|
1060
|
-
|
|
1061
|
-
|
|
2637
|
+
...executionCostModel ? { executionCostModel } : {},
|
|
2638
|
+
...fastMode ? {} : {
|
|
2639
|
+
inlineOrderLog: orderLog,
|
|
2640
|
+
inlinePositionLog: positionLog
|
|
2641
|
+
}
|
|
1062
2642
|
};
|
|
1063
2643
|
},
|
|
1064
2644
|
getPosition: async () => currentPosition || null,
|
|
@@ -1084,6 +2664,7 @@ var createTestConnector = (connector, context) => {
|
|
|
1084
2664
|
if (!candle || !currentPosition || !currentPosition.qty) {
|
|
1085
2665
|
return;
|
|
1086
2666
|
}
|
|
2667
|
+
applyFunding(candle);
|
|
1087
2668
|
const isLong = currentPosition.direction === "LONG";
|
|
1088
2669
|
const entryPrice = currentPosition.price;
|
|
1089
2670
|
const high = candle.high;
|
|
@@ -1093,47 +2674,217 @@ var createTestConnector = (connector, context) => {
|
|
|
1093
2674
|
const targetPrice = tp.price;
|
|
1094
2675
|
const reached = isLong ? high >= targetPrice : low <= targetPrice;
|
|
1095
2676
|
if (reached) {
|
|
2677
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
1096
2678
|
const qty = originalQty * tp.rate;
|
|
1097
|
-
const
|
|
2679
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
2680
|
+
stage: "exit",
|
|
2681
|
+
signal: currentPosition.signal
|
|
2682
|
+
});
|
|
2683
|
+
const executionPrice = applyExecutionSlippage({
|
|
2684
|
+
price: targetPrice,
|
|
2685
|
+
direction: currentPosition.direction,
|
|
2686
|
+
stage: "exit",
|
|
2687
|
+
signal: currentPosition.signal
|
|
2688
|
+
});
|
|
2689
|
+
const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
|
|
2690
|
+
const { fee, profit } = getNetProfit({
|
|
2691
|
+
grossProfit,
|
|
2692
|
+
price: executionPrice,
|
|
2693
|
+
qty
|
|
2694
|
+
});
|
|
2695
|
+
recordExitResult({
|
|
2696
|
+
timestamp: exitTimestamp,
|
|
2697
|
+
reason: "take_profit",
|
|
2698
|
+
requestedPrice: targetPrice,
|
|
2699
|
+
executionPrice,
|
|
2700
|
+
qty,
|
|
2701
|
+
grossProfit,
|
|
2702
|
+
fee,
|
|
2703
|
+
slippageBreakdown
|
|
2704
|
+
});
|
|
1098
2705
|
amount += profit;
|
|
1099
2706
|
currentPositionProfit += profit;
|
|
1100
2707
|
currentPosition.qty = parseFloat(
|
|
1101
2708
|
(currentPosition.qty - qty).toFixed(8)
|
|
1102
2709
|
);
|
|
1103
2710
|
logOrder({
|
|
1104
|
-
timestamp:
|
|
2711
|
+
timestamp: exitTimestamp,
|
|
1105
2712
|
qty,
|
|
1106
|
-
price:
|
|
2713
|
+
price: executionPrice,
|
|
1107
2714
|
profit,
|
|
1108
|
-
|
|
2715
|
+
fee,
|
|
2716
|
+
type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
|
|
2717
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
1109
2718
|
});
|
|
1110
2719
|
tp.done = true;
|
|
1111
2720
|
}
|
|
1112
2721
|
}
|
|
1113
2722
|
takeProfits = takeProfits.filter(({ done }) => !done);
|
|
1114
2723
|
if (currentPosition && currentPosition.qty <= 0) {
|
|
1115
|
-
clearPosition(candle
|
|
2724
|
+
clearPosition(getExitTimestamp(candle));
|
|
1116
2725
|
}
|
|
1117
2726
|
},
|
|
1118
2727
|
checkSl: async (candle) => {
|
|
1119
2728
|
if (!stopLossPrice || !currentPosition || !candle) {
|
|
1120
2729
|
return;
|
|
1121
2730
|
}
|
|
2731
|
+
applyFunding(candle);
|
|
1122
2732
|
const isLong = currentPosition.direction === "LONG";
|
|
1123
2733
|
const hitStop = isLong ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
|
|
1124
2734
|
if (hitStop) {
|
|
2735
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
1125
2736
|
const qty = currentPosition.qty;
|
|
1126
|
-
const
|
|
2737
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
2738
|
+
stage: "exit",
|
|
2739
|
+
signal: currentPosition.signal
|
|
2740
|
+
});
|
|
2741
|
+
const executionPrice = applyExecutionSlippage({
|
|
2742
|
+
price: stopLossPrice,
|
|
2743
|
+
direction: currentPosition.direction,
|
|
2744
|
+
stage: "exit",
|
|
2745
|
+
signal: currentPosition.signal
|
|
2746
|
+
});
|
|
2747
|
+
const grossProfit = isLong ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
|
|
2748
|
+
const { fee, profit } = getNetProfit({
|
|
2749
|
+
grossProfit,
|
|
2750
|
+
price: executionPrice,
|
|
2751
|
+
qty
|
|
2752
|
+
});
|
|
2753
|
+
recordExitResult({
|
|
2754
|
+
timestamp: exitTimestamp,
|
|
2755
|
+
reason: "stop_loss",
|
|
2756
|
+
requestedPrice: stopLossPrice,
|
|
2757
|
+
executionPrice,
|
|
2758
|
+
qty,
|
|
2759
|
+
grossProfit,
|
|
2760
|
+
fee,
|
|
2761
|
+
slippageBreakdown
|
|
2762
|
+
});
|
|
1127
2763
|
amount += profit;
|
|
1128
2764
|
currentPositionProfit += profit;
|
|
1129
2765
|
logOrder({
|
|
1130
|
-
timestamp:
|
|
2766
|
+
timestamp: exitTimestamp,
|
|
1131
2767
|
qty,
|
|
1132
2768
|
profit,
|
|
1133
|
-
price:
|
|
1134
|
-
|
|
2769
|
+
price: executionPrice,
|
|
2770
|
+
fee,
|
|
2771
|
+
type: isLong ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
|
|
2772
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
1135
2773
|
});
|
|
1136
|
-
clearPosition(
|
|
2774
|
+
clearPosition(exitTimestamp);
|
|
2775
|
+
}
|
|
2776
|
+
},
|
|
2777
|
+
checkExits: async (candle) => {
|
|
2778
|
+
if (!candle || !currentPosition) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
applyFunding(candle);
|
|
2782
|
+
if (stopLossPrice) {
|
|
2783
|
+
const isLong2 = currentPosition.direction === "LONG";
|
|
2784
|
+
const hitStop = isLong2 ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
|
|
2785
|
+
if (hitStop) {
|
|
2786
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
2787
|
+
const qty = currentPosition.qty;
|
|
2788
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
2789
|
+
stage: "exit",
|
|
2790
|
+
signal: currentPosition.signal
|
|
2791
|
+
});
|
|
2792
|
+
const executionPrice = applyExecutionSlippage({
|
|
2793
|
+
price: stopLossPrice,
|
|
2794
|
+
direction: currentPosition.direction,
|
|
2795
|
+
stage: "exit",
|
|
2796
|
+
signal: currentPosition.signal
|
|
2797
|
+
});
|
|
2798
|
+
const grossProfit = isLong2 ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
|
|
2799
|
+
const { fee, profit } = getNetProfit({
|
|
2800
|
+
grossProfit,
|
|
2801
|
+
price: executionPrice,
|
|
2802
|
+
qty
|
|
2803
|
+
});
|
|
2804
|
+
recordExitResult({
|
|
2805
|
+
timestamp: exitTimestamp,
|
|
2806
|
+
reason: "stop_loss",
|
|
2807
|
+
requestedPrice: stopLossPrice,
|
|
2808
|
+
executionPrice,
|
|
2809
|
+
qty,
|
|
2810
|
+
grossProfit,
|
|
2811
|
+
fee,
|
|
2812
|
+
slippageBreakdown
|
|
2813
|
+
});
|
|
2814
|
+
amount += profit;
|
|
2815
|
+
currentPositionProfit += profit;
|
|
2816
|
+
logOrder({
|
|
2817
|
+
timestamp: exitTimestamp,
|
|
2818
|
+
qty,
|
|
2819
|
+
profit,
|
|
2820
|
+
price: executionPrice,
|
|
2821
|
+
fee,
|
|
2822
|
+
type: isLong2 ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
|
|
2823
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
2824
|
+
});
|
|
2825
|
+
clearPosition(exitTimestamp);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
if (!currentPosition || !currentPosition.qty) {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
const isLong = currentPosition.direction === "LONG";
|
|
2832
|
+
const entryPrice = currentPosition.price;
|
|
2833
|
+
const high = candle.high;
|
|
2834
|
+
const low = candle.low;
|
|
2835
|
+
for (const tp of takeProfits) {
|
|
2836
|
+
if (!currentPosition || currentPosition.qty <= 0) break;
|
|
2837
|
+
const targetPrice = tp.price;
|
|
2838
|
+
const reached = isLong ? high >= targetPrice : low <= targetPrice;
|
|
2839
|
+
if (reached) {
|
|
2840
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
2841
|
+
const qty = originalQty * tp.rate;
|
|
2842
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
2843
|
+
stage: "exit",
|
|
2844
|
+
signal: currentPosition.signal
|
|
2845
|
+
});
|
|
2846
|
+
const executionPrice = applyExecutionSlippage({
|
|
2847
|
+
price: targetPrice,
|
|
2848
|
+
direction: currentPosition.direction,
|
|
2849
|
+
stage: "exit",
|
|
2850
|
+
signal: currentPosition.signal
|
|
2851
|
+
});
|
|
2852
|
+
const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
|
|
2853
|
+
const { fee, profit } = getNetProfit({
|
|
2854
|
+
grossProfit,
|
|
2855
|
+
price: executionPrice,
|
|
2856
|
+
qty
|
|
2857
|
+
});
|
|
2858
|
+
recordExitResult({
|
|
2859
|
+
timestamp: exitTimestamp,
|
|
2860
|
+
reason: "take_profit",
|
|
2861
|
+
requestedPrice: targetPrice,
|
|
2862
|
+
executionPrice,
|
|
2863
|
+
qty,
|
|
2864
|
+
grossProfit,
|
|
2865
|
+
fee,
|
|
2866
|
+
slippageBreakdown
|
|
2867
|
+
});
|
|
2868
|
+
amount += profit;
|
|
2869
|
+
currentPositionProfit += profit;
|
|
2870
|
+
currentPosition.qty = parseFloat(
|
|
2871
|
+
(currentPosition.qty - qty).toFixed(8)
|
|
2872
|
+
);
|
|
2873
|
+
logOrder({
|
|
2874
|
+
timestamp: exitTimestamp,
|
|
2875
|
+
qty,
|
|
2876
|
+
price: executionPrice,
|
|
2877
|
+
profit,
|
|
2878
|
+
fee,
|
|
2879
|
+
type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
|
|
2880
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
2881
|
+
});
|
|
2882
|
+
tp.done = true;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
takeProfits = takeProfits.filter(({ done }) => !done);
|
|
2886
|
+
if (currentPosition && currentPosition.qty <= 0) {
|
|
2887
|
+
clearPosition(getExitTimestamp(candle));
|
|
1137
2888
|
}
|
|
1138
2889
|
},
|
|
1139
2890
|
placeOrder: async (order) => {
|
|
@@ -1141,17 +2892,77 @@ var createTestConnector = (connector, context) => {
|
|
|
1141
2892
|
return false;
|
|
1142
2893
|
}
|
|
1143
2894
|
const isLong = order.direction === "LONG";
|
|
1144
|
-
|
|
2895
|
+
const entrySlippageBreakdown = getExecutionSlippageBreakdown({
|
|
2896
|
+
stage: "entry",
|
|
2897
|
+
signal: order.signal
|
|
2898
|
+
});
|
|
2899
|
+
const entryPrice = applyExecutionSlippage({
|
|
2900
|
+
price: order.price,
|
|
2901
|
+
direction: order.direction,
|
|
2902
|
+
stage: "entry",
|
|
2903
|
+
signal: order.signal
|
|
2904
|
+
});
|
|
2905
|
+
currentPosition = { ...order, price: entryPrice, amount };
|
|
2906
|
+
currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
|
|
1145
2907
|
originalQty = order.qty;
|
|
1146
|
-
const fee
|
|
1147
|
-
|
|
2908
|
+
const { fee, profit } = getNetProfit({
|
|
2909
|
+
grossProfit: 0,
|
|
2910
|
+
price: entryPrice,
|
|
2911
|
+
qty: order.qty,
|
|
2912
|
+
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
2913
|
+
});
|
|
2914
|
+
const entrySlippageCost = getSlippageCost({
|
|
2915
|
+
requestedPrice: order.price,
|
|
2916
|
+
executionPrice: entryPrice,
|
|
2917
|
+
direction: order.direction,
|
|
2918
|
+
stage: "entry",
|
|
2919
|
+
qty: order.qty
|
|
2920
|
+
});
|
|
1148
2921
|
amount += profit;
|
|
1149
2922
|
currentPositionProfit = profit;
|
|
2923
|
+
currentTradeResult = currentSignalId ? {
|
|
2924
|
+
signalId: currentSignalId,
|
|
2925
|
+
direction: order.direction,
|
|
2926
|
+
qty: order.qty,
|
|
2927
|
+
closedQty: 0,
|
|
2928
|
+
entryTimestamp: order.timestamp,
|
|
2929
|
+
exitTimestamp: null,
|
|
2930
|
+
exitReason: null,
|
|
2931
|
+
requestedEntryPrice: order.price,
|
|
2932
|
+
entryPrice,
|
|
2933
|
+
requestedExitPrice: null,
|
|
2934
|
+
exitPrice: null,
|
|
2935
|
+
grossProfit: 0,
|
|
2936
|
+
netProfit: profit,
|
|
2937
|
+
openFee: fee,
|
|
2938
|
+
closeFee: 0,
|
|
2939
|
+
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
2940
|
+
totalFee: fee,
|
|
2941
|
+
entrySlippagePrice: entryPrice - order.price,
|
|
2942
|
+
entrySlippageBps: getSlippageBps(order.price, entryPrice),
|
|
2943
|
+
entryBaseSlippageBps: entrySlippageBreakdown.baseSlippageBps,
|
|
2944
|
+
entrySpreadBps: entrySlippageBreakdown.spreadBps,
|
|
2945
|
+
entrySpreadSlippageBps: entrySlippageBreakdown.spreadSlippageBps,
|
|
2946
|
+
entryMarketImpactBps: entrySlippageBreakdown.marketImpactBps,
|
|
2947
|
+
entryDelayRiskBps: entrySlippageBreakdown.delayRiskBps,
|
|
2948
|
+
entrySlippageCost,
|
|
2949
|
+
exitSlippagePrice: null,
|
|
2950
|
+
exitSlippageBps: null,
|
|
2951
|
+
exitBaseSlippageBps: null,
|
|
2952
|
+
exitSpreadBps: null,
|
|
2953
|
+
exitSpreadSlippageBps: null,
|
|
2954
|
+
exitMarketImpactBps: null,
|
|
2955
|
+
exitDelayRiskBps: null,
|
|
2956
|
+
exitSlippageCost: 0,
|
|
2957
|
+
totalSlippageCost: entrySlippageCost
|
|
2958
|
+
} : null;
|
|
1150
2959
|
logOrder({
|
|
1151
2960
|
...order,
|
|
2961
|
+
price: entryPrice,
|
|
1152
2962
|
profit,
|
|
1153
2963
|
fee,
|
|
1154
|
-
type: isLong ? "OPEN_LONG" : "OPEN_SHORT"
|
|
2964
|
+
type: isLong ? "OPEN_LONG" : "OPEN_SHORT",
|
|
2965
|
+
...getExecutionSlippageLogData(entrySlippageBreakdown, "entry")
|
|
1155
2966
|
});
|
|
1156
2967
|
return true;
|
|
1157
2968
|
},
|
|
@@ -1178,14 +2989,42 @@ var createTestConnector = (connector, context) => {
|
|
|
1178
2989
|
return false;
|
|
1179
2990
|
}
|
|
1180
2991
|
const isLong = currentPosition.direction === "LONG";
|
|
1181
|
-
const
|
|
2992
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
2993
|
+
stage: "exit",
|
|
2994
|
+
signal: currentPosition.signal
|
|
2995
|
+
});
|
|
2996
|
+
const executionPrice = applyExecutionSlippage({
|
|
2997
|
+
price: order.price,
|
|
2998
|
+
direction: currentPosition.direction,
|
|
2999
|
+
stage: "exit",
|
|
3000
|
+
signal: currentPosition.signal
|
|
3001
|
+
});
|
|
3002
|
+
const grossProfit = isLong ? (executionPrice - currentPosition.price) * currentPosition.qty : (currentPosition.price - executionPrice) * currentPosition.qty;
|
|
3003
|
+
const { fee, profit } = getNetProfit({
|
|
3004
|
+
grossProfit,
|
|
3005
|
+
price: executionPrice,
|
|
3006
|
+
qty: currentPosition.qty
|
|
3007
|
+
});
|
|
3008
|
+
recordExitResult({
|
|
3009
|
+
timestamp: order.timestamp,
|
|
3010
|
+
reason: "exit",
|
|
3011
|
+
requestedPrice: order.price,
|
|
3012
|
+
executionPrice,
|
|
3013
|
+
qty: currentPosition.qty,
|
|
3014
|
+
grossProfit,
|
|
3015
|
+
fee,
|
|
3016
|
+
slippageBreakdown
|
|
3017
|
+
});
|
|
1182
3018
|
amount += profit;
|
|
1183
3019
|
currentPositionProfit += profit;
|
|
1184
3020
|
logOrder({
|
|
1185
3021
|
...order,
|
|
3022
|
+
price: executionPrice,
|
|
1186
3023
|
qty: currentPosition.qty,
|
|
1187
3024
|
profit,
|
|
1188
|
-
|
|
3025
|
+
fee,
|
|
3026
|
+
type: isLong ? "CLOSE_LONG" : "CLOSE_SHORT",
|
|
3027
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
1189
3028
|
});
|
|
1190
3029
|
clearPosition(order.timestamp);
|
|
1191
3030
|
return true;
|
|
@@ -1196,10 +3035,194 @@ var createTestConnector = (connector, context) => {
|
|
|
1196
3035
|
};
|
|
1197
3036
|
};
|
|
1198
3037
|
|
|
3038
|
+
// src/executionCosts.ts
|
|
3039
|
+
var import_constants3 = require("@tradejs/core/constants");
|
|
3040
|
+
var finiteOr = (value, fallback) => {
|
|
3041
|
+
const parsed = Number(value);
|
|
3042
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
3043
|
+
};
|
|
3044
|
+
var feeCache = /* @__PURE__ */ new WeakMap();
|
|
3045
|
+
var fundingCache = /* @__PURE__ */ new WeakMap();
|
|
3046
|
+
var loadTradingFee = async (connector, symbol) => {
|
|
3047
|
+
if (!connector.getTradingFeeRate) return null;
|
|
3048
|
+
let cache = feeCache.get(connector);
|
|
3049
|
+
if (!cache) {
|
|
3050
|
+
cache = /* @__PURE__ */ new Map();
|
|
3051
|
+
feeCache.set(connector, cache);
|
|
3052
|
+
}
|
|
3053
|
+
const key = symbol.toUpperCase();
|
|
3054
|
+
const cached = cache.get(key);
|
|
3055
|
+
if (cached) return cached;
|
|
3056
|
+
const rate = await connector.getTradingFeeRate(symbol);
|
|
3057
|
+
cache.set(key, rate);
|
|
3058
|
+
return rate;
|
|
3059
|
+
};
|
|
3060
|
+
var loadFundingRates = (connector, symbol, startTime, endTime) => {
|
|
3061
|
+
if (!connector.getFundingRateHistory) return Promise.resolve([]);
|
|
3062
|
+
let cache = fundingCache.get(connector);
|
|
3063
|
+
if (!cache) {
|
|
3064
|
+
cache = /* @__PURE__ */ new Map();
|
|
3065
|
+
fundingCache.set(connector, cache);
|
|
3066
|
+
}
|
|
3067
|
+
const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
|
|
3068
|
+
const cached = cache.get(key);
|
|
3069
|
+
if (cached) return cached;
|
|
3070
|
+
const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
|
|
3071
|
+
cache.set(key, pending);
|
|
3072
|
+
return pending;
|
|
3073
|
+
};
|
|
3074
|
+
var resolveExecutionCosts = async (params) => {
|
|
3075
|
+
const { connector, symbol, config, startTime, endTime, instrument } = params;
|
|
3076
|
+
const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
|
|
3077
|
+
const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
|
|
3078
|
+
const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
|
|
3079
|
+
const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? import_constants3.FEE_PERCENT;
|
|
3080
|
+
const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? import_constants3.FEE_PERCENT;
|
|
3081
|
+
const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
|
|
3082
|
+
const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
|
|
3083
|
+
const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
|
|
3084
|
+
const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
|
|
3085
|
+
const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
|
|
3086
|
+
const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
|
|
3087
|
+
const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
|
|
3088
|
+
const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
|
|
3089
|
+
const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
|
|
3090
|
+
return {
|
|
3091
|
+
model: {
|
|
3092
|
+
fees: { makerRate, takerRate, source: feeSource },
|
|
3093
|
+
funding: {
|
|
3094
|
+
enabled: fundingEnabled,
|
|
3095
|
+
source: fundingSource,
|
|
3096
|
+
points: fundingRates.length,
|
|
3097
|
+
fromTimestamp: fundingRates[0]?.timestamp ?? null,
|
|
3098
|
+
toTimestamp: fundingRates.at(-1)?.timestamp ?? null
|
|
3099
|
+
},
|
|
3100
|
+
slippage: {
|
|
3101
|
+
baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, import_constants3.BACKTEST_BASE_SLIPPAGE_BPS),
|
|
3102
|
+
spreadMultiplier: finiteOr(
|
|
3103
|
+
config.SLIPPAGE_SPREAD_MULTIPLIER,
|
|
3104
|
+
import_constants3.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
|
|
3105
|
+
),
|
|
3106
|
+
marketImpactBps: finiteOr(
|
|
3107
|
+
config.SLIPPAGE_MARKET_IMPACT_BPS,
|
|
3108
|
+
import_constants3.BACKTEST_MARKET_IMPACT_BPS
|
|
3109
|
+
),
|
|
3110
|
+
delayRiskMultiplier: finiteOr(
|
|
3111
|
+
config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
|
|
3112
|
+
import_constants3.BACKTEST_DELAY_RISK_MULTIPLIER
|
|
3113
|
+
),
|
|
3114
|
+
source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
|
|
3115
|
+
},
|
|
3116
|
+
leverage: {
|
|
3117
|
+
requested: requestedLeverage,
|
|
3118
|
+
effective: effectiveLeverage,
|
|
3119
|
+
maxAllowed
|
|
3120
|
+
},
|
|
3121
|
+
quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
|
|
3122
|
+
capturedAt: Date.now()
|
|
3123
|
+
},
|
|
3124
|
+
fundingRates
|
|
3125
|
+
};
|
|
3126
|
+
};
|
|
3127
|
+
|
|
1199
3128
|
// src/testing.ts
|
|
3129
|
+
var isBacktestEntryDelayControlCode = (value) => typeof value === "string" && value.startsWith("BACKTEST_ENTRY_DELAY_");
|
|
3130
|
+
var CLOSED_RESULT_FLUSH_INTERVAL = 500;
|
|
3131
|
+
var DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS = 6e4;
|
|
3132
|
+
var resolvePositiveInt = (value, fallback) => {
|
|
3133
|
+
const parsed = parseInt(String(value ?? ""), 10);
|
|
3134
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
3135
|
+
};
|
|
3136
|
+
var getStrategyCandleTimeoutMs = () => resolvePositiveInt(
|
|
3137
|
+
process.env.BACKTEST_STRATEGY_CANDLE_TIMEOUT_MS,
|
|
3138
|
+
DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS
|
|
3139
|
+
);
|
|
3140
|
+
var getEffectiveTimeoutMs = (baseTimeoutMs, stageTimeoutMs) => {
|
|
3141
|
+
const base = baseTimeoutMs && baseTimeoutMs > 0 ? Math.trunc(baseTimeoutMs) : null;
|
|
3142
|
+
const stage = stageTimeoutMs && stageTimeoutMs > 0 ? Math.trunc(stageTimeoutMs) : null;
|
|
3143
|
+
if (base == null) return stage;
|
|
3144
|
+
if (stage == null) return base;
|
|
3145
|
+
return Math.min(base, stage);
|
|
3146
|
+
};
|
|
3147
|
+
var buildCandleByTimestamp = (candles) => new Map(
|
|
3148
|
+
(candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
|
|
3149
|
+
);
|
|
3150
|
+
var buildBacktestDatasetMetadata = ({
|
|
3151
|
+
backtestRunId,
|
|
3152
|
+
backtestTestKey,
|
|
3153
|
+
chunkId
|
|
3154
|
+
}) => {
|
|
3155
|
+
if (!backtestRunId || !backtestTestKey || !chunkId) {
|
|
3156
|
+
return {};
|
|
3157
|
+
}
|
|
3158
|
+
return {
|
|
3159
|
+
backtestRunId,
|
|
3160
|
+
backtestTestKey,
|
|
3161
|
+
backtestChunkId: chunkId
|
|
3162
|
+
};
|
|
3163
|
+
};
|
|
3164
|
+
var cloneAiPayloadSignal = (signal) => {
|
|
3165
|
+
const cloneValue = (value) => {
|
|
3166
|
+
if (value == null) {
|
|
3167
|
+
return value;
|
|
3168
|
+
}
|
|
3169
|
+
if (typeof structuredClone === "function") {
|
|
3170
|
+
return structuredClone(value);
|
|
3171
|
+
}
|
|
3172
|
+
return JSON.parse(JSON.stringify(value));
|
|
3173
|
+
};
|
|
3174
|
+
return {
|
|
3175
|
+
...signal,
|
|
3176
|
+
figures: cloneValue(signal.figures),
|
|
3177
|
+
indicators: cloneValue(signal.indicators),
|
|
3178
|
+
additionalIndicators: cloneValue(signal.additionalIndicators)
|
|
3179
|
+
};
|
|
3180
|
+
};
|
|
3181
|
+
var buildReplaySignalEvaluationRecord = ({
|
|
3182
|
+
signal,
|
|
3183
|
+
testId,
|
|
3184
|
+
userName,
|
|
3185
|
+
strategyName,
|
|
3186
|
+
symbol,
|
|
3187
|
+
interval,
|
|
3188
|
+
candle
|
|
3189
|
+
}) => {
|
|
3190
|
+
if (!signal || typeof signal === "string") {
|
|
3191
|
+
return {
|
|
3192
|
+
evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
|
|
3193
|
+
userName,
|
|
3194
|
+
strategy: strategyName,
|
|
3195
|
+
symbol,
|
|
3196
|
+
interval,
|
|
3197
|
+
timestamp: candle.timestamp,
|
|
3198
|
+
evaluatedAt: candle.timestamp,
|
|
3199
|
+
status: "skip",
|
|
3200
|
+
reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
|
|
3204
|
+
return {
|
|
3205
|
+
evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signalTimestamp}`,
|
|
3206
|
+
userName,
|
|
3207
|
+
strategy: signal.strategy || strategyName,
|
|
3208
|
+
symbol: signal.symbol || symbol,
|
|
3209
|
+
interval: signal.interval || interval,
|
|
3210
|
+
timestamp: signalTimestamp,
|
|
3211
|
+
evaluatedAt: candle.timestamp,
|
|
3212
|
+
status: "signal",
|
|
3213
|
+
reason: signal.orderSkipReason || signal.orderStatus,
|
|
3214
|
+
signalId: signal.signalId,
|
|
3215
|
+
direction: signal.direction,
|
|
3216
|
+
orderStatus: signal.orderStatus,
|
|
3217
|
+
orderSkipReason: signal.orderSkipReason,
|
|
3218
|
+
aiAnalysis: signal.aiAnalysis ?? null,
|
|
3219
|
+
ml: signal.ml
|
|
3220
|
+
};
|
|
3221
|
+
};
|
|
1200
3222
|
var createTestingKlineCacheState = () => ({
|
|
1201
3223
|
coinKlineCache: /* @__PURE__ */ new Map(),
|
|
1202
3224
|
btcKlineCache: /* @__PURE__ */ new Map(),
|
|
3225
|
+
ethKlineCache: /* @__PURE__ */ new Map(),
|
|
1203
3226
|
btcBinanceKlineCache: /* @__PURE__ */ new Map(),
|
|
1204
3227
|
btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
|
|
1205
3228
|
preparedDataCache: /* @__PURE__ */ new Map(),
|
|
@@ -1226,11 +3249,15 @@ var getKlineCacheKey = (params) => {
|
|
|
1226
3249
|
preloadStart,
|
|
1227
3250
|
end,
|
|
1228
3251
|
interval,
|
|
1229
|
-
cacheOnly
|
|
3252
|
+
cacheOnly,
|
|
3253
|
+
universe,
|
|
3254
|
+
accountId
|
|
1230
3255
|
} = params;
|
|
1231
3256
|
return [
|
|
1232
3257
|
userName,
|
|
1233
3258
|
connectorName,
|
|
3259
|
+
universe ?? "crypto",
|
|
3260
|
+
accountId ?? "default",
|
|
1234
3261
|
symbol,
|
|
1235
3262
|
preloadStart,
|
|
1236
3263
|
end,
|
|
@@ -1248,21 +3275,47 @@ var getPreparedDataCacheKey = (params) => {
|
|
|
1248
3275
|
end,
|
|
1249
3276
|
interval,
|
|
1250
3277
|
btcBinanceConnectorName,
|
|
1251
|
-
btcCoinbaseConnectorName
|
|
3278
|
+
btcCoinbaseConnectorName,
|
|
3279
|
+
backtestExecutionInterval,
|
|
3280
|
+
universe,
|
|
3281
|
+
accountId
|
|
1252
3282
|
} = params;
|
|
1253
3283
|
return [
|
|
1254
3284
|
userName,
|
|
1255
3285
|
connectorName,
|
|
3286
|
+
universe ?? "crypto",
|
|
3287
|
+
accountId ?? "default",
|
|
1256
3288
|
symbol,
|
|
1257
3289
|
preloadStart,
|
|
1258
3290
|
start,
|
|
1259
3291
|
end,
|
|
1260
3292
|
interval,
|
|
3293
|
+
backtestExecutionInterval,
|
|
1261
3294
|
btcBinanceConnectorName,
|
|
1262
3295
|
btcCoinbaseConnectorName
|
|
1263
3296
|
].join(":");
|
|
1264
3297
|
};
|
|
1265
|
-
var getConnectorCacheKey = (params) => [
|
|
3298
|
+
var getConnectorCacheKey = (params) => [
|
|
3299
|
+
params.userName,
|
|
3300
|
+
params.connectorName,
|
|
3301
|
+
params.universe ?? "crypto",
|
|
3302
|
+
params.accountId ?? "default"
|
|
3303
|
+
].join(":");
|
|
3304
|
+
var BACKTEST_INTERVAL = "15";
|
|
3305
|
+
var resolveBacktestExecutionInterval = (interval) => {
|
|
3306
|
+
const normalized = String(interval);
|
|
3307
|
+
if (normalized === "15") {
|
|
3308
|
+
return import_constants4.BACKTEST_EXECUTION_INTERVAL;
|
|
3309
|
+
}
|
|
3310
|
+
if (normalized === "60") {
|
|
3311
|
+
return "15";
|
|
3312
|
+
}
|
|
3313
|
+
return null;
|
|
3314
|
+
};
|
|
3315
|
+
var resolveIntervalMs = (interval) => {
|
|
3316
|
+
const intervalMinutes = Number(interval);
|
|
3317
|
+
return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 6e4 : Number(BACKTEST_INTERVAL) * 6e4;
|
|
3318
|
+
};
|
|
1266
3319
|
var splitCandlesForTesting = (candles, start, preloadStart) => {
|
|
1267
3320
|
const prevData = [];
|
|
1268
3321
|
const testData = [];
|
|
@@ -1276,9 +3329,56 @@ var splitCandlesForTesting = (candles, start, preloadStart) => {
|
|
|
1276
3329
|
}
|
|
1277
3330
|
return { prevData, testData };
|
|
1278
3331
|
};
|
|
3332
|
+
var getCurrentOpenTimestamp = (interval) => {
|
|
3333
|
+
const intervalMs = resolveIntervalMs(interval);
|
|
3334
|
+
return Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
3335
|
+
};
|
|
3336
|
+
var filterClosedAlignedCandles = (data, btcData, interval) => {
|
|
3337
|
+
const currentOpenTimestamp = getCurrentOpenTimestamp(interval);
|
|
3338
|
+
const closedData = [];
|
|
3339
|
+
const closedBtcData = [];
|
|
3340
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
3341
|
+
const candle = data[index];
|
|
3342
|
+
const btcCandle = btcData[index];
|
|
3343
|
+
if (!candle || !btcCandle || candle.timestamp >= currentOpenTimestamp) {
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
closedData.push(candle);
|
|
3347
|
+
closedBtcData.push(btcCandle);
|
|
3348
|
+
}
|
|
3349
|
+
return {
|
|
3350
|
+
data: closedData,
|
|
3351
|
+
btcData: closedBtcData
|
|
3352
|
+
};
|
|
3353
|
+
};
|
|
3354
|
+
var shouldLoadBacktestExecutionCandles = (interval) => {
|
|
3355
|
+
if (!import_constants4.BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
|
|
3356
|
+
return false;
|
|
3357
|
+
}
|
|
3358
|
+
const executionInterval = resolveBacktestExecutionInterval(interval);
|
|
3359
|
+
if (!executionInterval) {
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
const primaryMs = resolveIntervalMs(interval);
|
|
3363
|
+
const executionMs = resolveIntervalMs(executionInterval);
|
|
3364
|
+
return executionMs > 0 && executionMs < primaryMs;
|
|
3365
|
+
};
|
|
1279
3366
|
var getCachedConnector = async (params) => {
|
|
1280
|
-
const {
|
|
1281
|
-
|
|
3367
|
+
const {
|
|
3368
|
+
state,
|
|
3369
|
+
projectRoot,
|
|
3370
|
+
userName,
|
|
3371
|
+
connectorName,
|
|
3372
|
+
universe,
|
|
3373
|
+
accountId,
|
|
3374
|
+
deploymentId
|
|
3375
|
+
} = params;
|
|
3376
|
+
const cacheKey = getConnectorCacheKey({
|
|
3377
|
+
userName,
|
|
3378
|
+
connectorName,
|
|
3379
|
+
universe,
|
|
3380
|
+
accountId
|
|
3381
|
+
});
|
|
1282
3382
|
const cachedConnector = state.connectorCache.get(cacheKey);
|
|
1283
3383
|
if (cachedConnector) {
|
|
1284
3384
|
return cachedConnector;
|
|
@@ -1291,7 +3391,10 @@ var getCachedConnector = async (params) => {
|
|
|
1291
3391
|
return void 0;
|
|
1292
3392
|
}
|
|
1293
3393
|
const connector = await connectorCreator({
|
|
1294
|
-
userName
|
|
3394
|
+
userName,
|
|
3395
|
+
universe,
|
|
3396
|
+
accountId,
|
|
3397
|
+
deploymentId
|
|
1295
3398
|
});
|
|
1296
3399
|
state.connectorCache.set(cacheKey, connector);
|
|
1297
3400
|
return connector;
|
|
@@ -1306,107 +3409,64 @@ var resetTestingKlineCache = (cwd) => {
|
|
|
1306
3409
|
getTradejsProjectCwd(normalizedCwd)
|
|
1307
3410
|
);
|
|
1308
3411
|
};
|
|
1309
|
-
var
|
|
1310
|
-
userName,
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
connectorName,
|
|
1319
|
-
ml = false,
|
|
1320
|
-
ai = false,
|
|
1321
|
-
chunkId = "single",
|
|
1322
|
-
timeoutMs
|
|
1323
|
-
}) => {
|
|
1324
|
-
if (!start) {
|
|
1325
|
-
throw new Error("no start");
|
|
1326
|
-
}
|
|
1327
|
-
const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
|
|
1328
|
-
const startedAt = Date.now();
|
|
1329
|
-
const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
|
|
1330
|
-
const getRemainingTimeoutMs = (stage) => {
|
|
1331
|
-
if (!timeoutMs || timeoutMs <= 0) {
|
|
1332
|
-
return null;
|
|
1333
|
-
}
|
|
1334
|
-
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
1335
|
-
if (remainingMs <= 0) {
|
|
1336
|
-
throw new Error(formatTimeoutMessage(stage));
|
|
1337
|
-
}
|
|
1338
|
-
return remainingMs;
|
|
1339
|
-
};
|
|
1340
|
-
const throwIfTimedOut = (stage) => {
|
|
1341
|
-
getRemainingTimeoutMs(stage);
|
|
1342
|
-
};
|
|
1343
|
-
const withTimeout = async (stage, promise) => {
|
|
1344
|
-
const remainingMs = getRemainingTimeoutMs(stage);
|
|
1345
|
-
if (remainingMs == null) {
|
|
1346
|
-
return promise;
|
|
3412
|
+
var releaseTestingSymbolCache = (params) => {
|
|
3413
|
+
const { cwd, userName, connectorName, symbol } = params;
|
|
3414
|
+
const { state } = getTestingKlineCacheState(cwd);
|
|
3415
|
+
const connectorPrefix = [userName, connectorName].join(":") + ":";
|
|
3416
|
+
for (const cache of [state.coinKlineCache, state.preparedDataCache]) {
|
|
3417
|
+
for (const key of cache.keys()) {
|
|
3418
|
+
if (key.startsWith(connectorPrefix) && key.split(":").includes(symbol)) {
|
|
3419
|
+
cache.delete(key);
|
|
3420
|
+
}
|
|
1347
3421
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
var prepareTestingData = async (params) => {
|
|
3425
|
+
const {
|
|
3426
|
+
state,
|
|
3427
|
+
projectRoot,
|
|
3428
|
+
userName,
|
|
3429
|
+
connectorName,
|
|
3430
|
+
symbol,
|
|
3431
|
+
preloadStart,
|
|
3432
|
+
start,
|
|
3433
|
+
end,
|
|
3434
|
+
interval,
|
|
3435
|
+
universe = "crypto",
|
|
3436
|
+
accountId,
|
|
3437
|
+
deploymentId
|
|
3438
|
+
} = params;
|
|
3439
|
+
const binanceConnector = universe === "crypto" ? await getCachedConnector({
|
|
3440
|
+
state,
|
|
3441
|
+
projectRoot,
|
|
3442
|
+
userName,
|
|
3443
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
3444
|
+
}) : void 0;
|
|
3445
|
+
const coinbaseConnector = universe === "crypto" ? await getCachedConnector({
|
|
3446
|
+
state,
|
|
3447
|
+
projectRoot,
|
|
3448
|
+
userName,
|
|
3449
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
3450
|
+
}) : void 0;
|
|
3451
|
+
const cacheOnly = true;
|
|
3452
|
+
const connector = await getCachedConnector({
|
|
3453
|
+
state,
|
|
3454
|
+
projectRoot,
|
|
3455
|
+
userName,
|
|
3456
|
+
connectorName,
|
|
3457
|
+
universe,
|
|
3458
|
+
accountId,
|
|
3459
|
+
deploymentId
|
|
3460
|
+
});
|
|
1374
3461
|
if (!connector) {
|
|
1375
3462
|
throw new Error(`Unknown connector: ${connectorName}`);
|
|
1376
3463
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
3464
|
+
if (!binanceConnector || !coinbaseConnector) {
|
|
3465
|
+
import_logger7.logger.warn(
|
|
3466
|
+
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
3467
|
+
connectorName
|
|
3468
|
+
);
|
|
1383
3469
|
}
|
|
1384
|
-
const binanceConnector = await withTimeout(
|
|
1385
|
-
"binance connector init",
|
|
1386
|
-
getCachedConnector({
|
|
1387
|
-
state,
|
|
1388
|
-
projectRoot,
|
|
1389
|
-
userName,
|
|
1390
|
-
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
1391
|
-
})
|
|
1392
|
-
);
|
|
1393
|
-
const coinbaseConnector = await withTimeout(
|
|
1394
|
-
"coinbase connector init",
|
|
1395
|
-
getCachedConnector({
|
|
1396
|
-
state,
|
|
1397
|
-
projectRoot,
|
|
1398
|
-
userName,
|
|
1399
|
-
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
1400
|
-
})
|
|
1401
|
-
);
|
|
1402
|
-
if (!binanceConnector || !coinbaseConnector) {
|
|
1403
|
-
import_logger5.logger.warn(
|
|
1404
|
-
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
1405
|
-
connectorName
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
const interval = "15";
|
|
1409
|
-
const cacheOnly = true;
|
|
1410
3470
|
const coinCacheKey = getKlineCacheKey({
|
|
1411
3471
|
userName,
|
|
1412
3472
|
connectorName,
|
|
@@ -1414,7 +3474,9 @@ var testing = async ({
|
|
|
1414
3474
|
preloadStart,
|
|
1415
3475
|
end,
|
|
1416
3476
|
interval,
|
|
1417
|
-
cacheOnly
|
|
3477
|
+
cacheOnly,
|
|
3478
|
+
universe,
|
|
3479
|
+
accountId
|
|
1418
3480
|
});
|
|
1419
3481
|
const btcCacheKey = getKlineCacheKey({
|
|
1420
3482
|
userName,
|
|
@@ -1423,12 +3485,23 @@ var testing = async ({
|
|
|
1423
3485
|
preloadStart,
|
|
1424
3486
|
end,
|
|
1425
3487
|
interval,
|
|
1426
|
-
cacheOnly
|
|
3488
|
+
cacheOnly,
|
|
3489
|
+
universe,
|
|
3490
|
+
accountId
|
|
3491
|
+
});
|
|
3492
|
+
const ethCacheKey = getKlineCacheKey({
|
|
3493
|
+
userName,
|
|
3494
|
+
connectorName,
|
|
3495
|
+
symbol: "ETHUSDT",
|
|
3496
|
+
preloadStart,
|
|
3497
|
+
end,
|
|
3498
|
+
interval,
|
|
3499
|
+
cacheOnly,
|
|
3500
|
+
universe,
|
|
3501
|
+
accountId
|
|
1427
3502
|
});
|
|
1428
3503
|
const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
|
|
1429
3504
|
const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
|
|
1430
|
-
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
1431
|
-
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
1432
3505
|
const btcBinanceCacheKey = getKlineCacheKey({
|
|
1433
3506
|
userName,
|
|
1434
3507
|
connectorName: btcBinanceConnectorName,
|
|
@@ -1436,7 +3509,8 @@ var testing = async ({
|
|
|
1436
3509
|
preloadStart,
|
|
1437
3510
|
end,
|
|
1438
3511
|
interval,
|
|
1439
|
-
cacheOnly
|
|
3512
|
+
cacheOnly,
|
|
3513
|
+
universe: "crypto"
|
|
1440
3514
|
});
|
|
1441
3515
|
const btcCoinbaseCacheKey = getKlineCacheKey({
|
|
1442
3516
|
userName,
|
|
@@ -1445,10 +3519,41 @@ var testing = async ({
|
|
|
1445
3519
|
preloadStart,
|
|
1446
3520
|
end,
|
|
1447
3521
|
interval,
|
|
1448
|
-
cacheOnly
|
|
3522
|
+
cacheOnly,
|
|
3523
|
+
universe: "crypto"
|
|
1449
3524
|
});
|
|
3525
|
+
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
3526
|
+
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
3527
|
+
const cachedEthData = state.ethKlineCache.get(ethCacheKey);
|
|
1450
3528
|
const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
|
|
1451
3529
|
const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
|
|
3530
|
+
const backtestExecutionInterval = resolveBacktestExecutionInterval(interval);
|
|
3531
|
+
const backtestExecutionCacheInterval = backtestExecutionInterval ?? interval;
|
|
3532
|
+
const shouldLoadExecutionCandles = backtestExecutionInterval != null && shouldLoadBacktestExecutionCandles(interval);
|
|
3533
|
+
const executionCoinCacheKey = getKlineCacheKey({
|
|
3534
|
+
userName,
|
|
3535
|
+
connectorName,
|
|
3536
|
+
symbol,
|
|
3537
|
+
preloadStart,
|
|
3538
|
+
end,
|
|
3539
|
+
interval: backtestExecutionCacheInterval,
|
|
3540
|
+
cacheOnly,
|
|
3541
|
+
universe,
|
|
3542
|
+
accountId
|
|
3543
|
+
});
|
|
3544
|
+
const executionBtcCacheKey = getKlineCacheKey({
|
|
3545
|
+
userName,
|
|
3546
|
+
connectorName,
|
|
3547
|
+
symbol: "BTCUSDT",
|
|
3548
|
+
preloadStart,
|
|
3549
|
+
end,
|
|
3550
|
+
interval: backtestExecutionCacheInterval,
|
|
3551
|
+
cacheOnly,
|
|
3552
|
+
universe,
|
|
3553
|
+
accountId
|
|
3554
|
+
});
|
|
3555
|
+
const cachedExecutionCoinData = shouldLoadExecutionCandles ? state.coinKlineCache.get(executionCoinCacheKey) : void 0;
|
|
3556
|
+
const cachedExecutionBtcData = shouldLoadExecutionCandles ? state.btcKlineCache.get(executionBtcCacheKey) : void 0;
|
|
1452
3557
|
const preparedDataCacheKey = getPreparedDataCacheKey({
|
|
1453
3558
|
userName,
|
|
1454
3559
|
connectorName,
|
|
@@ -1457,118 +3562,407 @@ var testing = async ({
|
|
|
1457
3562
|
start,
|
|
1458
3563
|
end,
|
|
1459
3564
|
interval,
|
|
3565
|
+
backtestExecutionInterval: backtestExecutionCacheInterval,
|
|
1460
3566
|
btcBinanceConnectorName,
|
|
1461
|
-
btcCoinbaseConnectorName
|
|
3567
|
+
btcCoinbaseConnectorName,
|
|
3568
|
+
universe,
|
|
3569
|
+
accountId
|
|
1462
3570
|
});
|
|
1463
|
-
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
3571
|
+
const cachedPreparedData = state.preparedDataCache.get(preparedDataCacheKey);
|
|
3572
|
+
if (cachedPreparedData) {
|
|
3573
|
+
return cachedPreparedData;
|
|
3574
|
+
}
|
|
3575
|
+
const btcDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
|
|
3576
|
+
symbol: "BTCUSDT",
|
|
3577
|
+
start: preloadStart,
|
|
3578
|
+
end,
|
|
3579
|
+
interval,
|
|
3580
|
+
silent: true,
|
|
3581
|
+
cacheOnly
|
|
3582
|
+
});
|
|
3583
|
+
const ethDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedEthData ? Promise.resolve(cachedEthData) : connector.kline({
|
|
3584
|
+
symbol: "ETHUSDT",
|
|
3585
|
+
start: preloadStart,
|
|
3586
|
+
end,
|
|
3587
|
+
interval,
|
|
3588
|
+
silent: true,
|
|
3589
|
+
cacheOnly
|
|
3590
|
+
});
|
|
3591
|
+
const btcBinanceDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : btcBinanceCacheKey === btcCacheKey ? btcDataPromise : (binanceConnector ?? connector).kline({
|
|
3592
|
+
symbol: "BTCUSDT",
|
|
3593
|
+
start: preloadStart,
|
|
3594
|
+
end,
|
|
3595
|
+
interval,
|
|
3596
|
+
silent: true,
|
|
3597
|
+
cacheOnly
|
|
3598
|
+
});
|
|
3599
|
+
const btcCoinbaseDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : btcCoinbaseCacheKey === btcCacheKey ? btcDataPromise : (coinbaseConnector ?? connector).kline({
|
|
3600
|
+
symbol: "BTCUSDT",
|
|
3601
|
+
start: preloadStart,
|
|
3602
|
+
end,
|
|
3603
|
+
interval,
|
|
3604
|
+
silent: true,
|
|
3605
|
+
cacheOnly
|
|
3606
|
+
});
|
|
3607
|
+
const executionDataPromise = !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionCoinData ? Promise.resolve(cachedExecutionCoinData) : connector.kline({
|
|
3608
|
+
symbol,
|
|
3609
|
+
start: preloadStart,
|
|
3610
|
+
end,
|
|
3611
|
+
interval: backtestExecutionInterval,
|
|
3612
|
+
silent: true,
|
|
3613
|
+
cacheOnly
|
|
3614
|
+
});
|
|
3615
|
+
const executionBtcDataPromise = universe === "tradfi" || !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionBtcData ? Promise.resolve(cachedExecutionBtcData) : connector.kline({
|
|
3616
|
+
symbol: "BTCUSDT",
|
|
3617
|
+
start: preloadStart,
|
|
3618
|
+
end,
|
|
3619
|
+
interval: backtestExecutionInterval,
|
|
3620
|
+
silent: true,
|
|
3621
|
+
cacheOnly
|
|
3622
|
+
});
|
|
3623
|
+
const [
|
|
3624
|
+
dataRaw,
|
|
3625
|
+
btcDataRaw,
|
|
3626
|
+
ethDataRaw,
|
|
3627
|
+
btcBinanceDataRaw,
|
|
3628
|
+
btcCoinbaseDataRaw,
|
|
3629
|
+
executionDataRaw,
|
|
3630
|
+
executionBtcDataRaw
|
|
3631
|
+
] = await Promise.all([
|
|
3632
|
+
cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
|
|
3633
|
+
symbol,
|
|
3634
|
+
start: preloadStart,
|
|
3635
|
+
end,
|
|
3636
|
+
interval,
|
|
3637
|
+
silent: true,
|
|
3638
|
+
cacheOnly
|
|
3639
|
+
}),
|
|
3640
|
+
btcDataPromise,
|
|
3641
|
+
ethDataPromise,
|
|
3642
|
+
btcBinanceDataPromise,
|
|
3643
|
+
btcCoinbaseDataPromise,
|
|
3644
|
+
executionDataPromise,
|
|
3645
|
+
executionBtcDataPromise
|
|
3646
|
+
]);
|
|
3647
|
+
if (!cachedCoinData) {
|
|
3648
|
+
state.coinKlineCache.set(coinCacheKey, dataRaw);
|
|
3649
|
+
}
|
|
3650
|
+
if (!cachedBtcData) {
|
|
3651
|
+
state.btcKlineCache.set(btcCacheKey, btcDataRaw);
|
|
3652
|
+
}
|
|
3653
|
+
if (!cachedEthData) {
|
|
3654
|
+
state.ethKlineCache.set(ethCacheKey, ethDataRaw);
|
|
3655
|
+
}
|
|
3656
|
+
if (!cachedBtcBinanceData) {
|
|
3657
|
+
state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceDataRaw);
|
|
3658
|
+
}
|
|
3659
|
+
if (!cachedBtcCoinbaseData) {
|
|
3660
|
+
state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseDataRaw);
|
|
3661
|
+
}
|
|
3662
|
+
if (shouldLoadExecutionCandles && !cachedExecutionCoinData) {
|
|
3663
|
+
state.coinKlineCache.set(executionCoinCacheKey, executionDataRaw);
|
|
3664
|
+
}
|
|
3665
|
+
if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
|
|
3666
|
+
state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
|
|
3667
|
+
}
|
|
3668
|
+
const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : (0, import_indicators3.alignSortedCandlesByTimestamp)(dataRaw, btcDataRaw);
|
|
3669
|
+
const { data, btcData } = filterClosedAlignedCandles(
|
|
3670
|
+
aligned.alignedCoinCandles,
|
|
3671
|
+
aligned.alignedBtcCandles,
|
|
3672
|
+
interval
|
|
3673
|
+
);
|
|
3674
|
+
const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
|
|
3675
|
+
const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
|
|
3676
|
+
const ethData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, ethDataRaw).alignedBtcCandles;
|
|
3677
|
+
const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
|
|
3678
|
+
alignedCoinCandles: executionDataRaw,
|
|
3679
|
+
alignedBtcCandles: executionDataRaw
|
|
3680
|
+
} : (0, import_indicators3.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
|
|
3681
|
+
const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
|
|
3682
|
+
alignedExecution.alignedCoinCandles,
|
|
3683
|
+
alignedExecution.alignedBtcCandles,
|
|
3684
|
+
backtestExecutionCacheInterval
|
|
3685
|
+
);
|
|
3686
|
+
const backtestExecutionDataByTimestamp = buildCandleByTimestamp(
|
|
3687
|
+
backtestExecutionData
|
|
3688
|
+
);
|
|
3689
|
+
const backtestExecutionBtcDataByTimestamp = buildCandleByTimestamp(
|
|
3690
|
+
backtestExecutionBtcData
|
|
3691
|
+
);
|
|
3692
|
+
const { prevData, testData } = splitCandlesForTesting(
|
|
3693
|
+
data,
|
|
3694
|
+
start,
|
|
3695
|
+
preloadStart
|
|
3696
|
+
);
|
|
3697
|
+
const { prevData: btcPrevData, testData: btcTestData } = splitCandlesForTesting(btcData, start, preloadStart);
|
|
3698
|
+
const { prevData: ethPrevData, testData: ethTestData } = splitCandlesForTesting(ethData, start, preloadStart);
|
|
3699
|
+
const preparedData = {
|
|
3700
|
+
data,
|
|
3701
|
+
btcData,
|
|
3702
|
+
ethData,
|
|
3703
|
+
prevData,
|
|
3704
|
+
btcPrevData,
|
|
3705
|
+
ethPrevData,
|
|
3706
|
+
testData,
|
|
3707
|
+
btcTestData,
|
|
3708
|
+
ethTestData,
|
|
3709
|
+
btcBinanceData,
|
|
3710
|
+
btcCoinbaseData,
|
|
3711
|
+
backtestExecutionInterval: backtestExecutionCacheInterval,
|
|
3712
|
+
backtestExecutionData,
|
|
3713
|
+
backtestExecutionBtcData,
|
|
3714
|
+
backtestExecutionDataByTimestamp,
|
|
3715
|
+
backtestExecutionBtcDataByTimestamp
|
|
3716
|
+
};
|
|
3717
|
+
state.preparedDataCache.set(preparedDataCacheKey, preparedData);
|
|
3718
|
+
return preparedData;
|
|
3719
|
+
};
|
|
3720
|
+
var canRunTestsInSharedCandleLoop = (tests) => {
|
|
3721
|
+
if (tests.length <= 1) {
|
|
3722
|
+
return false;
|
|
3723
|
+
}
|
|
3724
|
+
const first = tests[0];
|
|
3725
|
+
const firstStart = first.options?.start;
|
|
3726
|
+
const firstEnd = first.options?.end;
|
|
3727
|
+
return tests.every(
|
|
3728
|
+
(test) => test.userName === first.userName && test.connectorName === first.connectorName && (test.universe ?? "crypto") === (first.universe ?? "crypto") && (test.accountId ?? null) === (first.accountId ?? null) && (test.deploymentId ?? null) === (first.deploymentId ?? null) && test.symbol === first.symbol && test.strategyName === first.strategyName && test.options?.start === firstStart && test.options?.end === firstEnd && Boolean(test.ml) === Boolean(first.ml) && Boolean(test.ai) === Boolean(first.ai) && Boolean(test.fast) === Boolean(first.fast) && Boolean(test.collectReplaySignalEvaluations) === Boolean(first.collectReplaySignalEvaluations) && (test.interval ?? BACKTEST_INTERVAL) === (first.interval ?? BACKTEST_INTERVAL) && (test.timeoutMs ?? null) === (first.timeoutMs ?? null)
|
|
3729
|
+
);
|
|
3730
|
+
};
|
|
3731
|
+
var testing = async ({
|
|
3732
|
+
userName,
|
|
3733
|
+
symbol,
|
|
3734
|
+
options: { start, end },
|
|
3735
|
+
name,
|
|
3736
|
+
testId,
|
|
3737
|
+
testSuiteId,
|
|
3738
|
+
configId,
|
|
3739
|
+
strategyName,
|
|
3740
|
+
strategyConfig,
|
|
3741
|
+
connectorName,
|
|
3742
|
+
universe = "crypto",
|
|
3743
|
+
assetClass,
|
|
3744
|
+
instrument: requestedInstrument,
|
|
3745
|
+
accountId,
|
|
3746
|
+
deploymentId,
|
|
3747
|
+
policyProfileId,
|
|
3748
|
+
interval = BACKTEST_INTERVAL,
|
|
3749
|
+
ml = false,
|
|
3750
|
+
ai = false,
|
|
3751
|
+
fast = false,
|
|
3752
|
+
collectReplaySignalEvaluations = false,
|
|
3753
|
+
chunkId = "single",
|
|
3754
|
+
backtestRunId,
|
|
3755
|
+
backtestTestKey,
|
|
3756
|
+
timeoutMs
|
|
3757
|
+
}) => {
|
|
3758
|
+
if (!start) {
|
|
3759
|
+
throw new Error("no start");
|
|
3760
|
+
}
|
|
3761
|
+
const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
|
|
3762
|
+
const startedAt = Date.now();
|
|
3763
|
+
let activeStageStartedAt = startedAt;
|
|
3764
|
+
let lastProgressSentAt = 0;
|
|
3765
|
+
let lastProgressSignature = "";
|
|
3766
|
+
let currentCandleIndex = 0;
|
|
3767
|
+
let totalCandles = 0;
|
|
3768
|
+
const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
|
|
3769
|
+
const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test ${name} (${symbol}) timed out after ${stageTimeoutMs}ms during ${stage}`;
|
|
3770
|
+
const emitProgress = (stage, options = {}) => {
|
|
3771
|
+
const now = Date.now();
|
|
3772
|
+
const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
|
|
3773
|
+
const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
|
|
3774
|
+
const signature = [
|
|
3775
|
+
stage,
|
|
3776
|
+
candleIndex,
|
|
3777
|
+
candleTotal,
|
|
3778
|
+
Math.floor((now - activeStageStartedAt) / 5e3)
|
|
3779
|
+
].join(":");
|
|
3780
|
+
if (!options.force) {
|
|
3781
|
+
if (signature === lastProgressSignature) {
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
if (now - lastProgressSentAt < 4e3) {
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
1507
3787
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
3788
|
+
lastProgressSentAt = now;
|
|
3789
|
+
lastProgressSignature = signature;
|
|
3790
|
+
process.send?.({
|
|
3791
|
+
progress: true,
|
|
3792
|
+
testName: name,
|
|
3793
|
+
symbol,
|
|
3794
|
+
strategyName,
|
|
3795
|
+
stage,
|
|
3796
|
+
candleIndex,
|
|
3797
|
+
candleTotal,
|
|
3798
|
+
elapsedMs: now - startedAt,
|
|
3799
|
+
stageElapsedMs: now - activeStageStartedAt
|
|
3800
|
+
});
|
|
3801
|
+
};
|
|
3802
|
+
const getStageTimeoutMs = () => {
|
|
3803
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
3804
|
+
return null;
|
|
1510
3805
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
3806
|
+
return timeoutMs;
|
|
3807
|
+
};
|
|
3808
|
+
const throwIfTimedOut = (stage) => {
|
|
3809
|
+
if (getStageTimeoutMs() == null) {
|
|
3810
|
+
return;
|
|
1513
3811
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
);
|
|
1521
|
-
const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
|
|
1522
|
-
btcCoinbaseData,
|
|
1523
|
-
start,
|
|
1524
|
-
preloadStart
|
|
3812
|
+
emitProgress(stage);
|
|
3813
|
+
};
|
|
3814
|
+
const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
|
|
3815
|
+
const stageTimeoutMs = getEffectiveTimeoutMs(
|
|
3816
|
+
getStageTimeoutMs() ?? void 0,
|
|
3817
|
+
stageTimeoutOverrideMs
|
|
1525
3818
|
);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
3819
|
+
if (stageTimeoutMs == null) {
|
|
3820
|
+
return promise;
|
|
3821
|
+
}
|
|
3822
|
+
activeStageStartedAt = Date.now();
|
|
3823
|
+
emitProgress(stage, { force: true });
|
|
3824
|
+
return await new Promise((resolve, reject) => {
|
|
3825
|
+
const heartbeat = setInterval(() => {
|
|
3826
|
+
emitProgress(stage);
|
|
3827
|
+
}, 5e3);
|
|
3828
|
+
const timer = setTimeout(() => {
|
|
3829
|
+
clearInterval(heartbeat);
|
|
3830
|
+
reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
|
|
3831
|
+
}, stageTimeoutMs);
|
|
3832
|
+
promise.then(
|
|
3833
|
+
(value) => {
|
|
3834
|
+
clearInterval(heartbeat);
|
|
3835
|
+
clearTimeout(timer);
|
|
3836
|
+
resolve(value);
|
|
3837
|
+
},
|
|
3838
|
+
(error) => {
|
|
3839
|
+
clearInterval(heartbeat);
|
|
3840
|
+
clearTimeout(timer);
|
|
3841
|
+
reject(error);
|
|
3842
|
+
}
|
|
3843
|
+
);
|
|
3844
|
+
});
|
|
3845
|
+
};
|
|
3846
|
+
const runStage = (stage, fn) => {
|
|
3847
|
+
if (getStageTimeoutMs() == null) {
|
|
3848
|
+
return fn();
|
|
3849
|
+
}
|
|
3850
|
+
return withTimeout(stage, fn());
|
|
3851
|
+
};
|
|
3852
|
+
const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
|
|
3853
|
+
const { projectRoot, state } = getTestingKlineCacheState();
|
|
3854
|
+
const connector = await withTimeout(
|
|
3855
|
+
"connector init",
|
|
3856
|
+
getCachedConnector({
|
|
3857
|
+
state,
|
|
3858
|
+
projectRoot,
|
|
3859
|
+
userName,
|
|
3860
|
+
connectorName,
|
|
3861
|
+
universe,
|
|
3862
|
+
accountId,
|
|
3863
|
+
deploymentId
|
|
3864
|
+
})
|
|
3865
|
+
);
|
|
3866
|
+
if (!connector) {
|
|
3867
|
+
throw new Error(`Unknown connector: ${connectorName}`);
|
|
3868
|
+
}
|
|
3869
|
+
const strategyCreator = await withTimeout(
|
|
3870
|
+
"strategy lookup",
|
|
3871
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
3872
|
+
);
|
|
3873
|
+
if (!strategyCreator) {
|
|
3874
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
1539
3875
|
}
|
|
3876
|
+
const preparedData = await withTimeout(
|
|
3877
|
+
"kline preload",
|
|
3878
|
+
prepareTestingData({
|
|
3879
|
+
state,
|
|
3880
|
+
projectRoot,
|
|
3881
|
+
userName,
|
|
3882
|
+
connectorName,
|
|
3883
|
+
symbol,
|
|
3884
|
+
preloadStart,
|
|
3885
|
+
start,
|
|
3886
|
+
end,
|
|
3887
|
+
interval,
|
|
3888
|
+
universe,
|
|
3889
|
+
accountId,
|
|
3890
|
+
deploymentId
|
|
3891
|
+
})
|
|
3892
|
+
);
|
|
1540
3893
|
if (!preparedData) {
|
|
1541
3894
|
throw new Error("Prepared backtest data not available");
|
|
1542
3895
|
}
|
|
1543
3896
|
const {
|
|
1544
3897
|
prevData,
|
|
1545
3898
|
btcPrevData,
|
|
3899
|
+
ethPrevData,
|
|
3900
|
+
ethTestData,
|
|
1546
3901
|
testData,
|
|
1547
3902
|
btcTestData,
|
|
1548
|
-
|
|
1549
|
-
|
|
3903
|
+
btcBinanceData,
|
|
3904
|
+
btcCoinbaseData,
|
|
3905
|
+
backtestExecutionInterval,
|
|
3906
|
+
backtestExecutionData,
|
|
3907
|
+
backtestExecutionBtcData,
|
|
3908
|
+
backtestExecutionDataByTimestamp,
|
|
3909
|
+
backtestExecutionBtcDataByTimestamp
|
|
1550
3910
|
} = preparedData;
|
|
3911
|
+
const runtimePrevData = prevData.slice();
|
|
3912
|
+
const runtimeBtcPrevData = btcPrevData.slice();
|
|
3913
|
+
const runtimeEthData = [...ethPrevData, ...ethTestData];
|
|
3914
|
+
totalCandles = testData.length;
|
|
3915
|
+
const instrument = requestedInstrument;
|
|
3916
|
+
const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
|
|
3917
|
+
connector,
|
|
3918
|
+
symbol,
|
|
3919
|
+
config: strategyConfig,
|
|
3920
|
+
startTime: start,
|
|
3921
|
+
endTime: end,
|
|
3922
|
+
instrument
|
|
3923
|
+
});
|
|
1551
3924
|
const testConnector = createTestConnector(connector, {
|
|
1552
3925
|
userName,
|
|
1553
3926
|
mlEnabled: ml,
|
|
1554
|
-
aiEnabled: ai
|
|
3927
|
+
aiEnabled: ai,
|
|
3928
|
+
fastMode: fast,
|
|
3929
|
+
executionCostModel,
|
|
3930
|
+
fundingRates
|
|
1555
3931
|
});
|
|
1556
3932
|
const strategy = await withTimeout(
|
|
1557
3933
|
"strategy init",
|
|
1558
3934
|
strategyCreator({
|
|
1559
3935
|
userName,
|
|
1560
|
-
|
|
3936
|
+
connectorName,
|
|
3937
|
+
universe,
|
|
3938
|
+
assetClass: assetClass ?? instrument?.assetClass,
|
|
3939
|
+
instrument,
|
|
3940
|
+
accountId,
|
|
3941
|
+
deploymentId,
|
|
3942
|
+
policyProfileId,
|
|
3943
|
+
config: {
|
|
3944
|
+
...strategyConfig,
|
|
3945
|
+
INTERVAL: interval
|
|
3946
|
+
},
|
|
1561
3947
|
symbol,
|
|
1562
|
-
data:
|
|
1563
|
-
btcData:
|
|
1564
|
-
|
|
1565
|
-
|
|
3948
|
+
data: runtimePrevData,
|
|
3949
|
+
btcData: runtimeBtcPrevData,
|
|
3950
|
+
ethData: runtimeEthData,
|
|
3951
|
+
btcBinanceData,
|
|
3952
|
+
btcCoinbaseData,
|
|
3953
|
+
backtestExecutionMarketData: {
|
|
3954
|
+
interval: backtestExecutionInterval,
|
|
3955
|
+
data: backtestExecutionData,
|
|
3956
|
+
btcData: backtestExecutionBtcData,
|
|
3957
|
+
dataByTimestamp: backtestExecutionDataByTimestamp,
|
|
3958
|
+
btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
|
|
3959
|
+
},
|
|
1566
3960
|
connector: testConnector
|
|
1567
3961
|
})
|
|
1568
3962
|
);
|
|
1569
3963
|
const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
|
|
1570
3964
|
const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
|
|
1571
|
-
const replaySignalEvaluations = [];
|
|
3965
|
+
const replaySignalEvaluations = collectReplaySignalEvaluations ? [] : null;
|
|
1572
3966
|
const flushClosedResultsBatch = async () => {
|
|
1573
3967
|
if (!ml && !ai) return;
|
|
1574
3968
|
const batch = await testConnector.drainMlResultsBatch();
|
|
@@ -1580,7 +3974,14 @@ var testing = async ({
|
|
|
1580
3974
|
const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
|
|
1581
3975
|
profit: resultRecord.profit
|
|
1582
3976
|
});
|
|
1583
|
-
const row =
|
|
3977
|
+
const row = {
|
|
3978
|
+
...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
|
|
3979
|
+
...buildBacktestDatasetMetadata({
|
|
3980
|
+
backtestRunId,
|
|
3981
|
+
backtestTestKey,
|
|
3982
|
+
chunkId
|
|
3983
|
+
})
|
|
3984
|
+
};
|
|
1584
3985
|
await (0, import_ml2.appendMlDatasetRow)({
|
|
1585
3986
|
strategyName,
|
|
1586
3987
|
chunkId,
|
|
@@ -1590,62 +3991,54 @@ var testing = async ({
|
|
|
1590
3991
|
const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
|
|
1591
3992
|
if (aiRowBase) {
|
|
1592
3993
|
pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
3994
|
+
const { signal: aiSignal, ...rowBase } = aiRowBase;
|
|
1593
3995
|
await (0, import_ai2.appendAiDatasetRow)({
|
|
1594
3996
|
strategyName,
|
|
1595
3997
|
chunkId,
|
|
1596
3998
|
row: {
|
|
1597
|
-
...
|
|
1598
|
-
|
|
3999
|
+
...rowBase,
|
|
4000
|
+
payload: buildAiPayload(aiSignal),
|
|
4001
|
+
profit: resultRecord.profit,
|
|
4002
|
+
tradeResult: resultRecord.tradeResult
|
|
1599
4003
|
}
|
|
1600
4004
|
});
|
|
1601
4005
|
}
|
|
1602
4006
|
}
|
|
1603
4007
|
};
|
|
1604
|
-
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
4008
|
+
const processSignal = async (signal, candle) => {
|
|
4009
|
+
if (isBacktestEntryDelayControlCode(signal)) {
|
|
4010
|
+
return;
|
|
1607
4011
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
strategy: strategyName,
|
|
1621
|
-
symbol,
|
|
1622
|
-
interval,
|
|
1623
|
-
timestamp: candle.timestamp,
|
|
1624
|
-
evaluatedAt: candle.timestamp,
|
|
1625
|
-
status: "skip",
|
|
1626
|
-
reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
|
|
1627
|
-
});
|
|
1628
|
-
} else {
|
|
1629
|
-
replaySignalEvaluations.push({
|
|
1630
|
-
evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signal.timestamp || candle.timestamp}`,
|
|
1631
|
-
userName,
|
|
1632
|
-
strategy: signal.strategy || strategyName,
|
|
1633
|
-
symbol: signal.symbol || symbol,
|
|
1634
|
-
interval: signal.interval || interval,
|
|
1635
|
-
timestamp: typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp,
|
|
1636
|
-
evaluatedAt: candle.timestamp,
|
|
1637
|
-
status: "signal",
|
|
1638
|
-
reason: signal.orderSkipReason || signal.orderStatus,
|
|
1639
|
-
signalId: signal.signalId,
|
|
1640
|
-
direction: signal.direction,
|
|
1641
|
-
orderStatus: signal.orderStatus,
|
|
1642
|
-
orderSkipReason: signal.orderSkipReason,
|
|
1643
|
-
aiAnalysis: signal.aiAnalysis ?? null,
|
|
1644
|
-
ml: signal.ml
|
|
1645
|
-
});
|
|
4012
|
+
if (replaySignalEvaluations) {
|
|
4013
|
+
replaySignalEvaluations.push(
|
|
4014
|
+
buildReplaySignalEvaluationRecord({
|
|
4015
|
+
signal,
|
|
4016
|
+
testId,
|
|
4017
|
+
userName,
|
|
4018
|
+
strategyName,
|
|
4019
|
+
symbol,
|
|
4020
|
+
interval,
|
|
4021
|
+
candle
|
|
4022
|
+
})
|
|
4023
|
+
);
|
|
1646
4024
|
}
|
|
1647
4025
|
const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
|
|
1648
4026
|
if (shouldCapturePayload) {
|
|
4027
|
+
await withTimeout(
|
|
4028
|
+
"binance market context",
|
|
4029
|
+
enrichSignalWithBinanceMarketContext({
|
|
4030
|
+
signal,
|
|
4031
|
+
env: "BACKTEST"
|
|
4032
|
+
})
|
|
4033
|
+
);
|
|
4034
|
+
await withTimeout(
|
|
4035
|
+
"coinmarketcap context",
|
|
4036
|
+
enrichSignalWithCoinMarketCapContext({
|
|
4037
|
+
signal,
|
|
4038
|
+
env: "BACKTEST",
|
|
4039
|
+
enabled: Boolean(ml || ai)
|
|
4040
|
+
})
|
|
4041
|
+
);
|
|
1649
4042
|
await withTimeout(
|
|
1650
4043
|
"derivatives context",
|
|
1651
4044
|
enrichSignalWithDerivativesContext({
|
|
@@ -1662,6 +4055,7 @@ var testing = async ({
|
|
|
1662
4055
|
testId,
|
|
1663
4056
|
testSuiteId,
|
|
1664
4057
|
testName: name,
|
|
4058
|
+
configId,
|
|
1665
4059
|
symbol,
|
|
1666
4060
|
strategyName,
|
|
1667
4061
|
strategyConfig,
|
|
@@ -1677,25 +4071,517 @@ var testing = async ({
|
|
|
1677
4071
|
symbol: signal.symbol || symbol,
|
|
1678
4072
|
direction: signal.direction,
|
|
1679
4073
|
timestamp: signal.timestamp,
|
|
1680
|
-
|
|
4074
|
+
signal: cloneAiPayloadSignal(signal),
|
|
1681
4075
|
testId,
|
|
1682
4076
|
testSuiteId,
|
|
1683
4077
|
testName: name,
|
|
1684
|
-
|
|
4078
|
+
configId,
|
|
4079
|
+
connectorName,
|
|
4080
|
+
...buildBacktestDatasetMetadata({
|
|
4081
|
+
backtestRunId,
|
|
4082
|
+
backtestTestKey,
|
|
4083
|
+
chunkId
|
|
4084
|
+
})
|
|
1685
4085
|
});
|
|
1686
4086
|
}
|
|
4087
|
+
};
|
|
4088
|
+
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
4089
|
+
if (candleIndex % 25 === 0) {
|
|
4090
|
+
throwIfTimedOut("candle loop");
|
|
4091
|
+
}
|
|
4092
|
+
currentCandleIndex = candleIndex + 1;
|
|
4093
|
+
emitProgress("candle loop", {
|
|
4094
|
+
force: candleIndex === 0 || currentCandleIndex === totalCandles
|
|
4095
|
+
});
|
|
4096
|
+
const candle = testData[candleIndex];
|
|
4097
|
+
const btcCandle = btcTestData[candleIndex];
|
|
4098
|
+
const delayedSignal = await runStrategyCandleStage(
|
|
4099
|
+
"delayed entry",
|
|
4100
|
+
async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
|
|
4101
|
+
);
|
|
4102
|
+
if (delayedSignal && typeof delayedSignal !== "string") {
|
|
4103
|
+
await processSignal(delayedSignal, candle);
|
|
4104
|
+
}
|
|
4105
|
+
await runStage("exit checks", () => testConnector.checkExits(candle));
|
|
4106
|
+
const signal = await runStrategyCandleStage(
|
|
4107
|
+
"strategy signal",
|
|
4108
|
+
() => strategy(candle, btcCandle)
|
|
4109
|
+
);
|
|
4110
|
+
await processSignal(signal, candle);
|
|
4111
|
+
if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
|
|
4112
|
+
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
4113
|
+
}
|
|
1687
4114
|
}
|
|
1688
4115
|
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
1689
4116
|
const result = await withTimeout("collect result", testConnector.getResult());
|
|
1690
|
-
return {
|
|
4117
|
+
return replaySignalEvaluations ? {
|
|
1691
4118
|
...result,
|
|
1692
4119
|
inlineReplaySignalEvaluations: replaySignalEvaluations
|
|
4120
|
+
} : result;
|
|
4121
|
+
};
|
|
4122
|
+
var testingGroupInSharedCandleLoop = async (tests) => {
|
|
4123
|
+
if (!canRunTestsInSharedCandleLoop(tests)) {
|
|
4124
|
+
const results = [];
|
|
4125
|
+
for (const test of tests) {
|
|
4126
|
+
const result = await testing(test);
|
|
4127
|
+
if (result) {
|
|
4128
|
+
results.push({ test, result });
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
return results;
|
|
4132
|
+
}
|
|
4133
|
+
const first = tests[0];
|
|
4134
|
+
const {
|
|
4135
|
+
userName,
|
|
4136
|
+
symbol,
|
|
4137
|
+
options: { start, end },
|
|
4138
|
+
strategyName,
|
|
4139
|
+
connectorName,
|
|
4140
|
+
universe = "crypto",
|
|
4141
|
+
accountId,
|
|
4142
|
+
deploymentId,
|
|
4143
|
+
interval = BACKTEST_INTERVAL,
|
|
4144
|
+
ml = false,
|
|
4145
|
+
ai = false,
|
|
4146
|
+
fast = false,
|
|
4147
|
+
collectReplaySignalEvaluations = false,
|
|
4148
|
+
chunkId = "single",
|
|
4149
|
+
timeoutMs
|
|
4150
|
+
} = first;
|
|
4151
|
+
if (!start) {
|
|
4152
|
+
throw new Error("no start");
|
|
4153
|
+
}
|
|
4154
|
+
const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
|
|
4155
|
+
const startedAt = Date.now();
|
|
4156
|
+
let activeStageStartedAt = startedAt;
|
|
4157
|
+
let lastProgressSentAt = 0;
|
|
4158
|
+
let lastProgressSignature = "";
|
|
4159
|
+
let currentCandleIndex = 0;
|
|
4160
|
+
let totalCandles = 0;
|
|
4161
|
+
const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
|
|
4162
|
+
const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test group ${strategyName}/${symbol} timed out after ${stageTimeoutMs}ms during ${stage}`;
|
|
4163
|
+
const emitProgress = (stage, options = {}) => {
|
|
4164
|
+
const now = Date.now();
|
|
4165
|
+
const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
|
|
4166
|
+
const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
|
|
4167
|
+
const signature = [
|
|
4168
|
+
stage,
|
|
4169
|
+
candleIndex,
|
|
4170
|
+
candleTotal,
|
|
4171
|
+
Math.floor((now - activeStageStartedAt) / 5e3)
|
|
4172
|
+
].join(":");
|
|
4173
|
+
if (!options.force) {
|
|
4174
|
+
if (signature === lastProgressSignature) {
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
4177
|
+
if (now - lastProgressSentAt < 4e3) {
|
|
4178
|
+
return;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
lastProgressSentAt = now;
|
|
4182
|
+
lastProgressSignature = signature;
|
|
4183
|
+
process.send?.({
|
|
4184
|
+
progress: true,
|
|
4185
|
+
testName: first.name,
|
|
4186
|
+
symbol,
|
|
4187
|
+
strategyName,
|
|
4188
|
+
stage,
|
|
4189
|
+
candleIndex,
|
|
4190
|
+
candleTotal,
|
|
4191
|
+
elapsedMs: now - startedAt,
|
|
4192
|
+
stageElapsedMs: now - activeStageStartedAt
|
|
4193
|
+
});
|
|
4194
|
+
};
|
|
4195
|
+
const getStageTimeoutMs = () => {
|
|
4196
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
4197
|
+
return null;
|
|
4198
|
+
}
|
|
4199
|
+
return timeoutMs;
|
|
4200
|
+
};
|
|
4201
|
+
const throwIfTimedOut = (stage) => {
|
|
4202
|
+
if (getStageTimeoutMs() == null) {
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
emitProgress(stage);
|
|
4206
|
+
};
|
|
4207
|
+
const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
|
|
4208
|
+
const stageTimeoutMs = getEffectiveTimeoutMs(
|
|
4209
|
+
getStageTimeoutMs() ?? void 0,
|
|
4210
|
+
stageTimeoutOverrideMs
|
|
4211
|
+
);
|
|
4212
|
+
if (stageTimeoutMs == null) {
|
|
4213
|
+
return promise;
|
|
4214
|
+
}
|
|
4215
|
+
activeStageStartedAt = Date.now();
|
|
4216
|
+
emitProgress(stage, { force: true });
|
|
4217
|
+
return await new Promise((resolve, reject) => {
|
|
4218
|
+
const heartbeat = setInterval(() => {
|
|
4219
|
+
emitProgress(stage);
|
|
4220
|
+
}, 5e3);
|
|
4221
|
+
const timer = setTimeout(() => {
|
|
4222
|
+
clearInterval(heartbeat);
|
|
4223
|
+
reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
|
|
4224
|
+
}, stageTimeoutMs);
|
|
4225
|
+
promise.then(
|
|
4226
|
+
(value) => {
|
|
4227
|
+
clearInterval(heartbeat);
|
|
4228
|
+
clearTimeout(timer);
|
|
4229
|
+
resolve(value);
|
|
4230
|
+
},
|
|
4231
|
+
(error) => {
|
|
4232
|
+
clearInterval(heartbeat);
|
|
4233
|
+
clearTimeout(timer);
|
|
4234
|
+
reject(error);
|
|
4235
|
+
}
|
|
4236
|
+
);
|
|
4237
|
+
});
|
|
4238
|
+
};
|
|
4239
|
+
const runStage = (stage, fn) => {
|
|
4240
|
+
if (getStageTimeoutMs() == null) {
|
|
4241
|
+
return fn();
|
|
4242
|
+
}
|
|
4243
|
+
return withTimeout(stage, fn());
|
|
1693
4244
|
};
|
|
4245
|
+
const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
|
|
4246
|
+
const { projectRoot, state } = getTestingKlineCacheState();
|
|
4247
|
+
const connector = await withTimeout(
|
|
4248
|
+
"connector init",
|
|
4249
|
+
getCachedConnector({
|
|
4250
|
+
state,
|
|
4251
|
+
projectRoot,
|
|
4252
|
+
userName,
|
|
4253
|
+
connectorName,
|
|
4254
|
+
universe,
|
|
4255
|
+
accountId,
|
|
4256
|
+
deploymentId
|
|
4257
|
+
})
|
|
4258
|
+
);
|
|
4259
|
+
if (!connector) {
|
|
4260
|
+
throw new Error(`Unknown connector: ${connectorName}`);
|
|
4261
|
+
}
|
|
4262
|
+
const strategyCreator = await withTimeout(
|
|
4263
|
+
"strategy lookup",
|
|
4264
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
4265
|
+
);
|
|
4266
|
+
if (!strategyCreator) {
|
|
4267
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
4268
|
+
}
|
|
4269
|
+
const preparedData = await withTimeout(
|
|
4270
|
+
"kline preload",
|
|
4271
|
+
prepareTestingData({
|
|
4272
|
+
state,
|
|
4273
|
+
projectRoot,
|
|
4274
|
+
userName,
|
|
4275
|
+
connectorName,
|
|
4276
|
+
symbol,
|
|
4277
|
+
preloadStart,
|
|
4278
|
+
start,
|
|
4279
|
+
end,
|
|
4280
|
+
interval,
|
|
4281
|
+
universe,
|
|
4282
|
+
accountId,
|
|
4283
|
+
deploymentId
|
|
4284
|
+
})
|
|
4285
|
+
);
|
|
4286
|
+
if (!preparedData) {
|
|
4287
|
+
throw new Error("Prepared backtest data not available");
|
|
4288
|
+
}
|
|
4289
|
+
const {
|
|
4290
|
+
prevData,
|
|
4291
|
+
btcPrevData,
|
|
4292
|
+
ethPrevData,
|
|
4293
|
+
ethTestData,
|
|
4294
|
+
testData,
|
|
4295
|
+
btcTestData,
|
|
4296
|
+
btcBinanceData,
|
|
4297
|
+
btcCoinbaseData,
|
|
4298
|
+
backtestExecutionInterval,
|
|
4299
|
+
backtestExecutionData,
|
|
4300
|
+
backtestExecutionBtcData,
|
|
4301
|
+
backtestExecutionDataByTimestamp,
|
|
4302
|
+
backtestExecutionBtcDataByTimestamp
|
|
4303
|
+
} = preparedData;
|
|
4304
|
+
totalCandles = testData.length;
|
|
4305
|
+
const sharedIndicatorsReplayKey = [
|
|
4306
|
+
"shared",
|
|
4307
|
+
userName,
|
|
4308
|
+
connectorName,
|
|
4309
|
+
strategyName,
|
|
4310
|
+
symbol,
|
|
4311
|
+
interval,
|
|
4312
|
+
start,
|
|
4313
|
+
end,
|
|
4314
|
+
chunkId
|
|
4315
|
+
].join(":");
|
|
4316
|
+
const runners = [];
|
|
4317
|
+
try {
|
|
4318
|
+
for (const test of tests) {
|
|
4319
|
+
const instrument = test.instrument;
|
|
4320
|
+
const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
|
|
4321
|
+
connector,
|
|
4322
|
+
symbol: test.symbol,
|
|
4323
|
+
config: test.strategyConfig,
|
|
4324
|
+
startTime: start,
|
|
4325
|
+
endTime: end,
|
|
4326
|
+
instrument
|
|
4327
|
+
});
|
|
4328
|
+
const testConnector = createTestConnector(connector, {
|
|
4329
|
+
userName: test.userName,
|
|
4330
|
+
mlEnabled: test.ml,
|
|
4331
|
+
aiEnabled: test.ai,
|
|
4332
|
+
fastMode: test.fast,
|
|
4333
|
+
executionCostModel,
|
|
4334
|
+
fundingRates
|
|
4335
|
+
});
|
|
4336
|
+
const strategy = await withTimeout(
|
|
4337
|
+
"strategy init",
|
|
4338
|
+
strategyCreator({
|
|
4339
|
+
userName: test.userName,
|
|
4340
|
+
connectorName: test.connectorName,
|
|
4341
|
+
universe: test.universe ?? universe,
|
|
4342
|
+
assetClass: test.assetClass ?? instrument?.assetClass,
|
|
4343
|
+
instrument,
|
|
4344
|
+
accountId: test.accountId ?? accountId,
|
|
4345
|
+
deploymentId: test.deploymentId ?? deploymentId,
|
|
4346
|
+
policyProfileId: test.policyProfileId,
|
|
4347
|
+
config: {
|
|
4348
|
+
...test.strategyConfig,
|
|
4349
|
+
INTERVAL: test.interval ?? interval
|
|
4350
|
+
},
|
|
4351
|
+
symbol: test.symbol,
|
|
4352
|
+
data: prevData.slice(),
|
|
4353
|
+
btcData: btcPrevData.slice(),
|
|
4354
|
+
ethData: [...ethPrevData, ...ethTestData],
|
|
4355
|
+
btcBinanceData,
|
|
4356
|
+
btcCoinbaseData,
|
|
4357
|
+
backtestExecutionMarketData: {
|
|
4358
|
+
interval: backtestExecutionInterval,
|
|
4359
|
+
data: backtestExecutionData,
|
|
4360
|
+
btcData: backtestExecutionBtcData,
|
|
4361
|
+
dataByTimestamp: backtestExecutionDataByTimestamp,
|
|
4362
|
+
btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
|
|
4363
|
+
},
|
|
4364
|
+
connector: testConnector,
|
|
4365
|
+
sharedIndicatorsReplayKey
|
|
4366
|
+
})
|
|
4367
|
+
);
|
|
4368
|
+
runners.push({
|
|
4369
|
+
test,
|
|
4370
|
+
strategy,
|
|
4371
|
+
testConnector,
|
|
4372
|
+
pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
|
|
4373
|
+
pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
|
|
4374
|
+
replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
const flushClosedResultsBatch = async (runner) => {
|
|
4378
|
+
if (!runner.test.ml && !runner.test.ai) return;
|
|
4379
|
+
const batch = await runner.testConnector.drainMlResultsBatch();
|
|
4380
|
+
if (!batch.length) return;
|
|
4381
|
+
for (const resultRecord of batch) {
|
|
4382
|
+
const payload = runner.pendingMlPayloadBySignalId.get(
|
|
4383
|
+
resultRecord.signalId
|
|
4384
|
+
);
|
|
4385
|
+
if (payload) {
|
|
4386
|
+
runner.pendingMlPayloadBySignalId.delete(resultRecord.signalId);
|
|
4387
|
+
const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
|
|
4388
|
+
profit: resultRecord.profit
|
|
4389
|
+
});
|
|
4390
|
+
const resolvedChunkId = runner.test.chunkId ?? "single";
|
|
4391
|
+
const row = {
|
|
4392
|
+
...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
|
|
4393
|
+
...buildBacktestDatasetMetadata({
|
|
4394
|
+
backtestRunId: runner.test.backtestRunId,
|
|
4395
|
+
backtestTestKey: runner.test.backtestTestKey,
|
|
4396
|
+
chunkId: resolvedChunkId
|
|
4397
|
+
})
|
|
4398
|
+
};
|
|
4399
|
+
await (0, import_ml2.appendMlDatasetRow)({
|
|
4400
|
+
strategyName: runner.test.strategyName,
|
|
4401
|
+
chunkId: resolvedChunkId,
|
|
4402
|
+
row
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
const aiRowBase = runner.pendingAiRowBySignalId.get(
|
|
4406
|
+
resultRecord.signalId
|
|
4407
|
+
);
|
|
4408
|
+
if (aiRowBase) {
|
|
4409
|
+
runner.pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
4410
|
+
const { signal: aiSignal, ...rowBase } = aiRowBase;
|
|
4411
|
+
const resolvedChunkId = runner.test.chunkId ?? "single";
|
|
4412
|
+
await (0, import_ai2.appendAiDatasetRow)({
|
|
4413
|
+
strategyName: runner.test.strategyName,
|
|
4414
|
+
chunkId: resolvedChunkId,
|
|
4415
|
+
row: {
|
|
4416
|
+
...rowBase,
|
|
4417
|
+
payload: buildAiPayload(aiSignal),
|
|
4418
|
+
profit: resultRecord.profit,
|
|
4419
|
+
tradeResult: resultRecord.tradeResult
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
const processRunnerSignal = async (runner, signal, candle) => {
|
|
4426
|
+
if (isBacktestEntryDelayControlCode(signal)) {
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
const { test } = runner;
|
|
4430
|
+
if (runner.replaySignalEvaluations) {
|
|
4431
|
+
runner.replaySignalEvaluations.push(
|
|
4432
|
+
buildReplaySignalEvaluationRecord({
|
|
4433
|
+
signal,
|
|
4434
|
+
testId: test.testId,
|
|
4435
|
+
userName: test.userName,
|
|
4436
|
+
strategyName: test.strategyName,
|
|
4437
|
+
symbol: test.symbol,
|
|
4438
|
+
interval: test.interval ?? interval,
|
|
4439
|
+
candle
|
|
4440
|
+
})
|
|
4441
|
+
);
|
|
4442
|
+
}
|
|
4443
|
+
const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
|
|
4444
|
+
if (shouldCapturePayload) {
|
|
4445
|
+
await withTimeout(
|
|
4446
|
+
"binance market context",
|
|
4447
|
+
enrichSignalWithBinanceMarketContext({
|
|
4448
|
+
signal,
|
|
4449
|
+
env: "BACKTEST"
|
|
4450
|
+
})
|
|
4451
|
+
);
|
|
4452
|
+
await withTimeout(
|
|
4453
|
+
"coinmarketcap context",
|
|
4454
|
+
enrichSignalWithCoinMarketCapContext({
|
|
4455
|
+
signal,
|
|
4456
|
+
env: "BACKTEST",
|
|
4457
|
+
enabled: Boolean(test.ml || test.ai)
|
|
4458
|
+
})
|
|
4459
|
+
);
|
|
4460
|
+
await withTimeout(
|
|
4461
|
+
"derivatives context",
|
|
4462
|
+
enrichSignalWithDerivativesContext({
|
|
4463
|
+
signal,
|
|
4464
|
+
env: "BACKTEST"
|
|
4465
|
+
})
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
|
|
4469
|
+
const payload = buildMlPayload({
|
|
4470
|
+
signal,
|
|
4471
|
+
context: {
|
|
4472
|
+
userName: test.userName,
|
|
4473
|
+
testId: test.testId,
|
|
4474
|
+
testSuiteId: test.testSuiteId,
|
|
4475
|
+
testName: test.name,
|
|
4476
|
+
configId: test.configId,
|
|
4477
|
+
symbol: test.symbol,
|
|
4478
|
+
strategyName: test.strategyName,
|
|
4479
|
+
strategyConfig: test.strategyConfig,
|
|
4480
|
+
connectorName: test.connectorName
|
|
4481
|
+
}
|
|
4482
|
+
});
|
|
4483
|
+
runner.pendingMlPayloadBySignalId.set(signal.signalId, payload);
|
|
4484
|
+
}
|
|
4485
|
+
if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
|
|
4486
|
+
runner.pendingAiRowBySignalId.set(signal.signalId, {
|
|
4487
|
+
signalId: signal.signalId,
|
|
4488
|
+
strategyName: signal.strategy || test.strategyName,
|
|
4489
|
+
symbol: signal.symbol || test.symbol,
|
|
4490
|
+
direction: signal.direction,
|
|
4491
|
+
timestamp: signal.timestamp,
|
|
4492
|
+
signal: cloneAiPayloadSignal(signal),
|
|
4493
|
+
testId: test.testId,
|
|
4494
|
+
testSuiteId: test.testSuiteId,
|
|
4495
|
+
testName: test.name,
|
|
4496
|
+
configId: test.configId,
|
|
4497
|
+
connectorName: test.connectorName,
|
|
4498
|
+
...buildBacktestDatasetMetadata({
|
|
4499
|
+
backtestRunId: test.backtestRunId,
|
|
4500
|
+
backtestTestKey: test.backtestTestKey,
|
|
4501
|
+
chunkId: test.chunkId ?? "single"
|
|
4502
|
+
})
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
4507
|
+
if (candleIndex % 25 === 0) {
|
|
4508
|
+
throwIfTimedOut("candle loop");
|
|
4509
|
+
}
|
|
4510
|
+
currentCandleIndex = candleIndex + 1;
|
|
4511
|
+
emitProgress("candle loop", {
|
|
4512
|
+
force: candleIndex === 0 || currentCandleIndex === totalCandles
|
|
4513
|
+
});
|
|
4514
|
+
const candle = testData[candleIndex];
|
|
4515
|
+
const btcCandle = btcTestData[candleIndex];
|
|
4516
|
+
const detectorNoSignalByKey = /* @__PURE__ */ new Map();
|
|
4517
|
+
for (const runner of runners) {
|
|
4518
|
+
const { test, testConnector, strategy } = runner;
|
|
4519
|
+
const delayedSignal = await runStrategyCandleStage(
|
|
4520
|
+
"delayed entry",
|
|
4521
|
+
async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
|
|
4522
|
+
);
|
|
4523
|
+
if (delayedSignal && typeof delayedSignal !== "string") {
|
|
4524
|
+
await processRunnerSignal(runner, delayedSignal, candle);
|
|
4525
|
+
}
|
|
4526
|
+
await runStage("exit checks", () => testConnector.checkExits(candle));
|
|
4527
|
+
const detectorFanoutKey = strategy.detectorFanoutKey;
|
|
4528
|
+
const detectorSkipCode = detectorFanoutKey ? detectorNoSignalByKey.get(detectorFanoutKey) : void 0;
|
|
4529
|
+
const signal = await runStrategyCandleStage(
|
|
4530
|
+
detectorSkipCode ? "strategy detector skip" : "strategy signal",
|
|
4531
|
+
() => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
|
|
4532
|
+
candle,
|
|
4533
|
+
btcCandle,
|
|
4534
|
+
detectorSkipCode
|
|
4535
|
+
) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
|
|
4536
|
+
candle,
|
|
4537
|
+
btcCandle,
|
|
4538
|
+
detectorSkipCode
|
|
4539
|
+
) : strategy(candle, btcCandle)
|
|
4540
|
+
);
|
|
4541
|
+
if (detectorFanoutKey && strategy.detectorNoSignalSkipReason && typeof signal === "string" && signal === strategy.detectorNoSignalSkipReason) {
|
|
4542
|
+
detectorNoSignalByKey.set(detectorFanoutKey, signal);
|
|
4543
|
+
}
|
|
4544
|
+
await processRunnerSignal(runner, signal, candle);
|
|
4545
|
+
}
|
|
4546
|
+
if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
|
|
4547
|
+
await withTimeout(
|
|
4548
|
+
"flush closed results",
|
|
4549
|
+
Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
|
|
4550
|
+
);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
const results = [];
|
|
4554
|
+
for (const runner of runners) {
|
|
4555
|
+
await withTimeout(
|
|
4556
|
+
"flush closed results",
|
|
4557
|
+
flushClosedResultsBatch(runner)
|
|
4558
|
+
);
|
|
4559
|
+
const result = await withTimeout(
|
|
4560
|
+
"collect result",
|
|
4561
|
+
runner.testConnector.getResult()
|
|
4562
|
+
);
|
|
4563
|
+
results.push({
|
|
4564
|
+
test: runner.test,
|
|
4565
|
+
result: runner.replaySignalEvaluations ? {
|
|
4566
|
+
...result,
|
|
4567
|
+
inlineReplaySignalEvaluations: runner.replaySignalEvaluations
|
|
4568
|
+
} : result
|
|
4569
|
+
});
|
|
4570
|
+
}
|
|
4571
|
+
return results;
|
|
4572
|
+
} finally {
|
|
4573
|
+
(0, import_strategies4.releaseStrategyIndicatorsReplayCache)(sharedIndicatorsReplayKey);
|
|
4574
|
+
(0, import_strategies4.releaseStrategyReplayCache)(sharedIndicatorsReplayKey);
|
|
4575
|
+
}
|
|
1694
4576
|
};
|
|
1695
4577
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1696
4578
|
0 && (module.exports = {
|
|
4579
|
+
canRunTestsInSharedCandleLoop,
|
|
1697
4580
|
createTestConnector,
|
|
4581
|
+
releaseTestingSymbolCache,
|
|
1698
4582
|
resetTestingKlineCache,
|
|
4583
|
+
resolveExecutionCosts,
|
|
1699
4584
|
testing,
|
|
4585
|
+
testingGroupInSharedCandleLoop,
|
|
1700
4586
|
...require("@tradejs/core/backtest")
|
|
1701
4587
|
});
|