@tradejs/core 1.0.8 → 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.
Files changed (41) hide show
  1. package/dist/backtest.d.mts +109 -9
  2. package/dist/backtest.d.ts +109 -9
  3. package/dist/backtest.js +487 -145
  4. package/dist/backtest.mjs +414 -82
  5. package/dist/chunk-7P2KNFD4.mjs +11847 -0
  6. package/dist/{chunk-JLORHLL6.mjs → chunk-DXJ4NCFJ.mjs} +65 -6
  7. package/dist/chunk-OJPHC3S2.mjs +8 -0
  8. package/dist/{chunk-PQETJ42A.mjs → chunk-PNBS6J3G.mjs} +22 -1
  9. package/dist/constants.d.mts +26 -5
  10. package/dist/constants.d.ts +26 -5
  11. package/dist/constants.js +81 -9
  12. package/dist/constants.mjs +31 -5
  13. package/dist/data.mjs +3 -5
  14. package/dist/grid.d.mts +9 -0
  15. package/dist/grid.d.ts +9 -0
  16. package/dist/grid.js +168 -0
  17. package/dist/grid.mjs +98 -0
  18. package/dist/indicators-Da_i06-8.d.mts +288 -0
  19. package/dist/indicators-Da_i06-8.d.ts +288 -0
  20. package/dist/indicators.d.mts +4 -39
  21. package/dist/indicators.d.ts +4 -39
  22. package/dist/indicators.js +10488 -423
  23. package/dist/indicators.mjs +11 -3
  24. package/dist/strategies.d.mts +31 -12
  25. package/dist/strategies.d.ts +31 -12
  26. package/dist/strategies.js +11283 -336
  27. package/dist/strategies.mjs +1246 -119
  28. package/dist/{time-BMkFD4Kd.d.mts → time-BQ3AXmxo.d.mts} +3 -1
  29. package/dist/{time-BMkFD4Kd.d.ts → time-BQ3AXmxo.d.ts} +3 -1
  30. package/dist/time.d.mts +1 -1
  31. package/dist/time.d.ts +1 -1
  32. package/dist/time.js +38 -0
  33. package/dist/time.mjs +6 -2
  34. package/dist/trade.d.mts +54 -0
  35. package/dist/trade.d.ts +54 -0
  36. package/dist/trade.js +352 -0
  37. package/dist/trade.mjs +264 -0
  38. package/package.json +19 -5
  39. package/dist/chunk-622V7IAT.mjs +0 -1810
  40. package/dist/indicators-B-GGjP5F.d.mts +0 -65
  41. package/dist/indicators-B-GGjP5F.d.ts +0 -65
@@ -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-622V7IAT.mjs";
7
- import "./chunk-AYC2QVKI.mjs";
8
10
  import {
9
11
  getTimestamp
10
- } from "./chunk-PQETJ42A.mjs";
12
+ } from "./chunk-PNBS6J3G.mjs";
11
13
  import {
12
14
  FEE_PERCENT
13
- } from "./chunk-JLORHLL6.mjs";
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 controller = env === "BACKTEST" ? createIndicators(data, btcData, {
50
- periods,
51
- btcBinanceData,
52
- btcCoinbaseData,
53
- pluginRegistryScope
54
- }) : null;
55
- let currentBarPair;
56
- const withSnapshot = (value) => Object.assign(value, {
57
- snapshot: () => value.result()
58
- });
59
- const applyBar = (candle, btcCandle) => {
60
- if (!controller) return;
61
- controller.next(candle, btcCandle);
62
- };
63
- const ensureControllerInitialized = () => {
64
- if (controller) return withSnapshot(controller);
65
- controller = createIndicators(data.slice(0, -1), btcData.slice(0, -1), {
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
- const lastCandle = data[data.length - 1];
72
- const lastBtcCandle = btcData[btcData.length - 1];
73
- if (lastCandle && lastBtcCandle) {
74
- controller.next(lastCandle, lastBtcCandle);
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
- return withSnapshot(controller);
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 = { candle, btcCandle };
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
- if (!resolvedCandle || !resolvedBtcCandle) return;
87
- applyBar(resolvedCandle, resolvedBtcCandle);
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
- if (!controller) return void 0;
91
- return controller.next(candle, btcCandle);
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
- let currentPrice = lastCandle.close;
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
- signalId,
233
- strategy,
234
- symbol,
235
- interval,
236
- direction,
237
- timestamp,
238
- figures,
239
- prices,
240
- indicators,
241
- additionalIndicators,
242
- isConfigFromBacktest
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
- preloadStart,
299
- backtestPriceMode,
300
- isConfigFromBacktest
1292
+ isConfigFromBacktest,
1293
+ sharedReplayKey,
1294
+ getSharedReplayState
301
1295
  }) => {
302
- const getCurrentPosition = () => connector.getPosition(symbol);
303
- const isPositionExists = async () => {
304
- const position = await getCurrentPosition();
305
- return Boolean(
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
- const getMarketData = async (params = {}) => {
310
- const resolvedPreloadStart = params.preloadStart ?? preloadStart;
311
- if (typeof resolvedPreloadStart !== "number") {
312
- throw new Error("strategyApi.getMarketData requires preloadStart");
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
- const snapshot = await getStrategyMarketSnapshot({
315
- env,
316
- connector,
317
- symbol,
318
- interval,
319
- cachedData,
320
- preloadStart: resolvedPreloadStart,
321
- backtestPriceMode: params.backtestPriceMode ?? backtestPriceMode
322
- });
323
- return snapshot;
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 marketData = await getMarketData();
338
- const currentPrice = marketData.currentPrice;
339
- const timestamp = marketData.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
- code,
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: price ?? marketData.currentPrice,
393
- timestamp: timestamp ?? marketData.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
- getMarketData,
404
- nextIndicators: (candle, btcCandle) => indicatorsState?.next(candle, btcCandle),
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
  };