@tradejs/node 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/ai.js CHANGED
@@ -37,6 +37,7 @@ __export(ai_exports, {
37
37
  buildAiPayload: () => buildAiPayload,
38
38
  buildAiPrompts: () => buildAiPrompts,
39
39
  buildAiSystemPrompt: () => buildAiSystemPrompt,
40
+ buildCompactAiIndicatorsSnapshot: () => buildCompactAiIndicatorsSnapshot,
40
41
  ensureAiStrategyPluginsLoaded: () => ensureAiStrategyPluginsLoaded,
41
42
  getDeterministicAiGateContext: () => getDeterministicAiGateContext,
42
43
  getOpenRouterModelKwargs: () => getOpenRouterModelKwargs,
@@ -52,6 +53,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
52
53
 
53
54
  // src/aiShared.ts
54
55
  var MAX_AI_SERIES_POINTS = 5;
56
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
57
+ "tradejs.indicators.compactSnapshot"
58
+ );
59
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
55
60
  var trimSeriesDeep = (value) => {
56
61
  if (Array.isArray(value)) {
57
62
  const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
@@ -73,13 +78,15 @@ var trimSeriesDeep = (value) => {
73
78
  }
74
79
  return value;
75
80
  };
81
+ var buildCompactAiIndicatorsSnapshot = (value) => {
82
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
83
+ if (typeof compactSnapshot === "function") {
84
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
85
+ }
86
+ return trimSeriesDeep(value);
87
+ };
76
88
 
77
89
  // src/aiMarketContext.ts
78
- var SESSION_WINDOWS = [
79
- { name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
80
- { name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
81
- { name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
82
- ];
83
90
  var toRecord = (value) => {
84
91
  if (!value || typeof value !== "object" || Array.isArray(value)) {
85
92
  return null;
@@ -90,92 +97,485 @@ var toFiniteNumber = (value) => {
90
97
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
91
98
  return Number.isFinite(numeric) ? numeric : null;
92
99
  };
93
- var getLastFiniteNumber = (value) => {
94
- const numeric = toFiniteNumber(value);
95
- if (numeric != null) {
96
- return numeric;
97
- }
98
- if (!Array.isArray(value)) {
99
- return null;
100
- }
101
- for (let i = value.length - 1; i >= 0; i -= 1) {
102
- const nested = getLastFiniteNumber(value[i]);
103
- if (nested != null) {
104
- return nested;
105
- }
106
- }
107
- return null;
108
- };
109
100
  var roundTo = (value, decimals) => {
110
101
  const factor = 10 ** decimals;
111
102
  return Math.round(value * factor) / factor;
112
103
  };
113
- var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
114
- var buildTradingSessionContext = (timestamp) => {
115
- const date = new Date(timestamp);
116
- const utcHour = date.getUTCHours();
117
- const utcMinute = date.getUTCMinutes();
118
- const minuteUtc = utcHour * 60 + utcMinute;
119
- const activeSessions = SESSION_WINDOWS.filter(
120
- (session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
121
- ).map((session) => session.name);
122
- const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
123
- return {
124
- timezone: "UTC",
125
- utcHour,
126
- utcMinute,
127
- primarySession,
128
- activeSessions,
129
- isOverlap: activeSessions.length > 1,
130
- overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
131
- };
132
- };
133
104
  var buildMissingSpreadContext = () => ({
134
- source: "binance_coinbase_btc",
135
- indicatorKey: "payload.indicators.spread",
105
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
106
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
136
107
  available: false,
137
108
  value: null,
109
+ zScore: null,
138
110
  bps: null,
139
111
  absBps: null,
140
112
  bias: null,
141
113
  severity: null
142
114
  });
143
- var buildSpreadContextFromValue = (spread) => {
115
+ var buildSpreadContextFromSignal = (signal) => {
116
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
117
+ const relative = toRecord(baseContext?.relative);
118
+ const execution = toRecord(relative?.execution);
119
+ const spread = toFiniteNumber(execution?.venueSpread);
120
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
121
+ if (spread == null) {
122
+ return buildMissingSpreadContext();
123
+ }
144
124
  const value = roundTo(spread, 8);
145
125
  const bps = roundTo(value * 1e4, 2);
146
126
  const absBps = Math.abs(bps);
147
- const bias = Math.abs(bps) < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
127
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
148
128
  const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
149
129
  return {
150
- source: "binance_coinbase_btc",
151
- indicatorKey: "payload.indicators.spread",
130
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
131
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
152
132
  available: true,
153
133
  value,
134
+ zScore,
154
135
  bps,
155
136
  absBps,
156
137
  bias,
157
138
  severity
158
139
  };
159
140
  };
160
- var readSpreadFromSignal = (signal) => {
161
- const indicatorSpread = getLastFiniteNumber(signal.indicators?.spread);
162
- if (indicatorSpread != null) {
163
- return indicatorSpread;
141
+ var buildTrueDeltaContextFromSignal = (signal) => {
142
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
143
+ const participation = toRecord(baseContext?.participation);
144
+ const delta = toRecord(participation?.delta);
145
+ const source = String(delta?.source ?? "");
146
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
147
+ if (!delta || !isTrueDeltaSource) {
148
+ return {
149
+ source: source || null,
150
+ available: false,
151
+ buyPressurePct: null,
152
+ buyVolume: null,
153
+ sellVolume: null,
154
+ netDelta: null,
155
+ deltaPct: null,
156
+ signedVolumeZScore: null
157
+ };
158
+ }
159
+ return {
160
+ source,
161
+ available: true,
162
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
163
+ buyVolume: toFiniteNumber(delta.buyVolume),
164
+ sellVolume: toFiniteNumber(delta.sellVolume),
165
+ netDelta: toFiniteNumber(delta.netDelta),
166
+ deltaPct: toFiniteNumber(delta.deltaPct),
167
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
168
+ };
169
+ };
170
+ var buildTradeFlowContextFromSignal = (signal) => {
171
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
172
+ const participation = toRecord(baseContext?.participation);
173
+ const tradeFlow = toRecord(participation?.tradeFlow);
174
+ if (!tradeFlow) {
175
+ return {
176
+ source: null,
177
+ available: false,
178
+ interval: null,
179
+ stale: null,
180
+ trades: null,
181
+ buyPressurePct: null,
182
+ netBaseDelta: null,
183
+ netQuoteDelta: null
184
+ };
164
185
  }
165
- return getLastFiniteNumber(signal.additionalIndicators?.spread);
186
+ return {
187
+ source: String(tradeFlow.source ?? ""),
188
+ available: true,
189
+ interval: String(tradeFlow.interval ?? ""),
190
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
191
+ trades: toFiniteNumber(tradeFlow.trades),
192
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
193
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
194
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
195
+ };
166
196
  };
167
- var buildAiMarketContext = (signal) => {
168
- const existingMarketContext = toRecord(
169
- signal.additionalIndicators?.marketContext
170
- );
171
- const existingSpread = toRecord(existingMarketContext?.binanceCoinbaseSpread);
172
- const spread = readSpreadFromSignal(signal);
197
+ var buildMarketBreadthContextFromSignal = (signal) => {
198
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
199
+ const relative = toRecord(baseContext?.relative);
200
+ const breadth = toRecord(relative?.marketBreadth);
201
+ if (!breadth) {
202
+ return {
203
+ source: null,
204
+ available: false,
205
+ universe: null,
206
+ interval: null,
207
+ stale: null,
208
+ symbolsCount: null,
209
+ advanceDeclineRatio: null,
210
+ pctAboveMa20: null,
211
+ pctAboveMa50: null,
212
+ equalWeightedReturn: null,
213
+ volumeWeightedReturn: null,
214
+ dispersion: null
215
+ };
216
+ }
217
+ return {
218
+ source: String(breadth.source ?? ""),
219
+ available: true,
220
+ universe: String(breadth.universe ?? ""),
221
+ interval: String(breadth.interval ?? ""),
222
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
223
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
224
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
225
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
226
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
227
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
228
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
229
+ dispersion: toFiniteNumber(breadth.dispersion)
230
+ };
231
+ };
232
+ var buildTargetVsBtcContextFromSignal = (signal) => {
233
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
234
+ const relative = toRecord(baseContext?.relative);
235
+ const targetVsBtc = toRecord(relative?.targetVsBtc);
236
+ if (!targetVsBtc) {
237
+ return {
238
+ source: null,
239
+ available: false,
240
+ ratioReturn1h: null,
241
+ ratioReturn4h: null,
242
+ ratioReturn24h: null,
243
+ alphaVsBtc1h: null,
244
+ alphaVsBtc4h: null,
245
+ alphaVsBtc24h: null,
246
+ betaToBtc20: null,
247
+ correlationToBtc20: null,
248
+ ratioTrend: null
249
+ };
250
+ }
251
+ return {
252
+ source: String(targetVsBtc.source ?? ""),
253
+ available: true,
254
+ ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
255
+ ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
256
+ ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
257
+ alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
258
+ alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
259
+ alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
260
+ betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
261
+ correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
262
+ ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
263
+ };
264
+ };
265
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
266
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
267
+ const relative = toRecord(baseContext?.relative);
268
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
269
+ if (!btcAltRegime) {
270
+ return {
271
+ source: null,
272
+ available: false,
273
+ universe: null,
274
+ interval: null,
275
+ stale: null,
276
+ regime: null,
277
+ btcReturn24h: null,
278
+ altBasketReturn24h: null,
279
+ btcVsAltReturn24h: null,
280
+ btcTurnoverShare24h: null,
281
+ btcTurnoverShareChange24h: null,
282
+ altVolToBtcVol24h: null,
283
+ altDispersion24h: null
284
+ };
285
+ }
286
+ return {
287
+ source: String(btcAltRegime.source ?? ""),
288
+ available: true,
289
+ universe: String(btcAltRegime.universe ?? ""),
290
+ interval: String(btcAltRegime.interval ?? ""),
291
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
292
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
293
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
294
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
295
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
296
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
297
+ btcTurnoverShareChange24h: toFiniteNumber(
298
+ btcAltRegime.btcTurnoverShareChange24h
299
+ ),
300
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
301
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
302
+ };
303
+ };
304
+ var buildCmcGlobalContextFromSignal = (signal) => {
305
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
306
+ const relative = toRecord(baseContext?.relative);
307
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
308
+ if (!cmcGlobal) {
309
+ return {
310
+ source: null,
311
+ available: false,
312
+ interval: null,
313
+ asOfTs: null,
314
+ stale: null,
315
+ totalMarketCapUsd: null,
316
+ totalVolumeUsd: null,
317
+ totalVolumeReportedUsd: null,
318
+ altMarketCapUsd: null,
319
+ altVolumeUsd: null,
320
+ altVolumeReportedUsd: null,
321
+ btcDominancePct: null,
322
+ ethDominancePct: null,
323
+ btcDominanceChange24hPct: null,
324
+ ethDominanceChange24hPct: null,
325
+ altMarketCapChange24hPct: null,
326
+ altVolumeChange24hPct: null,
327
+ activeCryptocurrencies: null,
328
+ activeExchanges: null,
329
+ activeMarketPairs: null,
330
+ altLiquidityRegime: null
331
+ };
332
+ }
333
+ return {
334
+ source: String(cmcGlobal.source ?? ""),
335
+ available: true,
336
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
337
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
338
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
339
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
340
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
341
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
342
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
343
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
344
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
345
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
346
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
347
+ btcDominanceChange24hPct: toFiniteNumber(
348
+ cmcGlobal.btcDominanceChange24hPct
349
+ ),
350
+ ethDominanceChange24hPct: toFiniteNumber(
351
+ cmcGlobal.ethDominanceChange24hPct
352
+ ),
353
+ altMarketCapChange24hPct: toFiniteNumber(
354
+ cmcGlobal.altMarketCapChange24hPct
355
+ ),
356
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
357
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
358
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
359
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
360
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
361
+ };
362
+ };
363
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
364
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
365
+ const relative = toRecord(baseContext?.relative);
366
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
367
+ if (!cmcReferenceAssets) {
368
+ return {
369
+ source: null,
370
+ available: false,
371
+ interval: null,
372
+ asOfTs: null,
373
+ stale: null,
374
+ btcMarketCapUsd: null,
375
+ ethMarketCapUsd: null,
376
+ btcVolumeUsd: null,
377
+ ethVolumeUsd: null,
378
+ btcVolumeToMarketCap: null,
379
+ ethVolumeToMarketCap: null,
380
+ ethBtcMarketCapRatio: null,
381
+ ethBtcMarketCapRatioChange24hPct: null,
382
+ ethVsBtcVolumeRatio: null,
383
+ referenceLiquidityRegime: null
384
+ };
385
+ }
173
386
  return {
174
- ...existingMarketContext ?? {},
175
- tradingSession: buildTradingSessionContext(signal.timestamp),
176
- binanceCoinbaseSpread: spread != null ? buildSpreadContextFromValue(spread) : existingSpread ?? buildMissingSpreadContext()
387
+ source: String(cmcReferenceAssets.source ?? ""),
388
+ available: true,
389
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
390
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
391
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
392
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
393
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
394
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
395
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
396
+ btcVolumeToMarketCap: toFiniteNumber(
397
+ cmcReferenceAssets.btcVolumeToMarketCap
398
+ ),
399
+ ethVolumeToMarketCap: toFiniteNumber(
400
+ cmcReferenceAssets.ethVolumeToMarketCap
401
+ ),
402
+ ethBtcMarketCapRatio: toFiniteNumber(
403
+ cmcReferenceAssets.ethBtcMarketCapRatio
404
+ ),
405
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
406
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
407
+ ),
408
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
409
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
177
410
  };
178
411
  };
412
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
413
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
414
+ const relative = toRecord(baseContext?.relative);
415
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
416
+ if (!cmcExchangeLiquidity) {
417
+ return {
418
+ source: null,
419
+ available: false,
420
+ interval: null,
421
+ asOfTs: null,
422
+ stale: null,
423
+ exchangesCount: null,
424
+ totalVolumeUsd: null,
425
+ totalVolumeChange24hPct: null,
426
+ binanceVolumeUsd: null,
427
+ binanceVolumeShare: null,
428
+ topExchangeVolumeShare: null,
429
+ liquidityRegime: null
430
+ };
431
+ }
432
+ return {
433
+ source: String(cmcExchangeLiquidity.source ?? ""),
434
+ available: true,
435
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
436
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
437
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
438
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
439
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
440
+ totalVolumeChange24hPct: toFiniteNumber(
441
+ cmcExchangeLiquidity.totalVolumeChange24hPct
442
+ ),
443
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
444
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
445
+ topExchangeVolumeShare: toFiniteNumber(
446
+ cmcExchangeLiquidity.topExchangeVolumeShare
447
+ ),
448
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
449
+ };
450
+ };
451
+ var buildCmcFearGreedContextFromSignal = (signal) => {
452
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
453
+ const relative = toRecord(baseContext?.relative);
454
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
455
+ if (!cmcFearGreed) {
456
+ return {
457
+ source: null,
458
+ available: false,
459
+ interval: null,
460
+ asOfTs: null,
461
+ stale: null,
462
+ value: null,
463
+ valueChange24h: null,
464
+ valueChange7d: null,
465
+ classification: null,
466
+ sentimentRegime: null
467
+ };
468
+ }
469
+ return {
470
+ source: String(cmcFearGreed.source ?? ""),
471
+ available: true,
472
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
473
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
474
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
475
+ value: toFiniteNumber(cmcFearGreed.value),
476
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
477
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
478
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
479
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
480
+ };
481
+ };
482
+ var buildCmcIndexesContextFromSignal = (signal) => {
483
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
484
+ const relative = toRecord(baseContext?.relative);
485
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
486
+ if (!cmcIndexes) {
487
+ return {
488
+ source: null,
489
+ available: false,
490
+ interval: null,
491
+ asOfTs: null,
492
+ stale: null,
493
+ cmc100Value: null,
494
+ cmc100Change24hPct: null,
495
+ cmc100TopConstituentSymbol: null,
496
+ cmc100TopConstituentWeightPct: null,
497
+ cmc20Value: null,
498
+ cmc20Change24hPct: null,
499
+ cmc20TopConstituentSymbol: null,
500
+ cmc20TopConstituentWeightPct: null,
501
+ cmc20ToCmc100Ratio: null,
502
+ cmc20ToCmc100RatioChange24hPct: null,
503
+ indexRegime: null
504
+ };
505
+ }
506
+ return {
507
+ source: String(cmcIndexes.source ?? ""),
508
+ available: true,
509
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
510
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
511
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
512
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
513
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
514
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
515
+ cmc100TopConstituentWeightPct: toFiniteNumber(
516
+ cmcIndexes.cmc100TopConstituentWeightPct
517
+ ),
518
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
519
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
520
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
521
+ cmc20TopConstituentWeightPct: toFiniteNumber(
522
+ cmcIndexes.cmc20TopConstituentWeightPct
523
+ ),
524
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
525
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
526
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
527
+ ),
528
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
529
+ };
530
+ };
531
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
532
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
533
+ const relative = toRecord(baseContext?.relative);
534
+ const refs = toRecord(relative?.referenceTradeFlow);
535
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
536
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
537
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
538
+ if (!refs) {
539
+ return {
540
+ source: null,
541
+ available: false,
542
+ primaryReferenceSymbol: null,
543
+ referenceSymbols: [],
544
+ primaryTradeFlowBuyPressurePct: null,
545
+ primaryTradeFlowStale: null
546
+ };
547
+ }
548
+ return {
549
+ source: String(refs.source ?? ""),
550
+ available: true,
551
+ primaryReferenceSymbol,
552
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
553
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
554
+ primaryTradeFlow?.buyPressurePct
555
+ ),
556
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
557
+ };
558
+ };
559
+ var buildAiMarketContext = (signal) => ({
560
+ execution: {
561
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
562
+ },
563
+ participation: {
564
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
565
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
566
+ },
567
+ relative: {
568
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
569
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
570
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
571
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
572
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
573
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
574
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
575
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
576
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
577
+ }
578
+ });
179
579
 
180
580
  // src/strategy/manifests.ts
181
581
  var import_indicators = require("@tradejs/core/indicators");
@@ -184,7 +584,6 @@ var import_logger2 = require("@tradejs/infra/logger");
184
584
  // src/tradejsConfig.ts
185
585
  var import_fs = __toESM(require("fs"));
186
586
  var import_path = __toESM(require("path"));
187
- var import_module = require("module");
188
587
  var import_url = require("url");
189
588
  var import_config = require("@tradejs/core/config");
190
589
  var import_logger = require("@tradejs/infra/logger");
@@ -200,6 +599,7 @@ var cachedByCwd = /* @__PURE__ */ new Map();
200
599
  var announcedConfigFile = /* @__PURE__ */ new Set();
201
600
  var tsNodeRegistered = false;
202
601
  var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
602
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
203
603
  var getTradejsProjectCwd = (cwd) => {
204
604
  const explicit = String(cwd ?? "").trim();
205
605
  if (explicit) {
@@ -229,7 +629,14 @@ var normalizeConfig = (rawConfig) => {
229
629
  ...hooks ? { hooks } : {}
230
630
  };
231
631
  };
232
- var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
632
+ var getNodeCreateRequire = () => {
633
+ const builtinModule = process.getBuiltinModule?.("module");
634
+ if (typeof builtinModule?.createRequire === "function") {
635
+ return builtinModule.createRequire;
636
+ }
637
+ throw new TypeError("module.createRequire is not available");
638
+ };
639
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
233
640
  var ensureTsNodeRegistered = async () => {
234
641
  if (tsNodeRegistered) {
235
642
  return;
@@ -239,8 +646,8 @@ var ensureTsNodeRegistered = async () => {
239
646
  tsNode.register?.({
240
647
  transpileOnly: true,
241
648
  compilerOptions: {
242
- module: "commonjs",
243
- moduleResolution: "node"
649
+ module: "Node16",
650
+ moduleResolution: "node16"
244
651
  }
245
652
  });
246
653
  tsNodeRegistered = true;
@@ -267,6 +674,42 @@ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
267
674
  });
268
675
  tsconfigPathsRegisteredByCwd.add(projectRoot);
269
676
  };
677
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
678
+ const projectRoot = getTradejsProjectCwd(cwd);
679
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
680
+ if (cachedMatcher) {
681
+ const resolved2 = cachedMatcher(moduleName);
682
+ return resolved2 || null;
683
+ }
684
+ const tsconfigPathsModule = await import("tsconfig-paths");
685
+ const loadConfig = tsconfigPathsModule.loadConfig;
686
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
687
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
688
+ return null;
689
+ }
690
+ const loadedConfig = loadConfig(projectRoot);
691
+ if (loadedConfig.resultType !== "success") {
692
+ return null;
693
+ }
694
+ const matchPath = createMatchPath(
695
+ loadedConfig.absoluteBaseUrl,
696
+ loadedConfig.paths
697
+ );
698
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
699
+ ".ts",
700
+ ".tsx",
701
+ ".mts",
702
+ ".cts",
703
+ ".js",
704
+ ".jsx",
705
+ ".mjs",
706
+ ".cjs",
707
+ ".json"
708
+ ]) || "";
709
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
710
+ const resolved = matcher(moduleName);
711
+ return resolved || null;
712
+ };
270
713
  var toImportSpecifier = (moduleName) => {
271
714
  if (moduleName.startsWith("file://")) {
272
715
  return moduleName;
@@ -325,7 +768,18 @@ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
325
768
  }
326
769
  if (isBareModuleSpecifier(normalized)) {
327
770
  await ensureTsconfigPathsRegistered(cwd);
328
- return requireFn(normalized);
771
+ try {
772
+ return requireFn(normalized);
773
+ } catch (error) {
774
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
775
+ normalized,
776
+ cwd
777
+ );
778
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
779
+ return requireFn(resolvedByTsconfig);
780
+ }
781
+ throw error;
782
+ }
329
783
  }
330
784
  try {
331
785
  return await import(
@@ -595,6 +1049,9 @@ var strategies = new Proxy(
595
1049
  }
596
1050
  );
597
1051
 
1052
+ // src/strategy/policyProfiles.ts
1053
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
1054
+
598
1055
  // src/strategyAdapters/ai.ts
599
1056
  var toRecord2 = (value) => {
600
1057
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -622,13 +1079,13 @@ var buildBaseAiPayload = (signal) => {
622
1079
  }
623
1080
  },
624
1081
  figures: trimSeriesDeep(signal.figures ?? {}),
625
- indicators: trimSeriesDeep(signal.indicators),
1082
+ indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
626
1083
  additionalIndicators: trimSeriesDeep(additionalIndicators)
627
1084
  };
628
1085
  };
629
1086
  var defaultAiAdapter = {};
630
- var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)?.aiAdapter ?? defaultAiAdapter;
631
- var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
1087
+ var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
1088
+ var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
632
1089
  var buildAiPayloadByStrategy = (signal) => {
633
1090
  const basePayload = buildBaseAiPayload(signal);
634
1091
  const adapter = getSignalAiAdapter(signal);
@@ -715,7 +1172,7 @@ var getDeterministicQuality = (gateContext) => {
715
1172
  if (Number.isFinite(maxAllowedQuality)) {
716
1173
  return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
717
1174
  }
718
- return Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
1175
+ return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
719
1176
  };
720
1177
  var buildAiSystemPrompt = (signal) => `
721
1178
  You are an internal market-structure classifier for an already computed system signal.
@@ -779,24 +1236,44 @@ Input payload structure:
779
1236
  - payload.figures:
780
1237
  strategy-specific figures or geometry when available. Fields vary by strategy.
781
1238
  - payload.indicators:
782
- indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values.
1239
+ historical indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values. Treat this block as recent-history transport, not as the primary source of the current shared context.
783
1240
  - payload.additionalIndicators:
784
- strategy-specific summary/context fields. This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
785
- Examples: helperFlags, structureContext, spread, correlation, volatilitySummary.
1241
+ strategy-specific summary/context fields plus the canonical current shared context snapshot.
1242
+ This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
1243
+ Examples: baseContext, helperFlags, structureContext, volatilitySummary.
1244
+ Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
1245
+ \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
1246
+ \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
1247
+ \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
1248
+ \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, and Binance aggTrades trade-flow when available.
1249
+ \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
1250
+ \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
1251
+ \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
1252
+ \u2022 \`baseContext.gateFeatures\`: direction-aware, normalized fields derived from baseContext; prefer \`setup\`, \`scores\`, \`confirmations\`, \`conflicts\`, \`risk\`, and \`decisionHints\` for quick gate checks before inspecting raw nested context.
786
1253
  Always inspect \`payload.additionalIndicators.marketContext\` when present:
787
- \u2022 \`marketContext.tradingSession\`: UTC session at signal time: asia / europe / us / overlap / off_hours.
788
- \u2022 \`marketContext.binanceCoinbaseSpread\`: BTC spread between Coinbase and Binance from \`payload.indicators.spread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
1254
+ \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
1255
+ \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
1256
+ \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
1257
+ \u2022 \`marketContext.relative.marketBreadth\`: equal/volume-weighted alt-basket return, advance/decline ratio, and MA breadth for the configured Binance breadth universe.
1258
+ \u2022 \`marketContext.relative.targetVsBtc\`: target/BTC ratio returns, alpha, beta, and short-window correlation; use it to decide whether the target is leading or lagging BTC in the signal direction.
1259
+ \u2022 \`marketContext.relative.btcAltRegime\`: Binance-derived BTC-vs-alt basket regime, BTC/alt 24h returns, BTC turnover share, and alt dispersion; use it as a broad alt-market risk pocket.
1260
+ \u2022 \`marketContext.relative.cmcGlobal\`: historical CoinMarketCap global market metrics: total/alt market cap, total/alt volume, BTC/ETH dominance and 24h changes, active markets, \`interval\`, and \`altLiquidityRegime\`.
1261
+ \u2022 \`marketContext.relative.cmcReferenceAssets\`: historical CoinMarketCap BTC/ETH market-cap and volume context, ETH/BTC market-cap ratio, ETH-vs-BTC volume ratio, \`interval\`, and \`referenceLiquidityRegime\`.
1262
+ \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
1263
+ \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
1264
+ \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
1265
+ \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
789
1266
  If those fields exist, use them as a more explicit hint instead of trying to re-derive the same idea from raw lines or points.
790
- If \`derivativesContext\` exists, it is a derived Coinalyze summary for the time of the signal. Coinalyze context is built only from \`BTCUSDT\` and \`ETHUSDT\` reference symbols, not for every target coin. \`targetSymbol\` is just the source signal coin. Use BTC/ETH open interest, funding, liquidations, and pressure/riskFlags as positioning context, not as an independent trade idea.
1267
+ If \`baseContext.derivatives\` exists, its top-level \`summary\` and \`intervals\` are the primary BTCUSDT Coinalyze benchmark context for the time of the signal. \`secondaryReferenceSymbol\` identifies the ETHUSDT secondary benchmark, and \`referenceContexts\` contains BTCUSDT/ETHUSDT plus configured extra reference symbols such as BNBUSDT/SOLUSDT/TRXUSDT/XRPUSDT. If \`targetContext\` or \`targetDerived\` exists, those fields are the Coinalyze context for the actual target coin; use them as target-specific positioning evidence, but do not infer target-coin derivatives when they are absent.
791
1268
  Key patterns:
792
- \u2022 coin: \`maFast\`, \`atrPct\`, \`macd...\`, \`candles15m/candles1h/candles4h/candles1d\`, and \`*1h/*4h/*1d\`
793
- \u2022 BTC: \`btcMaFast\`, \`btcAtr\`, \`btcMacd...\`, \`btcCandles*\`, and \`btc*1h/*4h/*1d\`
794
- \u2022 strategy service keys are possible as well, for example \`correlation\`, \`spread\`, \`touches\`, \`distance\`
1269
+ \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
1270
+ \u2022 recent historical series: \`payload.indicators\`
1271
+ \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
795
1272
 
796
1273
  How to analyze, in order:
797
1274
  1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
798
- 2. Then use \`payload.additionalIndicators\` when it contains explicit strategy-specific context such as line state, spread, correlation, and similar fields.
799
- 3. Then assess confirmation or conflict from the current coin indicators.
1275
+ 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
1276
+ 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
800
1277
  4. Then evaluate BTC context.
801
1278
  5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
802
1279
  6. If strong conflicts exist, reduce quality or set direction to \`null\`.
@@ -805,13 +1282,24 @@ Explicit conflict rules:
805
1282
  - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
806
1283
  - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
807
1284
  - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
808
- - If \`derivativesContext.referenceContexts\` exists, check \`primaryReferenceSymbol\` first, then compare \`BTCUSDT\` and \`ETHUSDT\` as broad-market derivatives context. Do not search for Coinalyze data for \`targetSymbol\` unless \`targetSymbol\` itself is \`BTCUSDT\` or \`ETHUSDT\`.
809
- - If \`derivativesContext.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as crowded positioning and do not overstate quality without strong structural confirmation.
810
- - If \`derivativesContext.summary.directionAligned=false\`, explicitly mention the derivatives conflict in \`confirmations\` or \`qualityReason\`.
811
- - If \`derivativesContext\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
812
- - If \`marketContext.tradingSession\` exists, treat the session as a liquidity and volatility regime: asia is often thinner, europe/us are more active, and overlaps can amplify both momentum and noise. Do not reject a signal solely because of session, but mention clear session support or conflict in \`confirmations\` or \`qualityReason\`.
813
- - If \`marketContext.binanceCoinbaseSpread.available=true\` and \`severity=elevated/wide\`, treat it as cross-exchange divergence or BTC liquidity risk. Do not use the spread as a standalone long/short signal, but reduce confidence or require more confirmation when the rest of the structure is weak or BTC context conflicts.
814
- - If \`marketContext.binanceCoinbaseSpread\` is missing or \`available=false\`, do not infer anything from Binance/Coinbase spread and do not penalize the signal just because it is absent.
1285
+ - If \`baseContext.derivatives.referenceContexts\` exists, check \`primaryReferenceSymbol\` first as the BTC benchmark, then compare \`secondaryReferenceSymbol\`/ETHUSDT and any target-specific \`targetDerived\`. If \`targetDerived\` exists, compare it to the primary reference instead of treating reference pressure as the target coin's own pressure.
1286
+ - If top-level \`baseContext.derivatives.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as broad-market crowded positioning. If \`targetDerived.riskFlags\` contains the same directional crowding, treat that as target-specific crowded positioning.
1287
+ - If top-level \`baseContext.derivatives.summary.directionAligned=false\`, explicitly mention the broad-market derivatives conflict in \`confirmations\` or \`qualityReason\`. If \`targetDerived.directionAligned=false\`, explicitly mention the target-specific derivatives conflict.
1288
+ - If \`baseContext.derivatives\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
1289
+ - Use \`baseContext.regime.session\` directly as the canonical session/liquidity regime: asia is often thinner, europe/us are more active, and overlaps can amplify both momentum and noise. Do not reject a signal solely because of session, but mention clear session support or conflict in \`confirmations\` or \`qualityReason\`.
1290
+ - If \`marketContext.execution.binanceCoinbaseSpread.available=true\` and \`severity=elevated/wide\`, treat it as cross-exchange divergence or BTC liquidity risk. Do not use the spread as a standalone long/short signal, but reduce confidence or require more confirmation when the rest of the structure is weak or BTC context conflicts.
1291
+ - If \`marketContext.execution.binanceCoinbaseSpread\` is missing or \`available=false\`, do not infer anything from Binance/Coinbase spread and do not penalize the signal just because it is absent.
1292
+ - If \`marketContext.participation.trueDelta.available=true\`, use it as better participation evidence than OHLCV-derived proxy delta; still do not let delta override invalid price structure.
1293
+ - If \`marketContext.participation.tradeFlow.available=true\` and \`stale=false\`, use it as direct lower-timeframe participation evidence. Treat stale or missing tradeFlow as absent, not as negative evidence.
1294
+ - If \`marketContext.relative.marketBreadth.available=true\` and \`stale=false\`, use it as broad alt-market support/conflict. Breadth is contextual; do not let it override the target symbol structure.
1295
+ - If \`marketContext.relative.targetVsBtc.available=true\`, treat positive target/BTC ratio trend as support for alt LONGs and negative ratio trend as support for alt SHORTs; ignore it when the target structure is stronger and clearly explains the setup.
1296
+ - If \`marketContext.relative.btcAltRegime.available=true\` and \`stale=false\`, treat \`alt_lead\`/\`risk_on\` as broad support for alt LONGs and \`btc_lead\`/\`risk_off\` as pressure against alt LONGs or support for cautious alt SHORTs. Do not use it as a standalone entry reason.
1297
+ - If \`marketContext.relative.cmcGlobal.available=true\` and \`stale=false\`, use falling alt market cap/volume or rising BTC dominance as broad risk pressure for alt LONGs. Treat missing CMC history as absent context, not a bearish signal.
1298
+ - If \`marketContext.relative.cmcReferenceAssets.available=true\` and \`stale=false\`, use \`eth_led\` as broad support for ETH/high-beta alt strength and \`btc_led\`/\`thin\` as broad caution. Do not describe BTC/ETH reference history as target-symbol flow.
1299
+ - If \`marketContext.relative.cmcExchangeLiquidity.available=true\` and \`stale=false\`, treat \`contracting\`, \`thin\`, or \`concentrated\` as broad liquidity risk; \`expanding\` or \`balanced\` supports cleaner execution context but is not a standalone entry reason.
1300
+ - If \`marketContext.relative.cmcFearGreed.available=true\` and \`stale=false\`, use \`risk_on\` as broad support for LONGs and \`risk_off\`/\`capitulation\` as broad pressure. Treat \`euphoric\` as overheating/chase caution, not as standalone SHORT proof.
1301
+ - If \`marketContext.relative.cmcIndexes.available=true\` and \`stale=false\`, use \`top20_led\` as broad support for mega-cap leadership, \`large_cap_led\` as broader CMC100 participation, and \`risk_off\` as broad pressure. Do not use CMC index history as a standalone entry reason.
1302
+ - If \`marketContext.relative.referenceTradeFlow.available=true\`, treat BTC/ETH trade-flow as broad market context only. For alt symbols, do not describe it as the target coin's own flow.
815
1303
  - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
816
1304
  If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
817
1305
 
@@ -861,7 +1349,7 @@ var getDeterministicAiGateContext = (payload) => {
861
1349
  ...Object.values(additionalIndicators ?? {}).map(asRecord)
862
1350
  ].filter((value) => Boolean(value));
863
1351
  return candidates.find(
864
- (candidate) => Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
1352
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
865
1353
  ) ?? null;
866
1354
  };
867
1355
  var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
@@ -872,6 +1360,18 @@ Trade payload:
872
1360
  ${JSON.stringify(payload)}
873
1361
  ${buildAiHumanPromptAddonByStrategy(signal, payload)}
874
1362
  `;
1363
+ var getAiInvocationError = (error) => {
1364
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
1365
+ const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
1366
+ details
1367
+ );
1368
+ const wrapped = new Error(
1369
+ isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
1370
+ );
1371
+ wrapped.cause = error;
1372
+ return wrapped;
1373
+ };
1374
+ var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
875
1375
  var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
876
1376
  var userSettingsCache = /* @__PURE__ */ new Map();
877
1377
  var aiModelCache = /* @__PURE__ */ new Map();
@@ -1003,10 +1503,17 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
1003
1503
  ]
1004
1504
  })
1005
1505
  );
1006
- const response = await model.invoke(messages);
1007
- const parsed = parseAIResponse(
1008
- normalizeResponseContent(response.content)
1009
- );
1506
+ let response;
1507
+ try {
1508
+ response = await model.invoke(messages);
1509
+ } catch (error) {
1510
+ throw getAiInvocationError(error);
1511
+ }
1512
+ const responseContent = normalizeResponseContent(response?.content);
1513
+ if (isEmptyResponseContent(responseContent)) {
1514
+ throw new Error("AI provider returned an empty chat completion");
1515
+ }
1516
+ const parsed = parseAIResponse(responseContent);
1010
1517
  const normalized = normalizeAnalysis(parsed);
1011
1518
  if (!options.signal) {
1012
1519
  return normalized;
@@ -1064,6 +1571,7 @@ var askAI = async (signal, options = {}) => {
1064
1571
  buildAiPayload,
1065
1572
  buildAiPrompts,
1066
1573
  buildAiSystemPrompt,
1574
+ buildCompactAiIndicatorsSnapshot,
1067
1575
  ensureAiStrategyPluginsLoaded,
1068
1576
  getDeterministicAiGateContext,
1069
1577
  getOpenRouterModelKwargs,