@tradejs/core 1.0.9 → 1.0.10
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/dist/backtest.d.mts +109 -9
- package/dist/backtest.d.ts +109 -9
- package/dist/backtest.js +487 -145
- package/dist/backtest.mjs +414 -82
- package/dist/chunk-7P2KNFD4.mjs +11847 -0
- package/dist/{chunk-2ORZC66W.mjs → chunk-DXJ4NCFJ.mjs} +63 -6
- package/dist/chunk-OJPHC3S2.mjs +8 -0
- package/dist/{chunk-FNLPYYML.mjs → chunk-PNBS6J3G.mjs} +22 -1
- package/dist/constants.d.mts +25 -5
- package/dist/constants.d.ts +25 -5
- package/dist/constants.js +78 -9
- package/dist/constants.mjs +29 -5
- package/dist/data.mjs +3 -5
- package/dist/grid.d.mts +9 -0
- package/dist/grid.d.ts +9 -0
- package/dist/grid.js +168 -0
- package/dist/grid.mjs +98 -0
- package/dist/indicators-Da_i06-8.d.mts +288 -0
- package/dist/indicators-Da_i06-8.d.ts +288 -0
- package/dist/indicators.d.mts +4 -39
- package/dist/indicators.d.ts +4 -39
- package/dist/indicators.js +10488 -423
- package/dist/indicators.mjs +11 -3
- package/dist/strategies.d.mts +31 -12
- package/dist/strategies.d.ts +31 -12
- package/dist/strategies.js +11283 -336
- package/dist/strategies.mjs +1246 -119
- package/dist/{time-BMkFD4Kd.d.mts → time-BQ3AXmxo.d.mts} +3 -1
- package/dist/{time-BMkFD4Kd.d.ts → time-BQ3AXmxo.d.ts} +3 -1
- package/dist/time.d.mts +1 -1
- package/dist/time.d.ts +1 -1
- package/dist/time.js +38 -0
- package/dist/time.mjs +6 -2
- package/dist/trade.d.mts +54 -0
- package/dist/trade.d.ts +54 -0
- package/dist/trade.js +352 -0
- package/dist/trade.mjs +264 -0
- package/package.json +19 -5
- package/dist/chunk-UK6VTOUX.mjs +0 -1810
- package/dist/indicators-B-GGjP5F.d.mts +0 -65
- package/dist/indicators-B-GGjP5F.d.ts +0 -65
package/dist/strategies.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createIndicators,
|
|
3
|
+
getRequiredControllerSeedWindow
|
|
4
|
+
} from "./chunk-7P2KNFD4.mjs";
|
|
5
|
+
import "./chunk-AYC2QVKI.mjs";
|
|
6
|
+
import "./chunk-M7QGVZ3J.mjs";
|
|
1
7
|
import {
|
|
2
8
|
uuid
|
|
3
9
|
} from "./chunk-AJK4NS7Y.mjs";
|
|
4
|
-
import {
|
|
5
|
-
createIndicators
|
|
6
|
-
} from "./chunk-UK6VTOUX.mjs";
|
|
7
|
-
import "./chunk-AYC2QVKI.mjs";
|
|
8
10
|
import {
|
|
9
11
|
getTimestamp
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-PNBS6J3G.mjs";
|
|
11
13
|
import {
|
|
12
14
|
FEE_PERCENT
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-M7QGVZ3J.mjs";
|
|
15
|
+
} from "./chunk-DXJ4NCFJ.mjs";
|
|
15
16
|
|
|
16
17
|
// src/utils/strategyHelpers/indicators.ts
|
|
17
18
|
var buildDefaultIndicatorPeriods = (config) => {
|
|
@@ -37,85 +38,288 @@ var buildDefaultIndicatorPeriods = (config) => {
|
|
|
37
38
|
assignIfFinite("levelDelay", config.LEVEL_DELAY);
|
|
38
39
|
return periods;
|
|
39
40
|
};
|
|
41
|
+
var sharedReplayControllers = /* @__PURE__ */ new Map();
|
|
42
|
+
var canUseSharedReplayController = (env, sharedReplayKey) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
|
|
43
|
+
var createSnapshotController = (value) => {
|
|
44
|
+
const nextSnapshot = typeof value.snapshot === "function" ? value.snapshot.bind(value) : value.result.bind(value);
|
|
45
|
+
return Object.assign(value, {
|
|
46
|
+
snapshot: nextSnapshot
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
var releaseStrategyIndicatorsReplayCache = (keyPrefix) => {
|
|
50
|
+
for (const key of sharedReplayControllers.keys()) {
|
|
51
|
+
if (key === keyPrefix || key.startsWith(`${keyPrefix}:`)) {
|
|
52
|
+
sharedReplayControllers.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
40
56
|
var createStrategyIndicatorsState = ({
|
|
41
57
|
env,
|
|
42
58
|
data,
|
|
43
59
|
btcData,
|
|
60
|
+
ethData = [],
|
|
44
61
|
btcBinanceData,
|
|
45
62
|
btcCoinbaseData,
|
|
46
63
|
periods,
|
|
47
|
-
pluginRegistryScope
|
|
64
|
+
pluginRegistryScope,
|
|
65
|
+
initialRuntimeState,
|
|
66
|
+
replayStartIndex = 0,
|
|
67
|
+
sharedReplayKey,
|
|
68
|
+
useBtcReference = true
|
|
48
69
|
}) => {
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
let ethDataByTimestamp = null;
|
|
71
|
+
const controllerSeedWindow = getRequiredControllerSeedWindow(periods);
|
|
72
|
+
const createController = (initialDataEnd) => {
|
|
73
|
+
const endIndex = initialDataEnd == null ? data.length : initialDataEnd;
|
|
74
|
+
const startIndex = Math.max(
|
|
75
|
+
replayStartIndex,
|
|
76
|
+
endIndex - controllerSeedWindow
|
|
77
|
+
);
|
|
78
|
+
const resolveEthDataWindow = (fromIndex, toIndex, coinWindow) => coinWindow.every(
|
|
79
|
+
(candle, offset) => ethData[fromIndex + offset]?.timestamp === candle.timestamp
|
|
80
|
+
) ? ethData.slice(fromIndex, toIndex) : (() => {
|
|
81
|
+
const timestamps = new Set(
|
|
82
|
+
coinWindow.map((candle) => candle.timestamp)
|
|
83
|
+
);
|
|
84
|
+
return ethData.filter((candle) => timestamps.has(candle.timestamp));
|
|
85
|
+
})();
|
|
86
|
+
const createControllerWithState = (coinWindow, btcWindow, ethWindow, state) => createIndicators(coinWindow, useBtcReference ? btcWindow : [], {
|
|
66
87
|
periods,
|
|
88
|
+
ethData: ethWindow,
|
|
67
89
|
btcBinanceData,
|
|
68
90
|
btcCoinbaseData,
|
|
69
|
-
pluginRegistryScope
|
|
91
|
+
pluginRegistryScope,
|
|
92
|
+
initialRuntimeState: state ?? void 0
|
|
70
93
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
if (startIndex <= replayStartIndex) {
|
|
95
|
+
const dataWindow2 = data.slice(startIndex, endIndex);
|
|
96
|
+
return createControllerWithState(
|
|
97
|
+
dataWindow2,
|
|
98
|
+
btcData.slice(startIndex, endIndex),
|
|
99
|
+
resolveEthDataWindow(startIndex, endIndex, dataWindow2),
|
|
100
|
+
initialRuntimeState
|
|
101
|
+
);
|
|
75
102
|
}
|
|
76
|
-
|
|
103
|
+
const warmupData = data.slice(replayStartIndex, startIndex);
|
|
104
|
+
const warmupController = createControllerWithState(
|
|
105
|
+
warmupData,
|
|
106
|
+
btcData.slice(replayStartIndex, startIndex),
|
|
107
|
+
resolveEthDataWindow(replayStartIndex, startIndex, warmupData),
|
|
108
|
+
initialRuntimeState
|
|
109
|
+
);
|
|
110
|
+
const dataWindow = data.slice(startIndex, endIndex);
|
|
111
|
+
return createControllerWithState(
|
|
112
|
+
dataWindow,
|
|
113
|
+
btcData.slice(startIndex, endIndex),
|
|
114
|
+
resolveEthDataWindow(startIndex, endIndex, dataWindow),
|
|
115
|
+
warmupController.checkpointRuntimeState()
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
const sharedReplayState = canUseSharedReplayController(env, sharedReplayKey) && sharedReplayKey ? (() => {
|
|
119
|
+
let existing = sharedReplayControllers.get(sharedReplayKey);
|
|
120
|
+
if (!existing) {
|
|
121
|
+
existing = {
|
|
122
|
+
controller: null,
|
|
123
|
+
appliedDataEnd: replayStartIndex,
|
|
124
|
+
lastTimestamp: null,
|
|
125
|
+
lastResult: void 0
|
|
126
|
+
};
|
|
127
|
+
sharedReplayControllers.set(sharedReplayKey, existing);
|
|
128
|
+
}
|
|
129
|
+
return existing;
|
|
130
|
+
})() : null;
|
|
131
|
+
let controller = null;
|
|
132
|
+
let appliedDataEnd = replayStartIndex;
|
|
133
|
+
let currentBarPair;
|
|
134
|
+
const resolveEthCandle = (index, candle) => {
|
|
135
|
+
if (currentBarPair?.candle.timestamp === candle.timestamp && currentBarPair.ethCandle) {
|
|
136
|
+
return currentBarPair.ethCandle;
|
|
137
|
+
}
|
|
138
|
+
const alignedEthCandle = ethData[index];
|
|
139
|
+
if (alignedEthCandle?.timestamp === candle.timestamp) {
|
|
140
|
+
return alignedEthCandle;
|
|
141
|
+
}
|
|
142
|
+
ethDataByTimestamp ??= new Map(
|
|
143
|
+
ethData.map((item) => [item.timestamp, item])
|
|
144
|
+
);
|
|
145
|
+
return ethDataByTimestamp.get(candle.timestamp);
|
|
146
|
+
};
|
|
147
|
+
const applyBar = (candle, btcCandle, ethCandle) => {
|
|
148
|
+
if (sharedReplayState) {
|
|
149
|
+
if (sharedReplayState.lastTimestamp === candle.timestamp) {
|
|
150
|
+
return sharedReplayState.lastResult ?? sharedReplayState.controller?.latestSnapshot();
|
|
151
|
+
}
|
|
152
|
+
if (sharedReplayState.lastTimestamp != null && candle.timestamp < sharedReplayState.lastTimestamp) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Shared replay indicators received non-monotonic candle timestamp ${candle.timestamp} after ${sharedReplayState.lastTimestamp}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (!sharedReplayState.controller) {
|
|
158
|
+
sharedReplayState.controller = createController(
|
|
159
|
+
sharedReplayState.appliedDataEnd
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
sharedReplayState.lastTimestamp = candle.timestamp;
|
|
163
|
+
sharedReplayState.lastResult = ethCandle == null ? sharedReplayState.controller.next(candle, btcCandle) : sharedReplayState.controller.next(candle, btcCandle, ethCandle);
|
|
164
|
+
return sharedReplayState.lastResult;
|
|
165
|
+
}
|
|
166
|
+
if (!controller) {
|
|
167
|
+
controller = createController(appliedDataEnd);
|
|
168
|
+
}
|
|
169
|
+
const result = ethCandle == null ? controller.next(candle, btcCandle) : controller.next(candle, btcCandle, ethCandle);
|
|
170
|
+
return result;
|
|
171
|
+
};
|
|
172
|
+
const syncDataRange = (targetDataEnd) => {
|
|
173
|
+
const safeTargetDataEnd = Math.max(replayStartIndex, targetDataEnd);
|
|
174
|
+
if (sharedReplayState) {
|
|
175
|
+
if (!sharedReplayState.controller) {
|
|
176
|
+
sharedReplayState.controller = createController(safeTargetDataEnd);
|
|
177
|
+
sharedReplayState.appliedDataEnd = safeTargetDataEnd;
|
|
178
|
+
const lastCandle = data[safeTargetDataEnd - 1];
|
|
179
|
+
sharedReplayState.lastTimestamp = lastCandle?.timestamp ?? null;
|
|
180
|
+
sharedReplayState.lastResult = void 0;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (safeTargetDataEnd < sharedReplayState.appliedDataEnd) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Shared replay indicators cannot rewind from index ${sharedReplayState.appliedDataEnd} to ${safeTargetDataEnd}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
for (let index = sharedReplayState.appliedDataEnd; index < safeTargetDataEnd; index += 1) {
|
|
189
|
+
const candle = data[index];
|
|
190
|
+
const btcCandle = btcData[index];
|
|
191
|
+
const ethCandle = candle == null ? void 0 : resolveEthCandle(index, candle);
|
|
192
|
+
if (!candle || useBtcReference && !btcCandle) continue;
|
|
193
|
+
sharedReplayState.lastTimestamp = candle.timestamp;
|
|
194
|
+
sharedReplayState.lastResult = ethCandle == null ? sharedReplayState.controller.next(candle, btcCandle) : sharedReplayState.controller.next(candle, btcCandle, ethCandle);
|
|
195
|
+
}
|
|
196
|
+
sharedReplayState.appliedDataEnd = safeTargetDataEnd;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!controller) {
|
|
200
|
+
controller = createController(safeTargetDataEnd);
|
|
201
|
+
appliedDataEnd = safeTargetDataEnd;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (safeTargetDataEnd < appliedDataEnd) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Indicators cannot rewind from index ${appliedDataEnd} to ${safeTargetDataEnd}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
for (let index = appliedDataEnd; index < safeTargetDataEnd; index += 1) {
|
|
210
|
+
const candle = data[index];
|
|
211
|
+
const btcCandle = btcData[index];
|
|
212
|
+
const ethCandle = candle == null ? void 0 : resolveEthCandle(index, candle);
|
|
213
|
+
if (!candle || useBtcReference && !btcCandle) continue;
|
|
214
|
+
if (ethCandle == null) {
|
|
215
|
+
controller.next(candle, btcCandle);
|
|
216
|
+
} else {
|
|
217
|
+
controller.next(candle, btcCandle, ethCandle);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
appliedDataEnd = safeTargetDataEnd;
|
|
221
|
+
};
|
|
222
|
+
const syncToCurrentData = () => {
|
|
223
|
+
syncDataRange(data.length);
|
|
224
|
+
};
|
|
225
|
+
const getAppliedDataEnd = () => sharedReplayState ? sharedReplayState.appliedDataEnd : appliedDataEnd;
|
|
226
|
+
const markAppliedDataEnd = (value) => {
|
|
227
|
+
if (sharedReplayState) {
|
|
228
|
+
sharedReplayState.appliedDataEnd = Math.max(
|
|
229
|
+
sharedReplayState.appliedDataEnd,
|
|
230
|
+
value
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
appliedDataEnd = Math.max(appliedDataEnd, value);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const ensureControllerInitialized = () => {
|
|
237
|
+
syncToCurrentData();
|
|
238
|
+
if (sharedReplayState) {
|
|
239
|
+
if (!sharedReplayState.controller) {
|
|
240
|
+
sharedReplayState.controller = createController(
|
|
241
|
+
sharedReplayState.appliedDataEnd
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return createSnapshotController(sharedReplayState.controller);
|
|
245
|
+
}
|
|
246
|
+
if (!controller) {
|
|
247
|
+
controller = createController(appliedDataEnd);
|
|
248
|
+
}
|
|
249
|
+
return createSnapshotController(controller);
|
|
77
250
|
};
|
|
78
251
|
return {
|
|
79
|
-
isInitialized: () => controller != null,
|
|
80
|
-
setCurrentBar: (candle, btcCandle) => {
|
|
81
|
-
currentBarPair = {
|
|
252
|
+
isInitialized: () => sharedReplayState ? sharedReplayState.controller != null : controller != null,
|
|
253
|
+
setCurrentBar: (candle, btcCandle, ethCandle) => {
|
|
254
|
+
currentBarPair = {
|
|
255
|
+
candle,
|
|
256
|
+
btcCandle: useBtcReference ? btcCandle : void 0,
|
|
257
|
+
ethCandle: useBtcReference ? ethCandle : void 0
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
updateReferenceData: (nextReferenceData) => {
|
|
261
|
+
btcBinanceData = nextReferenceData.btcBinanceData;
|
|
262
|
+
btcCoinbaseData = nextReferenceData.btcCoinbaseData;
|
|
263
|
+
controller?.updateReferenceData(nextReferenceData);
|
|
264
|
+
sharedReplayState?.controller?.updateReferenceData(nextReferenceData);
|
|
82
265
|
},
|
|
83
|
-
onBar: (candle, btcCandle) => {
|
|
266
|
+
onBar: (candle, btcCandle, ethCandle) => {
|
|
84
267
|
const resolvedCandle = candle ?? currentBarPair?.candle;
|
|
85
|
-
const resolvedBtcCandle = btcCandle ?? currentBarPair?.btcCandle;
|
|
86
|
-
|
|
87
|
-
|
|
268
|
+
const resolvedBtcCandle = useBtcReference ? btcCandle ?? currentBarPair?.btcCandle : void 0;
|
|
269
|
+
const resolvedEthCandle = ethCandle ?? currentBarPair?.ethCandle ?? (resolvedCandle == null ? void 0 : resolveEthCandle(data.length - 1, resolvedCandle));
|
|
270
|
+
if (!resolvedCandle || useBtcReference && !resolvedBtcCandle) return;
|
|
271
|
+
if (data[data.length - 1]?.timestamp === resolvedCandle.timestamp && (!useBtcReference || btcData[btcData.length - 1]?.timestamp === resolvedBtcCandle?.timestamp)) {
|
|
272
|
+
if (getAppliedDataEnd() >= data.length) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
syncDataRange(data.length - 1);
|
|
276
|
+
applyBar(resolvedCandle, resolvedBtcCandle, resolvedEthCandle);
|
|
277
|
+
markAppliedDataEnd(data.length);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
applyBar(resolvedCandle, resolvedBtcCandle, resolvedEthCandle);
|
|
88
281
|
},
|
|
89
|
-
next: (candle, btcCandle) => {
|
|
90
|
-
|
|
91
|
-
|
|
282
|
+
next: (candle, btcCandle, ethCandle) => {
|
|
283
|
+
const resolvedBtcCandle = useBtcReference ? btcCandle : void 0;
|
|
284
|
+
const resolvedEthCandle = ethCandle ?? resolveEthCandle(data.length - 1, candle);
|
|
285
|
+
const explicitCurrent = data[data.length - 1]?.timestamp === candle.timestamp && (!useBtcReference || btcData[btcData.length - 1]?.timestamp === resolvedBtcCandle?.timestamp);
|
|
286
|
+
if (explicitCurrent && getAppliedDataEnd() >= data.length) {
|
|
287
|
+
return ensureControllerInitialized().latestSnapshot();
|
|
288
|
+
}
|
|
289
|
+
const explicitCandleDataEnd = explicitCurrent ? data.length - 1 : data.length;
|
|
290
|
+
syncDataRange(explicitCandleDataEnd);
|
|
291
|
+
const result = applyBar(candle, resolvedBtcCandle, resolvedEthCandle);
|
|
292
|
+
if (explicitCandleDataEnd === data.length - 1) {
|
|
293
|
+
markAppliedDataEnd(data.length);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
92
296
|
},
|
|
93
297
|
// Lazy bootstrap for live mode: initialize on history before current bar and then apply current bar once.
|
|
94
298
|
ensureInitializedWithCurrentBar: ensureControllerInitialized,
|
|
95
|
-
snapshot: () => ensureControllerInitialized().snapshot(),
|
|
96
|
-
latestNumber: (key) =>
|
|
97
|
-
const snapshot = ensureControllerInitialized().snapshot();
|
|
98
|
-
const value = snapshot?.[key];
|
|
99
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
100
|
-
return void 0;
|
|
101
|
-
}
|
|
102
|
-
const last = value[value.length - 1];
|
|
103
|
-
return typeof last === "number" ? last : void 0;
|
|
104
|
-
}
|
|
299
|
+
snapshot: (options) => ensureControllerInitialized().snapshot(options),
|
|
300
|
+
latestNumber: (key) => ensureControllerInitialized().latestNumber(key)
|
|
105
301
|
};
|
|
106
302
|
};
|
|
107
303
|
|
|
108
304
|
// src/utils/strategyHelpers/market.ts
|
|
305
|
+
var resolveBacktestExecutionPrice = (candle, backtestPriceMode = "open") => {
|
|
306
|
+
if (backtestPriceMode === "mid") {
|
|
307
|
+
return (candle.open + candle.close) / 2;
|
|
308
|
+
}
|
|
309
|
+
if (backtestPriceMode === "open") {
|
|
310
|
+
return candle.open;
|
|
311
|
+
}
|
|
312
|
+
return candle.close;
|
|
313
|
+
};
|
|
109
314
|
var getStrategyMarketSnapshot = async ({
|
|
110
315
|
env,
|
|
111
316
|
connector,
|
|
112
317
|
symbol,
|
|
113
318
|
interval,
|
|
114
319
|
cachedData,
|
|
115
|
-
preloadStart
|
|
116
|
-
backtestPriceMode = "mid"
|
|
320
|
+
preloadStart
|
|
117
321
|
}) => {
|
|
118
|
-
const fullData = env === "BACKTEST" || env === "CRON" ? cachedData : await connector.kline({
|
|
322
|
+
const fullData = env === "BACKTEST" || env === "CRON" || env === "PARITY" ? cachedData : await connector.kline({
|
|
119
323
|
symbol,
|
|
120
324
|
start: preloadStart,
|
|
121
325
|
end: getTimestamp(),
|
|
@@ -123,18 +327,7 @@ var getStrategyMarketSnapshot = async ({
|
|
|
123
327
|
interval
|
|
124
328
|
});
|
|
125
329
|
const lastCandle = fullData[fullData.length - 1];
|
|
126
|
-
|
|
127
|
-
if (env === "BACKTEST") {
|
|
128
|
-
if (backtestPriceMode === "mid") {
|
|
129
|
-
currentPrice = (lastCandle.open + lastCandle.close) / 2;
|
|
130
|
-
} else if (backtestPriceMode === "open") {
|
|
131
|
-
currentPrice = lastCandle.open;
|
|
132
|
-
} else if (backtestPriceMode === "rand") {
|
|
133
|
-
const min = Math.min(lastCandle.low, lastCandle.high);
|
|
134
|
-
const max = Math.max(lastCandle.low, lastCandle.high);
|
|
135
|
-
currentPrice = min + Math.random() * (max - min);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
330
|
+
const currentPrice = lastCandle.close;
|
|
138
331
|
return {
|
|
139
332
|
fullData,
|
|
140
333
|
lastCandle,
|
|
@@ -202,8 +395,792 @@ var createLastTradeController = ({
|
|
|
202
395
|
getLastTradeTimestamp: () => lastTradeTimestamp
|
|
203
396
|
};
|
|
204
397
|
};
|
|
398
|
+
var stableStringify = (value, seen = /* @__PURE__ */ new WeakSet()) => {
|
|
399
|
+
if (value == null) {
|
|
400
|
+
return String(value);
|
|
401
|
+
}
|
|
402
|
+
const valueType = typeof value;
|
|
403
|
+
if (valueType === "number") {
|
|
404
|
+
return Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
|
|
405
|
+
}
|
|
406
|
+
if (valueType === "string") {
|
|
407
|
+
return JSON.stringify(value);
|
|
408
|
+
}
|
|
409
|
+
if (valueType === "boolean") {
|
|
410
|
+
return String(value);
|
|
411
|
+
}
|
|
412
|
+
if (valueType === "bigint") {
|
|
413
|
+
return JSON.stringify(`${String(value)}n`);
|
|
414
|
+
}
|
|
415
|
+
if (valueType === "undefined" || valueType === "function") {
|
|
416
|
+
return JSON.stringify(`[${valueType}]`);
|
|
417
|
+
}
|
|
418
|
+
if (Array.isArray(value)) {
|
|
419
|
+
return `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
|
|
420
|
+
}
|
|
421
|
+
if (value instanceof Date) {
|
|
422
|
+
return JSON.stringify(value.toISOString());
|
|
423
|
+
}
|
|
424
|
+
if (valueType === "object") {
|
|
425
|
+
const record = value;
|
|
426
|
+
if (seen.has(record)) {
|
|
427
|
+
return JSON.stringify("[Circular]");
|
|
428
|
+
}
|
|
429
|
+
seen.add(record);
|
|
430
|
+
const serialized = `{${Object.keys(record).sort().map(
|
|
431
|
+
(key) => `${JSON.stringify(key)}:${stableStringify(record[key], seen)}`
|
|
432
|
+
).join(",")}}`;
|
|
433
|
+
seen.delete(record);
|
|
434
|
+
return serialized;
|
|
435
|
+
}
|
|
436
|
+
return JSON.stringify(String(value));
|
|
437
|
+
};
|
|
438
|
+
var hashStableValue = (value) => {
|
|
439
|
+
const serialized = stableStringify(value);
|
|
440
|
+
let hash = 2166136261;
|
|
441
|
+
for (let index = 0; index < serialized.length; index += 1) {
|
|
442
|
+
hash ^= serialized.charCodeAt(index);
|
|
443
|
+
hash = Math.imul(hash, 16777619);
|
|
444
|
+
}
|
|
445
|
+
return (hash >>> 0).toString(36);
|
|
446
|
+
};
|
|
447
|
+
var canUseSharedStrategyState = ({
|
|
448
|
+
env,
|
|
449
|
+
sharedReplayKey,
|
|
450
|
+
getSharedReplayState,
|
|
451
|
+
sharedReplay
|
|
452
|
+
}) => sharedReplay && (env === "BACKTEST" || env === "PARITY" || env === "CRON") && Boolean(sharedReplayKey && getSharedReplayState);
|
|
453
|
+
var createStrategyStateControllerFactory = ({
|
|
454
|
+
env,
|
|
455
|
+
sharedReplayKey,
|
|
456
|
+
getSharedReplayState
|
|
457
|
+
}) => {
|
|
458
|
+
const localStores = /* @__PURE__ */ new Map();
|
|
459
|
+
return (key, createState, options = {}) => {
|
|
460
|
+
const normalizedKey = String(key ?? "").trim();
|
|
461
|
+
if (!normalizedKey) {
|
|
462
|
+
throw new Error("strategyApi.createStateController requires a state key");
|
|
463
|
+
}
|
|
464
|
+
const configKey = typeof options.configKey === "string" && options.configKey.trim() ? options.configKey.trim() : null;
|
|
465
|
+
const controllerKey = configKey ? `${normalizedKey}:${configKey}` : normalizedKey;
|
|
466
|
+
const createStore = () => ({
|
|
467
|
+
state: createState(),
|
|
468
|
+
lastTimestamp: null,
|
|
469
|
+
lastResult: void 0,
|
|
470
|
+
hasLastResult: false
|
|
471
|
+
});
|
|
472
|
+
const sharedAllowed = canUseSharedStrategyState({
|
|
473
|
+
env,
|
|
474
|
+
sharedReplayKey,
|
|
475
|
+
getSharedReplayState,
|
|
476
|
+
sharedReplay: options.sharedReplay !== false
|
|
477
|
+
});
|
|
478
|
+
const store = sharedAllowed ? getSharedReplayState(
|
|
479
|
+
`${sharedReplayKey}:state:${controllerKey}`,
|
|
480
|
+
createStore
|
|
481
|
+
) : (() => {
|
|
482
|
+
const existing = localStores.get(controllerKey);
|
|
483
|
+
if (existing) {
|
|
484
|
+
return existing;
|
|
485
|
+
}
|
|
486
|
+
const created = createStore();
|
|
487
|
+
localStores.set(
|
|
488
|
+
controllerKey,
|
|
489
|
+
created
|
|
490
|
+
);
|
|
491
|
+
return created;
|
|
492
|
+
})();
|
|
493
|
+
const snapshot = () => options.snapshot ? options.snapshot(store.state) : store.state;
|
|
494
|
+
const clearCachedResult = () => {
|
|
495
|
+
store.lastResult = void 0;
|
|
496
|
+
store.hasLastResult = false;
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
get: () => store.state,
|
|
500
|
+
set: (state) => {
|
|
501
|
+
store.state = state;
|
|
502
|
+
clearCachedResult();
|
|
503
|
+
},
|
|
504
|
+
update: (fn) => {
|
|
505
|
+
fn(store.state);
|
|
506
|
+
clearCachedResult();
|
|
507
|
+
return store.state;
|
|
508
|
+
},
|
|
509
|
+
oncePerTimestamp: (timestamp, compute) => {
|
|
510
|
+
if (!Number.isFinite(timestamp)) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Strategy state controller "${controllerKey}" received non-finite timestamp ${String(timestamp)}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (store.lastTimestamp === timestamp && store.hasLastResult) {
|
|
516
|
+
return store.lastResult;
|
|
517
|
+
}
|
|
518
|
+
if (options.monotonic !== false && store.lastTimestamp != null && timestamp < store.lastTimestamp) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Strategy state controller "${controllerKey}" received non-monotonic timestamp ${timestamp} after ${store.lastTimestamp}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const result = compute(store.state);
|
|
524
|
+
store.lastTimestamp = timestamp;
|
|
525
|
+
store.lastResult = result;
|
|
526
|
+
store.hasLastResult = true;
|
|
527
|
+
return result;
|
|
528
|
+
},
|
|
529
|
+
snapshot,
|
|
530
|
+
hash: () => {
|
|
531
|
+
const currentSnapshot = snapshot();
|
|
532
|
+
return options.hash ? options.hash(currentSnapshot) : hashStableValue(currentSnapshot);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
};
|
|
536
|
+
};
|
|
205
537
|
|
|
206
538
|
// src/utils/strategyHelpers/signalBuilders.ts
|
|
539
|
+
var COMPACT_MTF_CANDLE_LIMIT = 3;
|
|
540
|
+
var isRecordLike = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
541
|
+
var asFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
542
|
+
var asStringOrNull = (value) => typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
543
|
+
var cloneSignalPayloadDataProperties = (value, seen = /* @__PURE__ */ new WeakMap()) => {
|
|
544
|
+
if (Array.isArray(value)) {
|
|
545
|
+
return value.map((item) => cloneSignalPayloadDataProperties(item, seen));
|
|
546
|
+
}
|
|
547
|
+
if (!value || typeof value !== "object") {
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
const objectValue = value;
|
|
551
|
+
const cached = seen.get(objectValue);
|
|
552
|
+
if (cached) {
|
|
553
|
+
return cached;
|
|
554
|
+
}
|
|
555
|
+
const clone = {};
|
|
556
|
+
seen.set(objectValue, clone);
|
|
557
|
+
for (const [key, descriptor] of Object.entries(
|
|
558
|
+
Object.getOwnPropertyDescriptors(objectValue)
|
|
559
|
+
)) {
|
|
560
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
clone[key] = cloneSignalPayloadDataProperties(descriptor.value, seen);
|
|
564
|
+
}
|
|
565
|
+
return clone;
|
|
566
|
+
};
|
|
567
|
+
var copyEnumerableDataPropertiesExcept = (value, excludedKeys) => {
|
|
568
|
+
const clone = {};
|
|
569
|
+
for (const [key, descriptor] of Object.entries(
|
|
570
|
+
Object.getOwnPropertyDescriptors(value)
|
|
571
|
+
)) {
|
|
572
|
+
if (excludedKeys.has(key) || !descriptor.enumerable || !("value" in descriptor)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
clone[key] = descriptor.value;
|
|
576
|
+
}
|
|
577
|
+
return clone;
|
|
578
|
+
};
|
|
579
|
+
var cloneCompactArrayTail = (value, limit = COMPACT_MTF_CANDLE_LIMIT) => Array.isArray(value) ? cloneSignalPayloadDataProperties(value.slice(-limit)) : [];
|
|
580
|
+
var cloneCompactMtfContext = (baseContext) => {
|
|
581
|
+
const descriptor = Object.getOwnPropertyDescriptor(baseContext, "mtf");
|
|
582
|
+
const mtf = descriptor && "get" in descriptor && typeof descriptor.get === "function" ? descriptor.get.call(baseContext) : descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
583
|
+
if (!isRecordLike(mtf)) {
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
const candles = isRecordLike(mtf.candles) ? mtf.candles : {};
|
|
587
|
+
const benchmarkCandles = isRecordLike(mtf.benchmarkCandles) ? mtf.benchmarkCandles : {};
|
|
588
|
+
return {
|
|
589
|
+
compact: true,
|
|
590
|
+
candles: {
|
|
591
|
+
m15: cloneCompactArrayTail(candles.m15),
|
|
592
|
+
h1: cloneCompactArrayTail(candles.h1),
|
|
593
|
+
h4: cloneCompactArrayTail(candles.h4),
|
|
594
|
+
d1: cloneCompactArrayTail(candles.d1)
|
|
595
|
+
},
|
|
596
|
+
benchmarkCandles: {
|
|
597
|
+
m15: cloneCompactArrayTail(benchmarkCandles.m15),
|
|
598
|
+
h1: cloneCompactArrayTail(benchmarkCandles.h1),
|
|
599
|
+
h4: cloneCompactArrayTail(benchmarkCandles.h4),
|
|
600
|
+
d1: cloneCompactArrayTail(benchmarkCandles.d1)
|
|
601
|
+
},
|
|
602
|
+
...isRecordLike(mtf.summary) ? { summary: cloneSignalPayloadDataProperties(mtf.summary) } : {}
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
var toRankBucket = (value) => {
|
|
606
|
+
if (value == null) return "unknown";
|
|
607
|
+
if (value >= 95) return "extreme";
|
|
608
|
+
if (value >= 80) return "high";
|
|
609
|
+
if (value <= 20) return "low";
|
|
610
|
+
return "normal";
|
|
611
|
+
};
|
|
612
|
+
var toRangePositionBucket = (value) => {
|
|
613
|
+
if (value == null) return "unknown";
|
|
614
|
+
if (value <= 0.2) return "low";
|
|
615
|
+
if (value >= 0.8) return "high";
|
|
616
|
+
return "middle";
|
|
617
|
+
};
|
|
618
|
+
var toVolumeBucket = (value) => {
|
|
619
|
+
if (value == null) return "unknown";
|
|
620
|
+
if (value < 0.8) return "thin";
|
|
621
|
+
if (value < 1.5) return "normal";
|
|
622
|
+
if (value < 3) return "elevated";
|
|
623
|
+
return "spike";
|
|
624
|
+
};
|
|
625
|
+
var toVenueSpreadSeverity = (value) => {
|
|
626
|
+
if (value == null) return "unknown";
|
|
627
|
+
const abs = Math.abs(value);
|
|
628
|
+
if (abs >= 2) return "wide";
|
|
629
|
+
if (abs >= 1) return "elevated";
|
|
630
|
+
return "normal";
|
|
631
|
+
};
|
|
632
|
+
var toDirectionalAlignment = ({
|
|
633
|
+
direction,
|
|
634
|
+
bullValue,
|
|
635
|
+
bearValue,
|
|
636
|
+
value
|
|
637
|
+
}) => {
|
|
638
|
+
if (!direction || !value) return null;
|
|
639
|
+
return direction === "LONG" ? value === bullValue : value === bearValue;
|
|
640
|
+
};
|
|
641
|
+
var toMtfAlignmentForDirection = ({
|
|
642
|
+
direction,
|
|
643
|
+
mtfAlignment
|
|
644
|
+
}) => {
|
|
645
|
+
if (!direction || !mtfAlignment || mtfAlignment === "unknown") {
|
|
646
|
+
return "unknown";
|
|
647
|
+
}
|
|
648
|
+
if (mtfAlignment === "mixed") return "mixed";
|
|
649
|
+
if (mtfAlignment === "neutral") return "neutral";
|
|
650
|
+
if (direction === "LONG") {
|
|
651
|
+
return mtfAlignment === "aligned_bull" ? "aligned" : mtfAlignment === "aligned_bear" ? "against" : "unknown";
|
|
652
|
+
}
|
|
653
|
+
return mtfAlignment === "aligned_bear" ? "aligned" : mtfAlignment === "aligned_bull" ? "against" : "unknown";
|
|
654
|
+
};
|
|
655
|
+
var toRelativeStrengthBucket = ({
|
|
656
|
+
direction,
|
|
657
|
+
value
|
|
658
|
+
}) => {
|
|
659
|
+
if (!direction || value == null) return "unknown";
|
|
660
|
+
const signed = direction === "LONG" ? value : -value;
|
|
661
|
+
if (signed <= -3) return "strong_against";
|
|
662
|
+
if (signed < -1) return "mild_against";
|
|
663
|
+
if (signed >= 3) return "strong_with";
|
|
664
|
+
if (signed > 1) return "mild_with";
|
|
665
|
+
return "neutral";
|
|
666
|
+
};
|
|
667
|
+
var toPressureBias = (value) => value == null ? "unknown" : value >= 0.55 ? "bull" : value <= 0.45 ? "bear" : "neutral";
|
|
668
|
+
var toBiasAligned = ({
|
|
669
|
+
direction,
|
|
670
|
+
bias
|
|
671
|
+
}) => direction == null || bias === "unknown" || bias === "neutral" ? null : direction === "LONG" ? bias === "bull" : bias === "bear";
|
|
672
|
+
var clampScore = (value) => Math.max(0, Math.min(100, Math.round(value)));
|
|
673
|
+
var scoreEvidence = (evidence) => {
|
|
674
|
+
const known = evidence.filter((item) => typeof item === "boolean");
|
|
675
|
+
if (known.length === 0) return null;
|
|
676
|
+
const positives = known.filter(Boolean).length;
|
|
677
|
+
const negatives = known.length - positives;
|
|
678
|
+
return clampScore(50 + positives * 12 - negatives * 15);
|
|
679
|
+
};
|
|
680
|
+
var averageScores = (scores) => {
|
|
681
|
+
const known = scores.filter(
|
|
682
|
+
(score) => typeof score === "number" && Number.isFinite(score)
|
|
683
|
+
);
|
|
684
|
+
if (known.length === 0) return null;
|
|
685
|
+
return clampScore(
|
|
686
|
+
known.reduce((sum, score) => sum + score, 0) / known.length
|
|
687
|
+
);
|
|
688
|
+
};
|
|
689
|
+
var pushWhen = (items, condition, item) => {
|
|
690
|
+
if (condition) {
|
|
691
|
+
items.push(item);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
var buildBaseContextGateFeatures = ({
|
|
695
|
+
baseContext,
|
|
696
|
+
direction,
|
|
697
|
+
prices
|
|
698
|
+
}) => {
|
|
699
|
+
const mtfSummary = baseContext.mtf?.summary;
|
|
700
|
+
const mtfAlignmentForDirection = toMtfAlignmentForDirection({
|
|
701
|
+
direction,
|
|
702
|
+
mtfAlignment: mtfSummary?.mtfAlignment ?? null
|
|
703
|
+
});
|
|
704
|
+
const volatility = baseContext.regime?.volatility;
|
|
705
|
+
const volatilityPercentiles = volatility?.percentiles;
|
|
706
|
+
const localRange = baseContext.structure?.localRange;
|
|
707
|
+
const liquidity = baseContext.structure?.liquidity;
|
|
708
|
+
const volume = baseContext.participation?.volume;
|
|
709
|
+
const delta = baseContext.participation?.delta;
|
|
710
|
+
const tradeFlow = baseContext.participation?.tradeFlow;
|
|
711
|
+
const volumeStructure = baseContext.participation?.volumeStructure;
|
|
712
|
+
const relative = baseContext.relative?.benchmark;
|
|
713
|
+
const marketBreadth = baseContext.relative?.marketBreadth;
|
|
714
|
+
const cmcGlobal = baseContext.relative?.cmcGlobal;
|
|
715
|
+
const cmcReferenceAssets = baseContext.relative?.cmcReferenceAssets;
|
|
716
|
+
const cmcExchangeLiquidity = baseContext.relative?.cmcExchangeLiquidity;
|
|
717
|
+
const cmcFearGreed = baseContext.relative?.cmcFearGreed;
|
|
718
|
+
const cmcIndexes = baseContext.relative?.cmcIndexes;
|
|
719
|
+
const targetVsBtc = baseContext.relative?.targetVsBtc;
|
|
720
|
+
const targetVsEth = baseContext.relative?.targetVsEth;
|
|
721
|
+
const btcAltRegime = baseContext.relative?.btcAltRegime;
|
|
722
|
+
const referenceTradeFlow = baseContext.relative?.referenceTradeFlow;
|
|
723
|
+
const execution = baseContext.relative?.execution;
|
|
724
|
+
const primaryReferenceSymbol = referenceTradeFlow?.primaryReferenceSymbol;
|
|
725
|
+
const primaryReferenceTradeFlow = primaryReferenceSymbol != null ? referenceTradeFlow?.tradeFlowBySymbol?.[primaryReferenceSymbol] : void 0;
|
|
726
|
+
const atrPctRankBucket = toRankBucket(
|
|
727
|
+
asFiniteNumberOrNull(volatilityPercentiles?.atrPctRank100)
|
|
728
|
+
);
|
|
729
|
+
const bbWidthRankBucket = toRankBucket(
|
|
730
|
+
asFiniteNumberOrNull(volatilityPercentiles?.bbWidthRank100)
|
|
731
|
+
);
|
|
732
|
+
const atrPctZScore = asFiniteNumberOrNull(volatility?.atrPctZScore);
|
|
733
|
+
const breakoutState = localRange?.breakoutState ?? "unknown";
|
|
734
|
+
const rangePosition20 = asFiniteNumberOrNull(localRange?.rangePosition20);
|
|
735
|
+
const rangePositionBucket = toRangePositionBucket(rangePosition20);
|
|
736
|
+
const breakoutWithDirection = toDirectionalAlignment({
|
|
737
|
+
direction,
|
|
738
|
+
bullValue: "above_high_level",
|
|
739
|
+
bearValue: "below_low_level",
|
|
740
|
+
value: breakoutState
|
|
741
|
+
});
|
|
742
|
+
const failedBreakoutForDirection = toDirectionalAlignment({
|
|
743
|
+
direction,
|
|
744
|
+
bullValue: "failed_low_breakout",
|
|
745
|
+
bearValue: "failed_high_breakout",
|
|
746
|
+
value: breakoutState
|
|
747
|
+
});
|
|
748
|
+
const liquiditySweepForDirection = !liquidity || direction == null ? null : direction === "LONG" ? liquidity?.sweepState === "swept_low" : liquidity?.sweepState === "swept_high";
|
|
749
|
+
const nearPointOfControl = baseContext.participation?.priceVolumeProfile?.nearPointOfControl ?? null;
|
|
750
|
+
const volumeRel20 = asFiniteNumberOrNull(volume?.volumeRel20);
|
|
751
|
+
const buyPressurePct = asFiniteNumberOrNull(delta?.buyPressurePct);
|
|
752
|
+
const tradeFlowBuyPressurePct = asFiniteNumberOrNull(
|
|
753
|
+
tradeFlow?.buyPressurePct
|
|
754
|
+
);
|
|
755
|
+
const referenceTradeFlowBuyPressurePct = asFiniteNumberOrNull(
|
|
756
|
+
primaryReferenceTradeFlow?.buyPressurePct
|
|
757
|
+
);
|
|
758
|
+
const deltaDivergenceVsPrice = asStringOrNull(delta?.deltaDivergenceVsPrice);
|
|
759
|
+
const deltaBias = deltaDivergenceVsPrice === "bullish" || deltaDivergenceVsPrice === "bearish" ? deltaDivergenceVsPrice === "bullish" ? "bull" : "bear" : toPressureBias(buyPressurePct);
|
|
760
|
+
const tradeFlowBias = tradeFlow?.stale ? "unknown" : toPressureBias(tradeFlowBuyPressurePct);
|
|
761
|
+
const referenceTradeFlowBias = primaryReferenceTradeFlow?.stale ? "unknown" : toPressureBias(referenceTradeFlowBuyPressurePct);
|
|
762
|
+
const deltaAligned = toBiasAligned({ direction, bias: deltaBias });
|
|
763
|
+
const tradeFlowAligned = toBiasAligned({
|
|
764
|
+
direction,
|
|
765
|
+
bias: tradeFlowBias
|
|
766
|
+
});
|
|
767
|
+
const referenceTradeFlowAligned = toBiasAligned({
|
|
768
|
+
direction,
|
|
769
|
+
bias: referenceTradeFlowBias
|
|
770
|
+
});
|
|
771
|
+
const benchmarkTrendAlignment = relative?.trendAlignment ?? "unknown";
|
|
772
|
+
const relativeStrength1h = asFiniteNumberOrNull(relative?.relativeStrength1h);
|
|
773
|
+
const relativeStrengthBucket = toRelativeStrengthBucket({
|
|
774
|
+
direction,
|
|
775
|
+
value: relativeStrength1h
|
|
776
|
+
});
|
|
777
|
+
const benchmarkAligned = benchmarkTrendAlignment === "against_benchmark" ? false : toDirectionalAlignment({
|
|
778
|
+
direction,
|
|
779
|
+
bullValue: "aligned_bull",
|
|
780
|
+
bearValue: "aligned_bear",
|
|
781
|
+
value: benchmarkTrendAlignment
|
|
782
|
+
});
|
|
783
|
+
const totalUpVolumeShare = asFiniteNumberOrNull(
|
|
784
|
+
volumeStructure?.totalUpVolumeShare
|
|
785
|
+
);
|
|
786
|
+
const totalDownVolumeShare = asFiniteNumberOrNull(
|
|
787
|
+
volumeStructure?.totalDownVolumeShare
|
|
788
|
+
);
|
|
789
|
+
const directionalVolumeShare = direction === "LONG" ? totalUpVolumeShare : direction === "SHORT" ? totalDownVolumeShare : null;
|
|
790
|
+
const volumeStructureAligned = directionalVolumeShare == null ? null : directionalVolumeShare >= 0.5;
|
|
791
|
+
const marketBreadthReturn = asFiniteNumberOrNull(
|
|
792
|
+
marketBreadth?.equalWeightedReturn
|
|
793
|
+
);
|
|
794
|
+
const marketBreadthAligned = direction == null || marketBreadthReturn == null || marketBreadth?.stale ? null : direction === "LONG" ? marketBreadthReturn >= 0 : marketBreadthReturn <= 0;
|
|
795
|
+
const cmcAltLiquidityRegime = cmcGlobal?.altLiquidityRegime ?? "unknown";
|
|
796
|
+
const cmcAltLiquidityStale = typeof cmcGlobal?.stale === "boolean" ? cmcGlobal.stale : null;
|
|
797
|
+
const cmcAltLiquidityAligned = direction == null || cmcAltLiquidityStale === true || cmcAltLiquidityRegime === "unknown" || cmcAltLiquidityRegime === "neutral" ? null : direction === "LONG" ? cmcAltLiquidityRegime === "alt_friendly" : cmcAltLiquidityRegime === "btc_favored" || cmcAltLiquidityRegime === "risk_off";
|
|
798
|
+
const cmcEthBtcReferenceRegime = cmcReferenceAssets?.referenceLiquidityRegime ?? "unknown";
|
|
799
|
+
const cmcEthBtcStale = typeof cmcReferenceAssets?.stale === "boolean" ? cmcReferenceAssets.stale : null;
|
|
800
|
+
const cmcEthBtcAligned = direction == null || cmcEthBtcStale === true || cmcEthBtcReferenceRegime === "unknown" || cmcEthBtcReferenceRegime === "balanced" ? null : direction === "LONG" ? cmcEthBtcReferenceRegime === "eth_led" : cmcEthBtcReferenceRegime === "btc_led" || cmcEthBtcReferenceRegime === "thin";
|
|
801
|
+
const cmcExchangeLiquidityRegime = cmcExchangeLiquidity?.liquidityRegime ?? "unknown";
|
|
802
|
+
const cmcExchangeLiquidityStale = typeof cmcExchangeLiquidity?.stale === "boolean" ? cmcExchangeLiquidity.stale : null;
|
|
803
|
+
const cmcExchangeLiquidityVolumeChange24hPct = asFiniteNumberOrNull(
|
|
804
|
+
cmcExchangeLiquidity?.totalVolumeChange24hPct
|
|
805
|
+
);
|
|
806
|
+
const cmcExchangeLiquidityAligned = cmcExchangeLiquidityStale === true || cmcExchangeLiquidityRegime === "unknown" ? null : cmcExchangeLiquidityRegime === "expanding" || cmcExchangeLiquidityRegime === "balanced" || cmcExchangeLiquidityRegime === "binance_led";
|
|
807
|
+
const cmcFearGreedRegime = cmcFearGreed?.sentimentRegime ?? "unknown";
|
|
808
|
+
const cmcFearGreedStale = typeof cmcFearGreed?.stale === "boolean" ? cmcFearGreed.stale : null;
|
|
809
|
+
const cmcFearGreedValue = asFiniteNumberOrNull(cmcFearGreed?.value);
|
|
810
|
+
const cmcFearGreedValueChange24h = asFiniteNumberOrNull(
|
|
811
|
+
cmcFearGreed?.valueChange24h
|
|
812
|
+
);
|
|
813
|
+
const cmcFearGreedAligned = direction == null || cmcFearGreedStale === true || cmcFearGreedRegime === "unknown" || cmcFearGreedRegime === "neutral" || cmcFearGreedRegime === "euphoric" ? null : direction === "LONG" ? cmcFearGreedRegime === "risk_on" : cmcFearGreedRegime === "risk_off" || cmcFearGreedRegime === "capitulation";
|
|
814
|
+
const cmcIndexRegime = cmcIndexes?.indexRegime ?? "unknown";
|
|
815
|
+
const cmcIndexStale = typeof cmcIndexes?.stale === "boolean" ? cmcIndexes.stale : null;
|
|
816
|
+
const cmc20ToCmc100RatioChange24hPct = asFiniteNumberOrNull(
|
|
817
|
+
cmcIndexes?.cmc20ToCmc100RatioChange24hPct
|
|
818
|
+
);
|
|
819
|
+
const cmcIndexAligned = direction == null || cmcIndexStale === true || cmcIndexRegime === "unknown" || cmcIndexRegime === "balanced" ? null : direction === "LONG" ? cmcIndexRegime === "top20_led" || cmcIndexRegime === "large_cap_led" : cmcIndexRegime === "risk_off";
|
|
820
|
+
const targetVsBtcRatioReturn24h = asFiniteNumberOrNull(
|
|
821
|
+
targetVsBtc?.ratioReturn24h
|
|
822
|
+
);
|
|
823
|
+
const targetVsBtcAlpha24h = asFiniteNumberOrNull(targetVsBtc?.alphaVsBtc24h);
|
|
824
|
+
const targetVsBtcBeta20 = asFiniteNumberOrNull(targetVsBtc?.betaToBtc20);
|
|
825
|
+
const targetVsBtcCorrelation20 = asFiniteNumberOrNull(
|
|
826
|
+
targetVsBtc?.correlationToBtc20
|
|
827
|
+
);
|
|
828
|
+
const targetVsBtcRatioTrend = targetVsBtc?.ratioTrend ?? "unknown";
|
|
829
|
+
const targetVsBtcDirectionValue = targetVsBtcRatioReturn24h ?? targetVsBtcAlpha24h;
|
|
830
|
+
const targetVsBtcAligned = direction == null || targetVsBtcDirectionValue == null ? null : direction === "LONG" ? targetVsBtcDirectionValue >= 0 : targetVsBtcDirectionValue <= 0;
|
|
831
|
+
const targetVsEthRatioReturn24h = asFiniteNumberOrNull(
|
|
832
|
+
targetVsEth?.ratioReturn24h
|
|
833
|
+
);
|
|
834
|
+
const targetVsEthAlpha24h = asFiniteNumberOrNull(targetVsEth?.alphaVsEth24h);
|
|
835
|
+
const targetVsEthBeta20 = asFiniteNumberOrNull(targetVsEth?.betaToEth20);
|
|
836
|
+
const targetVsEthCorrelation20 = asFiniteNumberOrNull(
|
|
837
|
+
targetVsEth?.correlationToEth20
|
|
838
|
+
);
|
|
839
|
+
const targetVsEthRatioTrend = targetVsEth?.ratioTrend ?? "unknown";
|
|
840
|
+
const targetVsEthDirectionValue = targetVsEthRatioReturn24h ?? targetVsEthAlpha24h;
|
|
841
|
+
const targetVsEthAligned = direction == null || targetVsEthDirectionValue == null ? null : direction === "LONG" ? targetVsEthDirectionValue >= 0 : targetVsEthDirectionValue <= 0;
|
|
842
|
+
const btcAltRegimeValue = btcAltRegime?.regime ?? "unknown";
|
|
843
|
+
const btcAltRegimeStale = typeof btcAltRegime?.stale === "boolean" ? btcAltRegime.stale : null;
|
|
844
|
+
const btcAltRegimeAligned = direction == null || btcAltRegimeStale === true || btcAltRegimeValue === "unknown" || btcAltRegimeValue === "neutral" || btcAltRegimeValue === "mixed" ? null : direction === "LONG" ? btcAltRegimeValue === "alt_lead" || btcAltRegimeValue === "risk_on" : btcAltRegimeValue === "btc_lead" || btcAltRegimeValue === "risk_off";
|
|
845
|
+
const btcVsAltReturn24h = asFiniteNumberOrNull(
|
|
846
|
+
btcAltRegime?.btcVsAltReturn24h
|
|
847
|
+
);
|
|
848
|
+
const btcTurnoverShare24h = asFiniteNumberOrNull(
|
|
849
|
+
btcAltRegime?.btcTurnoverShare24h
|
|
850
|
+
);
|
|
851
|
+
const venueSpreadZScore = asFiniteNumberOrNull(execution?.venueSpreadZScore);
|
|
852
|
+
const venueSpreadSeverity = toVenueSpreadSeverity(venueSpreadZScore);
|
|
853
|
+
const higherTimeframeConflict = mtfAlignmentForDirection === "unknown" ? null : mtfAlignmentForDirection === "against" || mtfAlignmentForDirection === "mixed";
|
|
854
|
+
const extremeVolatilityRisk = Math.abs(atrPctZScore ?? 0) >= 2 || atrPctRankBucket === "extreme";
|
|
855
|
+
const compressionBreakoutSupport = (volatility?.state === "compressed" || bbWidthRankBucket === "low") && breakoutState !== "inside_range" && breakoutState !== "unknown";
|
|
856
|
+
const benchmarkConflict = benchmarkAligned === false || relativeStrengthBucket.endsWith("_against");
|
|
857
|
+
const derivativesSummary = baseContext.derivatives?.summary;
|
|
858
|
+
const derivativesDirectionAligned = typeof derivativesSummary?.directionAligned === "boolean" ? derivativesSummary.directionAligned : null;
|
|
859
|
+
const derivativesRiskFlags = Array.isArray(derivativesSummary?.riskFlags) ? derivativesSummary.riskFlags : [];
|
|
860
|
+
const derivativesCrowdedForDirection = direction === "LONG" ? derivativesRiskFlags.includes("crowded_long") : direction === "SHORT" ? derivativesRiskFlags.includes("crowded_short") : false;
|
|
861
|
+
const derivativesCrowdedAny = derivativesRiskFlags.includes("crowded_long") || derivativesRiskFlags.includes("crowded_short");
|
|
862
|
+
const atr = asFiniteNumberOrNull(baseContext.raw?.volatility?.atr);
|
|
863
|
+
const currentPrice = asFiniteNumberOrNull(prices?.currentPrice);
|
|
864
|
+
const takeProfitPrice = asFiniteNumberOrNull(prices?.takeProfitPrice);
|
|
865
|
+
const stopLossPrice = asFiniteNumberOrNull(prices?.stopLossPrice);
|
|
866
|
+
const stopDistanceAtr = direction == null || atr == null || atr <= 0 || currentPrice == null || stopLossPrice == null ? null : direction === "LONG" ? (currentPrice - stopLossPrice) / atr : (stopLossPrice - currentPrice) / atr;
|
|
867
|
+
const tpDistanceAtr = direction == null || atr == null || atr <= 0 || currentPrice == null || takeProfitPrice == null ? null : direction === "LONG" ? (takeProfitPrice - currentPrice) / atr : (currentPrice - takeProfitPrice) / atr;
|
|
868
|
+
const entryLocation = breakoutWithDirection === true ? direction === "SHORT" ? "breakdown" : "breakout" : rangePosition20 == null ? "unknown" : rangePosition20 <= 0.25 ? "near_support" : rangePosition20 >= 0.75 ? "near_resistance" : "mid_range";
|
|
869
|
+
const confirmations = [];
|
|
870
|
+
pushWhen(
|
|
871
|
+
confirmations,
|
|
872
|
+
mtfAlignmentForDirection === "aligned",
|
|
873
|
+
"mtf_aligned"
|
|
874
|
+
);
|
|
875
|
+
pushWhen(confirmations, (volumeRel20 ?? 0) >= 1.5, "volume_expansion");
|
|
876
|
+
pushWhen(confirmations, deltaAligned === true, "delta_aligned");
|
|
877
|
+
pushWhen(confirmations, tradeFlowAligned === true, "trade_flow_aligned");
|
|
878
|
+
pushWhen(
|
|
879
|
+
confirmations,
|
|
880
|
+
referenceTradeFlowAligned === true,
|
|
881
|
+
"reference_trade_flow_aligned"
|
|
882
|
+
);
|
|
883
|
+
pushWhen(
|
|
884
|
+
confirmations,
|
|
885
|
+
marketBreadthAligned === true,
|
|
886
|
+
"market_breadth_aligned"
|
|
887
|
+
);
|
|
888
|
+
pushWhen(
|
|
889
|
+
confirmations,
|
|
890
|
+
cmcAltLiquidityAligned === true,
|
|
891
|
+
"cmc_alt_liquidity_aligned"
|
|
892
|
+
);
|
|
893
|
+
pushWhen(confirmations, cmcEthBtcAligned === true, "cmc_eth_btc_aligned");
|
|
894
|
+
pushWhen(
|
|
895
|
+
confirmations,
|
|
896
|
+
cmcExchangeLiquidityAligned === true,
|
|
897
|
+
"cmc_exchange_liquidity_aligned"
|
|
898
|
+
);
|
|
899
|
+
pushWhen(
|
|
900
|
+
confirmations,
|
|
901
|
+
cmcFearGreedAligned === true,
|
|
902
|
+
"cmc_fear_greed_aligned"
|
|
903
|
+
);
|
|
904
|
+
pushWhen(confirmations, cmcIndexAligned === true, "cmc_index_aligned");
|
|
905
|
+
pushWhen(confirmations, targetVsBtcAligned === true, "target_vs_btc_aligned");
|
|
906
|
+
pushWhen(confirmations, targetVsEthAligned === true, "target_vs_eth_aligned");
|
|
907
|
+
pushWhen(
|
|
908
|
+
confirmations,
|
|
909
|
+
btcAltRegimeAligned === true,
|
|
910
|
+
"btc_alt_regime_aligned"
|
|
911
|
+
);
|
|
912
|
+
pushWhen(confirmations, benchmarkAligned === true, "benchmark_aligned");
|
|
913
|
+
pushWhen(confirmations, breakoutWithDirection === true, "breakout_confirmed");
|
|
914
|
+
pushWhen(
|
|
915
|
+
confirmations,
|
|
916
|
+
liquiditySweepForDirection === true,
|
|
917
|
+
"liquidity_sweep_aligned"
|
|
918
|
+
);
|
|
919
|
+
pushWhen(
|
|
920
|
+
confirmations,
|
|
921
|
+
derivativesDirectionAligned === true,
|
|
922
|
+
"derivatives_aligned"
|
|
923
|
+
);
|
|
924
|
+
const conflicts = [];
|
|
925
|
+
pushWhen(conflicts, mtfAlignmentForDirection === "against", "mtf_against");
|
|
926
|
+
pushWhen(conflicts, mtfAlignmentForDirection === "mixed", "mtf_mixed");
|
|
927
|
+
pushWhen(conflicts, benchmarkAligned === false, "benchmark_against");
|
|
928
|
+
pushWhen(
|
|
929
|
+
conflicts,
|
|
930
|
+
relativeStrengthBucket.endsWith("_against"),
|
|
931
|
+
"relative_strength_against"
|
|
932
|
+
);
|
|
933
|
+
pushWhen(conflicts, marketBreadthAligned === false, "market_breadth_against");
|
|
934
|
+
pushWhen(
|
|
935
|
+
conflicts,
|
|
936
|
+
cmcAltLiquidityAligned === false,
|
|
937
|
+
"cmc_alt_liquidity_against"
|
|
938
|
+
);
|
|
939
|
+
pushWhen(conflicts, cmcEthBtcAligned === false, "cmc_eth_btc_against");
|
|
940
|
+
pushWhen(
|
|
941
|
+
conflicts,
|
|
942
|
+
cmcExchangeLiquidityAligned === false,
|
|
943
|
+
"cmc_exchange_liquidity_against"
|
|
944
|
+
);
|
|
945
|
+
pushWhen(conflicts, cmcFearGreedAligned === false, "cmc_fear_greed_against");
|
|
946
|
+
pushWhen(conflicts, cmcIndexAligned === false, "cmc_index_against");
|
|
947
|
+
pushWhen(conflicts, targetVsBtcAligned === false, "target_vs_btc_against");
|
|
948
|
+
pushWhen(conflicts, targetVsEthAligned === false, "target_vs_eth_against");
|
|
949
|
+
pushWhen(conflicts, btcAltRegimeAligned === false, "btc_alt_regime_against");
|
|
950
|
+
pushWhen(conflicts, deltaAligned === false, "delta_against");
|
|
951
|
+
pushWhen(conflicts, tradeFlowAligned === false, "trade_flow_against");
|
|
952
|
+
pushWhen(
|
|
953
|
+
conflicts,
|
|
954
|
+
referenceTradeFlowAligned === false,
|
|
955
|
+
"reference_trade_flow_against"
|
|
956
|
+
);
|
|
957
|
+
pushWhen(conflicts, failedBreakoutForDirection === true, "failed_breakout");
|
|
958
|
+
pushWhen(conflicts, extremeVolatilityRisk, "extreme_volatility");
|
|
959
|
+
pushWhen(conflicts, venueSpreadSeverity === "wide", "wide_spread");
|
|
960
|
+
pushWhen(
|
|
961
|
+
conflicts,
|
|
962
|
+
derivativesDirectionAligned === false,
|
|
963
|
+
"derivatives_against"
|
|
964
|
+
);
|
|
965
|
+
pushWhen(conflicts, derivativesCrowdedForDirection, "derivatives_crowded");
|
|
966
|
+
const scores = {
|
|
967
|
+
structure: scoreEvidence([
|
|
968
|
+
breakoutWithDirection,
|
|
969
|
+
failedBreakoutForDirection == null ? null : failedBreakoutForDirection === false,
|
|
970
|
+
liquiditySweepForDirection,
|
|
971
|
+
nearPointOfControl
|
|
972
|
+
]),
|
|
973
|
+
participation: scoreEvidence([
|
|
974
|
+
volumeRel20 == null ? null : volumeRel20 >= 1.5,
|
|
975
|
+
deltaAligned,
|
|
976
|
+
tradeFlowAligned,
|
|
977
|
+
referenceTradeFlowAligned,
|
|
978
|
+
volumeStructureAligned
|
|
979
|
+
]),
|
|
980
|
+
relative: scoreEvidence([
|
|
981
|
+
benchmarkAligned,
|
|
982
|
+
marketBreadthAligned,
|
|
983
|
+
cmcAltLiquidityAligned,
|
|
984
|
+
cmcEthBtcAligned,
|
|
985
|
+
cmcFearGreedAligned,
|
|
986
|
+
cmcIndexAligned,
|
|
987
|
+
targetVsBtcAligned,
|
|
988
|
+
targetVsEthAligned,
|
|
989
|
+
btcAltRegimeAligned,
|
|
990
|
+
relativeStrengthBucket === "unknown" ? null : !relativeStrengthBucket.endsWith("_against")
|
|
991
|
+
]),
|
|
992
|
+
mtf: scoreEvidence([
|
|
993
|
+
mtfAlignmentForDirection === "unknown" ? null : mtfAlignmentForDirection === "aligned"
|
|
994
|
+
]),
|
|
995
|
+
execution: scoreEvidence([
|
|
996
|
+
venueSpreadSeverity === "unknown" ? null : venueSpreadSeverity !== "wide",
|
|
997
|
+
cmcExchangeLiquidityAligned
|
|
998
|
+
]),
|
|
999
|
+
derivatives: scoreEvidence([derivativesDirectionAligned]),
|
|
1000
|
+
totalContext: null
|
|
1001
|
+
};
|
|
1002
|
+
scores.totalContext = averageScores([
|
|
1003
|
+
scores.structure,
|
|
1004
|
+
scores.participation,
|
|
1005
|
+
scores.relative,
|
|
1006
|
+
scores.mtf,
|
|
1007
|
+
scores.execution,
|
|
1008
|
+
scores.derivatives
|
|
1009
|
+
]);
|
|
1010
|
+
const volatilityRisk = extremeVolatilityRisk ? "high" : atrPctRankBucket === "high" || bbWidthRankBucket === "high" ? "medium" : atrPctRankBucket === "unknown" && bbWidthRankBucket === "unknown" ? "unknown" : "low";
|
|
1011
|
+
const liquidityRisk = venueSpreadSeverity === "wide" || cmcExchangeLiquidityAligned === false ? "high" : venueSpreadSeverity === "elevated" ? "medium" : venueSpreadSeverity === "unknown" && cmcExchangeLiquidityAligned == null ? "unknown" : "low";
|
|
1012
|
+
const regimeRisk = higherTimeframeConflict === true ? "high" : mtfAlignmentForDirection === "neutral" || mtfAlignmentForDirection === "unknown" ? "medium" : "low";
|
|
1013
|
+
const crowdingRisk = derivativesCrowdedForDirection ? "high" : derivativesCrowdedAny ? "medium" : derivativesSummary ? "low" : "unknown";
|
|
1014
|
+
const chaseRisk = direction === "LONG" && rangePositionBucket === "high" && breakoutWithDirection !== true || direction === "SHORT" && rangePositionBucket === "low" && breakoutWithDirection !== true ? "high" : tpDistanceAtr != null && tpDistanceAtr < 1 || rangePositionBucket === "high" || rangePositionBucket === "low" ? "medium" : rangePositionBucket === "unknown" ? "unknown" : "low";
|
|
1015
|
+
const primaryIssue = derivativesCrowdedForDirection ? "crowded_derivatives" : higherTimeframeConflict === true ? "mtf_conflict" : venueSpreadSeverity === "wide" || cmcExchangeLiquidityAligned === false ? "bad_execution" : extremeVolatilityRisk ? "extreme_volatility" : benchmarkConflict || marketBreadthAligned === false || cmcAltLiquidityAligned === false || cmcEthBtcAligned === false || cmcFearGreedAligned === false || cmcIndexAligned === false || targetVsBtcAligned === false || targetVsEthAligned === false || btcAltRegimeAligned === false ? "market_context_against" : (scores.participation ?? 100) < 45 ? "weak_participation" : (scores.structure ?? 100) < 45 ? "weak_structure" : "none";
|
|
1016
|
+
const needsExtraConfirmation = conflicts.length > 0 || scores.totalContext != null && scores.totalContext < 60;
|
|
1017
|
+
const approveBias = conflicts.length >= 3 || primaryIssue === "crowded_derivatives" || primaryIssue === "bad_execution" || primaryIssue === "mtf_conflict" ? "reject" : confirmations.length >= 3 && conflicts.length === 0 ? "support" : "neutral";
|
|
1018
|
+
const maxReasonableQuality = approveBias === "reject" ? 2 : conflicts.length >= 2 || needsExtraConfirmation ? 3 : approveBias === "support" ? 5 : 4;
|
|
1019
|
+
return {
|
|
1020
|
+
direction,
|
|
1021
|
+
setup: {
|
|
1022
|
+
riskRatio: asFiniteNumberOrNull(prices?.riskRatio),
|
|
1023
|
+
rewardToVolatility: tpDistanceAtr,
|
|
1024
|
+
stopDistanceAtr,
|
|
1025
|
+
tpDistanceAtr,
|
|
1026
|
+
entryLocation
|
|
1027
|
+
},
|
|
1028
|
+
scores,
|
|
1029
|
+
confirmations: {
|
|
1030
|
+
count: confirmations.length,
|
|
1031
|
+
items: confirmations
|
|
1032
|
+
},
|
|
1033
|
+
conflicts: {
|
|
1034
|
+
count: conflicts.length,
|
|
1035
|
+
items: conflicts
|
|
1036
|
+
},
|
|
1037
|
+
risk: {
|
|
1038
|
+
regimeRisk,
|
|
1039
|
+
liquidityRisk,
|
|
1040
|
+
volatilityRisk,
|
|
1041
|
+
crowdingRisk,
|
|
1042
|
+
chaseRisk
|
|
1043
|
+
},
|
|
1044
|
+
decisionHints: {
|
|
1045
|
+
approveBias,
|
|
1046
|
+
maxReasonableQuality,
|
|
1047
|
+
needsExtraConfirmation,
|
|
1048
|
+
primaryIssue
|
|
1049
|
+
},
|
|
1050
|
+
mtf: {
|
|
1051
|
+
alignmentForDirection: mtfAlignmentForDirection,
|
|
1052
|
+
higherTimeframeConflict,
|
|
1053
|
+
h1TrendBias: mtfSummary?.h1TrendBias ?? "unknown",
|
|
1054
|
+
h4TrendBias: mtfSummary?.h4TrendBias ?? "unknown",
|
|
1055
|
+
d1TrendBias: mtfSummary?.d1TrendBias ?? "unknown",
|
|
1056
|
+
h1RangePosition: asFiniteNumberOrNull(mtfSummary?.h1RangePosition),
|
|
1057
|
+
h4VolatilityState: mtfSummary?.h4VolatilityState ?? "unknown"
|
|
1058
|
+
},
|
|
1059
|
+
volatility: {
|
|
1060
|
+
state: volatility?.state ?? "unknown",
|
|
1061
|
+
atrPctZScore,
|
|
1062
|
+
atrPctRankBucket,
|
|
1063
|
+
bbWidthRankBucket,
|
|
1064
|
+
extremeVolatilityRisk,
|
|
1065
|
+
compressionBreakoutSupport
|
|
1066
|
+
},
|
|
1067
|
+
structure: {
|
|
1068
|
+
breakoutState,
|
|
1069
|
+
rangePositionBucket,
|
|
1070
|
+
breakoutWithDirection,
|
|
1071
|
+
failedBreakoutForDirection,
|
|
1072
|
+
liquiditySweepForDirection,
|
|
1073
|
+
nearPointOfControl
|
|
1074
|
+
},
|
|
1075
|
+
participation: {
|
|
1076
|
+
volumeRel20,
|
|
1077
|
+
volumeBucket: toVolumeBucket(volumeRel20),
|
|
1078
|
+
deltaBias,
|
|
1079
|
+
deltaAligned,
|
|
1080
|
+
tradeFlowBuyPressurePct,
|
|
1081
|
+
tradeFlowAligned,
|
|
1082
|
+
referenceTradeFlowBuyPressurePct,
|
|
1083
|
+
referenceTradeFlowAligned,
|
|
1084
|
+
volumeStructureAligned
|
|
1085
|
+
},
|
|
1086
|
+
relative: {
|
|
1087
|
+
benchmarkTrendAlignment,
|
|
1088
|
+
benchmarkAligned,
|
|
1089
|
+
benchmarkConflict,
|
|
1090
|
+
relativeStrength1h,
|
|
1091
|
+
relativeStrengthBucket,
|
|
1092
|
+
marketBreadthReturn,
|
|
1093
|
+
marketBreadthAligned,
|
|
1094
|
+
marketBreadthStale: typeof marketBreadth?.stale === "boolean" ? marketBreadth.stale : null,
|
|
1095
|
+
cmcAltLiquidityRegime,
|
|
1096
|
+
cmcAltLiquidityAligned,
|
|
1097
|
+
cmcAltLiquidityStale,
|
|
1098
|
+
cmcEthBtcReferenceRegime,
|
|
1099
|
+
cmcEthBtcAligned,
|
|
1100
|
+
cmcEthBtcStale,
|
|
1101
|
+
cmcExchangeLiquidityRegime,
|
|
1102
|
+
cmcExchangeLiquidityAligned,
|
|
1103
|
+
cmcExchangeLiquidityStale,
|
|
1104
|
+
cmcExchangeLiquidityVolumeChange24hPct,
|
|
1105
|
+
cmcFearGreedValue,
|
|
1106
|
+
cmcFearGreedValueChange24h,
|
|
1107
|
+
cmcFearGreedRegime,
|
|
1108
|
+
cmcFearGreedAligned,
|
|
1109
|
+
cmcFearGreedStale,
|
|
1110
|
+
cmcIndexRegime,
|
|
1111
|
+
cmcIndexAligned,
|
|
1112
|
+
cmcIndexStale,
|
|
1113
|
+
cmc20ToCmc100RatioChange24hPct,
|
|
1114
|
+
targetVsBtcRatioReturn24h,
|
|
1115
|
+
targetVsBtcAlpha24h,
|
|
1116
|
+
targetVsBtcBeta20,
|
|
1117
|
+
targetVsBtcCorrelation20,
|
|
1118
|
+
targetVsBtcRatioTrend,
|
|
1119
|
+
targetVsBtcAligned,
|
|
1120
|
+
targetVsEthRatioReturn24h,
|
|
1121
|
+
targetVsEthAlpha24h,
|
|
1122
|
+
targetVsEthBeta20,
|
|
1123
|
+
targetVsEthCorrelation20,
|
|
1124
|
+
targetVsEthRatioTrend,
|
|
1125
|
+
targetVsEthAligned,
|
|
1126
|
+
btcAltRegime: btcAltRegimeValue,
|
|
1127
|
+
btcAltRegimeAligned,
|
|
1128
|
+
btcAltRegimeStale,
|
|
1129
|
+
btcVsAltReturn24h,
|
|
1130
|
+
btcTurnoverShare24h
|
|
1131
|
+
},
|
|
1132
|
+
execution: {
|
|
1133
|
+
venueSpreadZScore,
|
|
1134
|
+
venueSpreadSeverity
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
1138
|
+
var cloneBaseContextData = (baseContext, direction, prices) => {
|
|
1139
|
+
const clone = cloneSignalPayloadDataProperties(
|
|
1140
|
+
baseContext
|
|
1141
|
+
);
|
|
1142
|
+
const compactMtf = cloneCompactMtfContext(baseContext);
|
|
1143
|
+
if (compactMtf) {
|
|
1144
|
+
clone.mtf = compactMtf;
|
|
1145
|
+
}
|
|
1146
|
+
clone.gateFeatures = buildBaseContextGateFeatures({
|
|
1147
|
+
baseContext: clone,
|
|
1148
|
+
direction,
|
|
1149
|
+
prices
|
|
1150
|
+
});
|
|
1151
|
+
return clone;
|
|
1152
|
+
};
|
|
1153
|
+
var normalizeAdditionalIndicatorsBaseContext = (additionalIndicators, direction, prices) => {
|
|
1154
|
+
if (!additionalIndicators || typeof additionalIndicators !== "object" || Array.isArray(additionalIndicators)) {
|
|
1155
|
+
return additionalIndicators;
|
|
1156
|
+
}
|
|
1157
|
+
const baseContext = additionalIndicators.baseContext;
|
|
1158
|
+
if (baseContext == null) {
|
|
1159
|
+
return additionalIndicators;
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
...additionalIndicators,
|
|
1163
|
+
baseContext: cloneBaseContextData(baseContext, direction, prices)
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
var refreshSignalBaseContextGateFeatures = (signal) => {
|
|
1167
|
+
const baseContext = signal.additionalIndicators?.baseContext;
|
|
1168
|
+
if (!baseContext || typeof baseContext !== "object" || Array.isArray(baseContext)) {
|
|
1169
|
+
return signal;
|
|
1170
|
+
}
|
|
1171
|
+
signal.additionalIndicators = {
|
|
1172
|
+
...signal.additionalIndicators ?? {},
|
|
1173
|
+
baseContext: {
|
|
1174
|
+
...baseContext,
|
|
1175
|
+
gateFeatures: buildBaseContextGateFeatures({
|
|
1176
|
+
baseContext,
|
|
1177
|
+
direction: signal.direction,
|
|
1178
|
+
prices: signal.prices
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
return signal;
|
|
1183
|
+
};
|
|
207
1184
|
var mapAiRuntimeFromConfig = (config, overrides = {}) => ({
|
|
208
1185
|
enabled: Boolean(config.AI_ENABLED ?? true),
|
|
209
1186
|
mode: config.AI_MODE ?? "llm",
|
|
@@ -228,19 +1205,36 @@ var buildStrategySignal = ({
|
|
|
228
1205
|
indicators = {},
|
|
229
1206
|
additionalIndicators,
|
|
230
1207
|
isConfigFromBacktest
|
|
231
|
-
}) =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1208
|
+
}) => {
|
|
1209
|
+
const indicatorsRecord = indicators && typeof indicators === "object" ? indicators : {};
|
|
1210
|
+
const baseContext = indicatorsRecord.baseContext;
|
|
1211
|
+
const normalizedIndicators = baseContext == null ? indicators : copyEnumerableDataPropertiesExcept(
|
|
1212
|
+
indicatorsRecord,
|
|
1213
|
+
/* @__PURE__ */ new Set(["baseContext"])
|
|
1214
|
+
);
|
|
1215
|
+
const mergedAdditionalIndicators = baseContext == null ? additionalIndicators : {
|
|
1216
|
+
...additionalIndicators ?? {},
|
|
1217
|
+
baseContext: additionalIndicators?.baseContext ?? baseContext
|
|
1218
|
+
};
|
|
1219
|
+
const normalizedAdditionalIndicators = normalizeAdditionalIndicatorsBaseContext(
|
|
1220
|
+
mergedAdditionalIndicators,
|
|
1221
|
+
direction,
|
|
1222
|
+
prices
|
|
1223
|
+
);
|
|
1224
|
+
return {
|
|
1225
|
+
signalId,
|
|
1226
|
+
strategy,
|
|
1227
|
+
symbol,
|
|
1228
|
+
interval,
|
|
1229
|
+
direction,
|
|
1230
|
+
timestamp,
|
|
1231
|
+
figures,
|
|
1232
|
+
prices,
|
|
1233
|
+
indicators: normalizedIndicators,
|
|
1234
|
+
additionalIndicators: normalizedAdditionalIndicators,
|
|
1235
|
+
isConfigFromBacktest
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
244
1238
|
var buildEntrySignalDecision = ({
|
|
245
1239
|
code,
|
|
246
1240
|
entryContext,
|
|
@@ -295,33 +1289,98 @@ var createStrategyAPI = ({
|
|
|
295
1289
|
connector,
|
|
296
1290
|
cachedData,
|
|
297
1291
|
indicatorsState,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1292
|
+
isConfigFromBacktest,
|
|
1293
|
+
sharedReplayKey,
|
|
1294
|
+
getSharedReplayState
|
|
301
1295
|
}) => {
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
position && typeof position.qty === "number" && position.qty > 0
|
|
307
|
-
);
|
|
1296
|
+
const isBacktestEnv = env === "BACKTEST";
|
|
1297
|
+
const barCache = {
|
|
1298
|
+
timestamp: null,
|
|
1299
|
+
currentPosition: void 0
|
|
308
1300
|
};
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1301
|
+
let currentIndicatorsContextCache;
|
|
1302
|
+
const getCurrentBarTimestamp = () => {
|
|
1303
|
+
const lastCandle = cachedData[cachedData.length - 1];
|
|
1304
|
+
return typeof lastCandle?.timestamp === "number" ? lastCandle.timestamp : null;
|
|
1305
|
+
};
|
|
1306
|
+
const getCurrentIndicatorsCacheKey = () => {
|
|
1307
|
+
const lastCandle = cachedData[cachedData.length - 1];
|
|
1308
|
+
if (!lastCandle) {
|
|
1309
|
+
return "empty";
|
|
313
1310
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1311
|
+
return [
|
|
1312
|
+
cachedData.length,
|
|
1313
|
+
lastCandle.timestamp,
|
|
1314
|
+
lastCandle.open,
|
|
1315
|
+
lastCandle.high,
|
|
1316
|
+
lastCandle.low,
|
|
1317
|
+
lastCandle.close,
|
|
1318
|
+
lastCandle.volume,
|
|
1319
|
+
lastCandle.turnover
|
|
1320
|
+
].join(":");
|
|
1321
|
+
};
|
|
1322
|
+
const ensureBarCache = () => {
|
|
1323
|
+
if (!isBacktestEnv) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const currentBarTimestamp = getCurrentBarTimestamp();
|
|
1327
|
+
if (barCache.timestamp === currentBarTimestamp) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
barCache.timestamp = currentBarTimestamp;
|
|
1331
|
+
barCache.currentPosition = void 0;
|
|
1332
|
+
currentIndicatorsContextCache = void 0;
|
|
1333
|
+
};
|
|
1334
|
+
const getCurrentPosition = () => {
|
|
1335
|
+
if (!isBacktestEnv) {
|
|
1336
|
+
return connector.getPosition(symbol);
|
|
1337
|
+
}
|
|
1338
|
+
ensureBarCache();
|
|
1339
|
+
if (!barCache.currentPosition) {
|
|
1340
|
+
barCache.currentPosition = connector.getPosition(symbol);
|
|
1341
|
+
}
|
|
1342
|
+
return barCache.currentPosition;
|
|
1343
|
+
};
|
|
1344
|
+
const createStateController = createStrategyStateControllerFactory({
|
|
1345
|
+
env,
|
|
1346
|
+
sharedReplayKey,
|
|
1347
|
+
getSharedReplayState
|
|
1348
|
+
});
|
|
1349
|
+
const getBaseContextFromIndicators = (indicators) => isRecordLike(indicators) && isRecordLike(indicators.baseContext) ? indicators.baseContext : void 0;
|
|
1350
|
+
const getCurrentIndicatorsContext = () => {
|
|
1351
|
+
ensureBarCache();
|
|
1352
|
+
const cacheKey = getCurrentIndicatorsCacheKey();
|
|
1353
|
+
if (currentIndicatorsContextCache?.key === cacheKey) {
|
|
1354
|
+
return currentIndicatorsContextCache.context;
|
|
1355
|
+
}
|
|
1356
|
+
indicatorsState?.onBar();
|
|
1357
|
+
const indicators = indicatorsState?.snapshot();
|
|
1358
|
+
const baseContext = getBaseContextFromIndicators(indicators);
|
|
1359
|
+
const context = {
|
|
1360
|
+
indicators,
|
|
1361
|
+
...baseContext ? { baseContext } : {}
|
|
1362
|
+
};
|
|
1363
|
+
currentIndicatorsContextCache = {
|
|
1364
|
+
key: cacheKey,
|
|
1365
|
+
context
|
|
1366
|
+
};
|
|
1367
|
+
return context;
|
|
1368
|
+
};
|
|
1369
|
+
const getBaseContext = () => getCurrentIndicatorsContext().baseContext;
|
|
1370
|
+
const resolveDecisionPriceContext = () => {
|
|
1371
|
+
const candle = cachedData[cachedData.length - 1];
|
|
1372
|
+
if (!candle || !isFiniteNumber(candle.timestamp) || !isFiniteNumber(candle.close)) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
"strategyApi.getDecisionPriceContext requires a current closed candle"
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
timestamp: candle.timestamp,
|
|
1379
|
+
currentPrice: candle.close,
|
|
1380
|
+
candle
|
|
1381
|
+
};
|
|
324
1382
|
};
|
|
1383
|
+
const getDecisionPriceContext = async () => resolveDecisionPriceContext();
|
|
325
1384
|
return {
|
|
326
1385
|
skip: (code) => ({ kind: "skip", code }),
|
|
327
1386
|
entry: async ({
|
|
@@ -334,9 +1393,9 @@ var createStrategyAPI = ({
|
|
|
334
1393
|
orderPlan,
|
|
335
1394
|
runtime
|
|
336
1395
|
}) => {
|
|
337
|
-
const
|
|
338
|
-
const currentPrice =
|
|
339
|
-
const timestamp =
|
|
1396
|
+
const priceContext = resolveDecisionPriceContext();
|
|
1397
|
+
const currentPrice = priceContext.currentPrice;
|
|
1398
|
+
const timestamp = priceContext.timestamp;
|
|
340
1399
|
const stopLossPrice = orderPlan.stopLossPrice;
|
|
341
1400
|
const takeProfitPrice = resolveTakeProfitPrice({
|
|
342
1401
|
direction,
|
|
@@ -378,19 +1437,14 @@ var createStrategyAPI = ({
|
|
|
378
1437
|
runtime
|
|
379
1438
|
});
|
|
380
1439
|
},
|
|
381
|
-
exit: async ({
|
|
382
|
-
|
|
383
|
-
direction,
|
|
384
|
-
price,
|
|
385
|
-
timestamp
|
|
386
|
-
}) => {
|
|
387
|
-
const marketData = await getMarketData();
|
|
1440
|
+
exit: async ({ code, direction }) => {
|
|
1441
|
+
const priceContext = resolveDecisionPriceContext();
|
|
388
1442
|
return {
|
|
389
1443
|
kind: "exit",
|
|
390
1444
|
code: code ?? toDefaultExitCode(String(strategy), direction),
|
|
391
1445
|
closePlan: {
|
|
392
|
-
price:
|
|
393
|
-
timestamp:
|
|
1446
|
+
price: priceContext.currentPrice,
|
|
1447
|
+
timestamp: priceContext.timestamp,
|
|
394
1448
|
direction
|
|
395
1449
|
}
|
|
396
1450
|
};
|
|
@@ -400,18 +1454,86 @@ var createStrategyAPI = ({
|
|
|
400
1454
|
code: code ?? toDefaultProtectCode(String(strategy), protectPlan.direction),
|
|
401
1455
|
protectPlan
|
|
402
1456
|
}),
|
|
403
|
-
|
|
404
|
-
|
|
1457
|
+
getCurrentIndicatorsContext,
|
|
1458
|
+
getBaseContext,
|
|
1459
|
+
getDecisionPriceContext,
|
|
405
1460
|
getCurrentPosition,
|
|
406
|
-
isCurrentPositionExists: isPositionExists,
|
|
407
1461
|
getDirectionalTpSlPrices: (params) => getDirectionalTpSlPrices(params),
|
|
408
1462
|
createLastTradeController: (params) => createLastTradeController({
|
|
409
1463
|
env,
|
|
410
1464
|
...params
|
|
411
|
-
})
|
|
1465
|
+
}),
|
|
1466
|
+
createStateController
|
|
412
1467
|
};
|
|
413
1468
|
};
|
|
1469
|
+
|
|
1470
|
+
// src/utils/strategyHelpers/sharedReplay.ts
|
|
1471
|
+
var sharedStrategyReplayStates = /* @__PURE__ */ new Map();
|
|
1472
|
+
var processLike = globalThis.process;
|
|
1473
|
+
var SHARED_REPLAY_PROFILE = processLike?.env?.TRADEJS_STRATEGY_REPLAY_PROFILE === "1";
|
|
1474
|
+
var sharedReplayProfile = {
|
|
1475
|
+
requests: 0,
|
|
1476
|
+
hits: 0,
|
|
1477
|
+
creates: 0,
|
|
1478
|
+
uncached: 0,
|
|
1479
|
+
releases: 0,
|
|
1480
|
+
releasedEntries: 0
|
|
1481
|
+
};
|
|
1482
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1483
|
+
processLike?.once?.("exit", () => {
|
|
1484
|
+
console.log(
|
|
1485
|
+
JSON.stringify(
|
|
1486
|
+
{
|
|
1487
|
+
strategyReplayProfile: {
|
|
1488
|
+
...sharedReplayProfile,
|
|
1489
|
+
retainedEntries: sharedStrategyReplayStates.size
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
null,
|
|
1493
|
+
2
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
var getSharedStrategyReplayState = (key, createState) => {
|
|
1499
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1500
|
+
sharedReplayProfile.requests += 1;
|
|
1501
|
+
}
|
|
1502
|
+
if (!key) {
|
|
1503
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1504
|
+
sharedReplayProfile.uncached += 1;
|
|
1505
|
+
}
|
|
1506
|
+
return createState();
|
|
1507
|
+
}
|
|
1508
|
+
const existing = sharedStrategyReplayStates.get(key);
|
|
1509
|
+
if (existing) {
|
|
1510
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1511
|
+
sharedReplayProfile.hits += 1;
|
|
1512
|
+
}
|
|
1513
|
+
return existing;
|
|
1514
|
+
}
|
|
1515
|
+
const state = createState();
|
|
1516
|
+
sharedStrategyReplayStates.set(key, state);
|
|
1517
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1518
|
+
sharedReplayProfile.creates += 1;
|
|
1519
|
+
}
|
|
1520
|
+
return state;
|
|
1521
|
+
};
|
|
1522
|
+
var releaseStrategyReplayCache = (keyPrefix) => {
|
|
1523
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1524
|
+
sharedReplayProfile.releases += 1;
|
|
1525
|
+
}
|
|
1526
|
+
for (const key of sharedStrategyReplayStates.keys()) {
|
|
1527
|
+
if (key === keyPrefix || key.startsWith(`${keyPrefix}:`)) {
|
|
1528
|
+
sharedStrategyReplayStates.delete(key);
|
|
1529
|
+
if (SHARED_REPLAY_PROFILE) {
|
|
1530
|
+
sharedReplayProfile.releasedEntries += 1;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
414
1535
|
export {
|
|
1536
|
+
buildBaseContextGateFeatures,
|
|
415
1537
|
buildDefaultIndicatorPeriods,
|
|
416
1538
|
buildEntrySignalDecision,
|
|
417
1539
|
buildStrategySignal,
|
|
@@ -420,7 +1542,12 @@ export {
|
|
|
420
1542
|
createStrategyAPI,
|
|
421
1543
|
createStrategyIndicatorsState,
|
|
422
1544
|
getDirectionalTpSlPrices,
|
|
1545
|
+
getSharedStrategyReplayState,
|
|
423
1546
|
getStrategyMarketSnapshot,
|
|
424
1547
|
mapAiRuntimeFromConfig,
|
|
425
|
-
mapMlRuntimeFromConfig
|
|
1548
|
+
mapMlRuntimeFromConfig,
|
|
1549
|
+
refreshSignalBaseContextGateFeatures,
|
|
1550
|
+
releaseStrategyIndicatorsReplayCache,
|
|
1551
|
+
releaseStrategyReplayCache,
|
|
1552
|
+
resolveBacktestExecutionPrice
|
|
426
1553
|
};
|