@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.
@@ -0,0 +1,1136 @@
1
+ import {
2
+ ensureStrategyPluginsLoaded,
3
+ getStrategyManifest
4
+ } from "./chunk-QVSMINLG.mjs";
5
+
6
+ // src/ai.ts
7
+ import {
8
+ DEFAULT_AI_RESPONSE_LANGUAGE,
9
+ getAiResponseLanguagePromptName
10
+ } from "@tradejs/infra/aiLanguages";
11
+ import { setData, redisKeys } from "@tradejs/infra/redis";
12
+ import {
13
+ getUserSettings
14
+ } from "@tradejs/infra/userSettings";
15
+
16
+ // src/aiShared.ts
17
+ var MAX_AI_SERIES_POINTS = 5;
18
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
19
+ "tradejs.indicators.compactSnapshot"
20
+ );
21
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
22
+ var trimSeriesDeep = (value) => {
23
+ if (Array.isArray(value)) {
24
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
25
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
26
+ if (isMatrix) {
27
+ return trimmed;
28
+ }
29
+ return trimmed.map(
30
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
31
+ );
32
+ }
33
+ if (value && typeof value === "object") {
34
+ return Object.fromEntries(
35
+ Object.entries(value).map(([key, nested]) => [
36
+ key,
37
+ trimSeriesDeep(nested)
38
+ ])
39
+ );
40
+ }
41
+ return value;
42
+ };
43
+ var buildCompactAiIndicatorsSnapshot = (value) => {
44
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
45
+ if (typeof compactSnapshot === "function") {
46
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
47
+ }
48
+ return trimSeriesDeep(value);
49
+ };
50
+
51
+ // src/aiMarketContext.ts
52
+ var toRecord = (value) => {
53
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
54
+ return null;
55
+ }
56
+ return value;
57
+ };
58
+ var toFiniteNumber = (value) => {
59
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
60
+ return Number.isFinite(numeric) ? numeric : null;
61
+ };
62
+ var roundTo = (value, decimals) => {
63
+ const factor = 10 ** decimals;
64
+ return Math.round(value * factor) / factor;
65
+ };
66
+ var buildMissingSpreadContext = () => ({
67
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
68
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
69
+ available: false,
70
+ value: null,
71
+ zScore: null,
72
+ bps: null,
73
+ absBps: null,
74
+ bias: null,
75
+ severity: null
76
+ });
77
+ var buildSpreadContextFromSignal = (signal) => {
78
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
79
+ const relative = toRecord(baseContext?.relative);
80
+ const execution = toRecord(relative?.execution);
81
+ const spread = toFiniteNumber(execution?.venueSpread);
82
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
83
+ if (spread == null) {
84
+ return buildMissingSpreadContext();
85
+ }
86
+ const value = roundTo(spread, 8);
87
+ const bps = roundTo(value * 1e4, 2);
88
+ const absBps = Math.abs(bps);
89
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
90
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
91
+ return {
92
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
93
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
94
+ available: true,
95
+ value,
96
+ zScore,
97
+ bps,
98
+ absBps,
99
+ bias,
100
+ severity
101
+ };
102
+ };
103
+ var buildTrueDeltaContextFromSignal = (signal) => {
104
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
105
+ const participation = toRecord(baseContext?.participation);
106
+ const delta = toRecord(participation?.delta);
107
+ const source = String(delta?.source ?? "");
108
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
109
+ if (!delta || !isTrueDeltaSource) {
110
+ return {
111
+ source: source || null,
112
+ available: false,
113
+ buyPressurePct: null,
114
+ buyVolume: null,
115
+ sellVolume: null,
116
+ netDelta: null,
117
+ deltaPct: null,
118
+ signedVolumeZScore: null
119
+ };
120
+ }
121
+ return {
122
+ source,
123
+ available: true,
124
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
125
+ buyVolume: toFiniteNumber(delta.buyVolume),
126
+ sellVolume: toFiniteNumber(delta.sellVolume),
127
+ netDelta: toFiniteNumber(delta.netDelta),
128
+ deltaPct: toFiniteNumber(delta.deltaPct),
129
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
130
+ };
131
+ };
132
+ var buildTradeFlowContextFromSignal = (signal) => {
133
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
134
+ const participation = toRecord(baseContext?.participation);
135
+ const tradeFlow = toRecord(participation?.tradeFlow);
136
+ if (!tradeFlow) {
137
+ return {
138
+ source: null,
139
+ available: false,
140
+ interval: null,
141
+ stale: null,
142
+ trades: null,
143
+ buyPressurePct: null,
144
+ netBaseDelta: null,
145
+ netQuoteDelta: null
146
+ };
147
+ }
148
+ return {
149
+ source: String(tradeFlow.source ?? ""),
150
+ available: true,
151
+ interval: String(tradeFlow.interval ?? ""),
152
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
153
+ trades: toFiniteNumber(tradeFlow.trades),
154
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
155
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
156
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
157
+ };
158
+ };
159
+ var buildMarketBreadthContextFromSignal = (signal) => {
160
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
161
+ const relative = toRecord(baseContext?.relative);
162
+ const breadth = toRecord(relative?.marketBreadth);
163
+ if (!breadth) {
164
+ return {
165
+ source: null,
166
+ available: false,
167
+ universe: null,
168
+ interval: null,
169
+ stale: null,
170
+ symbolsCount: null,
171
+ advanceDeclineRatio: null,
172
+ pctAboveMa20: null,
173
+ pctAboveMa50: null,
174
+ equalWeightedReturn: null,
175
+ volumeWeightedReturn: null,
176
+ dispersion: null
177
+ };
178
+ }
179
+ return {
180
+ source: String(breadth.source ?? ""),
181
+ available: true,
182
+ universe: String(breadth.universe ?? ""),
183
+ interval: String(breadth.interval ?? ""),
184
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
185
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
186
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
187
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
188
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
189
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
190
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
191
+ dispersion: toFiniteNumber(breadth.dispersion)
192
+ };
193
+ };
194
+ var buildTargetVsBtcContextFromSignal = (signal) => {
195
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
196
+ const relative = toRecord(baseContext?.relative);
197
+ const targetVsBtc = toRecord(relative?.targetVsBtc);
198
+ if (!targetVsBtc) {
199
+ return {
200
+ source: null,
201
+ available: false,
202
+ ratioReturn1h: null,
203
+ ratioReturn4h: null,
204
+ ratioReturn24h: null,
205
+ alphaVsBtc1h: null,
206
+ alphaVsBtc4h: null,
207
+ alphaVsBtc24h: null,
208
+ betaToBtc20: null,
209
+ correlationToBtc20: null,
210
+ ratioTrend: null
211
+ };
212
+ }
213
+ return {
214
+ source: String(targetVsBtc.source ?? ""),
215
+ available: true,
216
+ ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
217
+ ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
218
+ ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
219
+ alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
220
+ alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
221
+ alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
222
+ betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
223
+ correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
224
+ ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
225
+ };
226
+ };
227
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
228
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
229
+ const relative = toRecord(baseContext?.relative);
230
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
231
+ if (!btcAltRegime) {
232
+ return {
233
+ source: null,
234
+ available: false,
235
+ universe: null,
236
+ interval: null,
237
+ stale: null,
238
+ regime: null,
239
+ btcReturn24h: null,
240
+ altBasketReturn24h: null,
241
+ btcVsAltReturn24h: null,
242
+ btcTurnoverShare24h: null,
243
+ btcTurnoverShareChange24h: null,
244
+ altVolToBtcVol24h: null,
245
+ altDispersion24h: null
246
+ };
247
+ }
248
+ return {
249
+ source: String(btcAltRegime.source ?? ""),
250
+ available: true,
251
+ universe: String(btcAltRegime.universe ?? ""),
252
+ interval: String(btcAltRegime.interval ?? ""),
253
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
254
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
255
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
256
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
257
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
258
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
259
+ btcTurnoverShareChange24h: toFiniteNumber(
260
+ btcAltRegime.btcTurnoverShareChange24h
261
+ ),
262
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
263
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
264
+ };
265
+ };
266
+ var buildCmcGlobalContextFromSignal = (signal) => {
267
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
268
+ const relative = toRecord(baseContext?.relative);
269
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
270
+ if (!cmcGlobal) {
271
+ return {
272
+ source: null,
273
+ available: false,
274
+ interval: null,
275
+ asOfTs: null,
276
+ stale: null,
277
+ totalMarketCapUsd: null,
278
+ totalVolumeUsd: null,
279
+ totalVolumeReportedUsd: null,
280
+ altMarketCapUsd: null,
281
+ altVolumeUsd: null,
282
+ altVolumeReportedUsd: null,
283
+ btcDominancePct: null,
284
+ ethDominancePct: null,
285
+ btcDominanceChange24hPct: null,
286
+ ethDominanceChange24hPct: null,
287
+ altMarketCapChange24hPct: null,
288
+ altVolumeChange24hPct: null,
289
+ activeCryptocurrencies: null,
290
+ activeExchanges: null,
291
+ activeMarketPairs: null,
292
+ altLiquidityRegime: null
293
+ };
294
+ }
295
+ return {
296
+ source: String(cmcGlobal.source ?? ""),
297
+ available: true,
298
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
299
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
300
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
301
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
302
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
303
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
304
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
305
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
306
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
307
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
308
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
309
+ btcDominanceChange24hPct: toFiniteNumber(
310
+ cmcGlobal.btcDominanceChange24hPct
311
+ ),
312
+ ethDominanceChange24hPct: toFiniteNumber(
313
+ cmcGlobal.ethDominanceChange24hPct
314
+ ),
315
+ altMarketCapChange24hPct: toFiniteNumber(
316
+ cmcGlobal.altMarketCapChange24hPct
317
+ ),
318
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
319
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
320
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
321
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
322
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
323
+ };
324
+ };
325
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
326
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
327
+ const relative = toRecord(baseContext?.relative);
328
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
329
+ if (!cmcReferenceAssets) {
330
+ return {
331
+ source: null,
332
+ available: false,
333
+ interval: null,
334
+ asOfTs: null,
335
+ stale: null,
336
+ btcMarketCapUsd: null,
337
+ ethMarketCapUsd: null,
338
+ btcVolumeUsd: null,
339
+ ethVolumeUsd: null,
340
+ btcVolumeToMarketCap: null,
341
+ ethVolumeToMarketCap: null,
342
+ ethBtcMarketCapRatio: null,
343
+ ethBtcMarketCapRatioChange24hPct: null,
344
+ ethVsBtcVolumeRatio: null,
345
+ referenceLiquidityRegime: null
346
+ };
347
+ }
348
+ return {
349
+ source: String(cmcReferenceAssets.source ?? ""),
350
+ available: true,
351
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
352
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
353
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
354
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
355
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
356
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
357
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
358
+ btcVolumeToMarketCap: toFiniteNumber(
359
+ cmcReferenceAssets.btcVolumeToMarketCap
360
+ ),
361
+ ethVolumeToMarketCap: toFiniteNumber(
362
+ cmcReferenceAssets.ethVolumeToMarketCap
363
+ ),
364
+ ethBtcMarketCapRatio: toFiniteNumber(
365
+ cmcReferenceAssets.ethBtcMarketCapRatio
366
+ ),
367
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
368
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
369
+ ),
370
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
371
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
372
+ };
373
+ };
374
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
375
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
376
+ const relative = toRecord(baseContext?.relative);
377
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
378
+ if (!cmcExchangeLiquidity) {
379
+ return {
380
+ source: null,
381
+ available: false,
382
+ interval: null,
383
+ asOfTs: null,
384
+ stale: null,
385
+ exchangesCount: null,
386
+ totalVolumeUsd: null,
387
+ totalVolumeChange24hPct: null,
388
+ binanceVolumeUsd: null,
389
+ binanceVolumeShare: null,
390
+ topExchangeVolumeShare: null,
391
+ liquidityRegime: null
392
+ };
393
+ }
394
+ return {
395
+ source: String(cmcExchangeLiquidity.source ?? ""),
396
+ available: true,
397
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
398
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
399
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
400
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
401
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
402
+ totalVolumeChange24hPct: toFiniteNumber(
403
+ cmcExchangeLiquidity.totalVolumeChange24hPct
404
+ ),
405
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
406
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
407
+ topExchangeVolumeShare: toFiniteNumber(
408
+ cmcExchangeLiquidity.topExchangeVolumeShare
409
+ ),
410
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
411
+ };
412
+ };
413
+ var buildCmcFearGreedContextFromSignal = (signal) => {
414
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
415
+ const relative = toRecord(baseContext?.relative);
416
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
417
+ if (!cmcFearGreed) {
418
+ return {
419
+ source: null,
420
+ available: false,
421
+ interval: null,
422
+ asOfTs: null,
423
+ stale: null,
424
+ value: null,
425
+ valueChange24h: null,
426
+ valueChange7d: null,
427
+ classification: null,
428
+ sentimentRegime: null
429
+ };
430
+ }
431
+ return {
432
+ source: String(cmcFearGreed.source ?? ""),
433
+ available: true,
434
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
435
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
436
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
437
+ value: toFiniteNumber(cmcFearGreed.value),
438
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
439
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
440
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
441
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
442
+ };
443
+ };
444
+ var buildCmcIndexesContextFromSignal = (signal) => {
445
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
446
+ const relative = toRecord(baseContext?.relative);
447
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
448
+ if (!cmcIndexes) {
449
+ return {
450
+ source: null,
451
+ available: false,
452
+ interval: null,
453
+ asOfTs: null,
454
+ stale: null,
455
+ cmc100Value: null,
456
+ cmc100Change24hPct: null,
457
+ cmc100TopConstituentSymbol: null,
458
+ cmc100TopConstituentWeightPct: null,
459
+ cmc20Value: null,
460
+ cmc20Change24hPct: null,
461
+ cmc20TopConstituentSymbol: null,
462
+ cmc20TopConstituentWeightPct: null,
463
+ cmc20ToCmc100Ratio: null,
464
+ cmc20ToCmc100RatioChange24hPct: null,
465
+ indexRegime: null
466
+ };
467
+ }
468
+ return {
469
+ source: String(cmcIndexes.source ?? ""),
470
+ available: true,
471
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
472
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
473
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
474
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
475
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
476
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
477
+ cmc100TopConstituentWeightPct: toFiniteNumber(
478
+ cmcIndexes.cmc100TopConstituentWeightPct
479
+ ),
480
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
481
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
482
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
483
+ cmc20TopConstituentWeightPct: toFiniteNumber(
484
+ cmcIndexes.cmc20TopConstituentWeightPct
485
+ ),
486
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
487
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
488
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
489
+ ),
490
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
491
+ };
492
+ };
493
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
494
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
495
+ const relative = toRecord(baseContext?.relative);
496
+ const refs = toRecord(relative?.referenceTradeFlow);
497
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
498
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
499
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
500
+ if (!refs) {
501
+ return {
502
+ source: null,
503
+ available: false,
504
+ primaryReferenceSymbol: null,
505
+ referenceSymbols: [],
506
+ primaryTradeFlowBuyPressurePct: null,
507
+ primaryTradeFlowStale: null
508
+ };
509
+ }
510
+ return {
511
+ source: String(refs.source ?? ""),
512
+ available: true,
513
+ primaryReferenceSymbol,
514
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
515
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
516
+ primaryTradeFlow?.buyPressurePct
517
+ ),
518
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
519
+ };
520
+ };
521
+ var buildAiMarketContext = (signal) => ({
522
+ execution: {
523
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
524
+ },
525
+ participation: {
526
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
527
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
528
+ },
529
+ relative: {
530
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
531
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
532
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
533
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
534
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
535
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
536
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
537
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
538
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
539
+ }
540
+ });
541
+
542
+ // src/strategy/policyProfiles.ts
543
+ var profileMatches = (profile, universe, assetClass) => {
544
+ const { appliesTo } = profile;
545
+ if (!appliesTo) return true;
546
+ if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
547
+ return false;
548
+ }
549
+ if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
550
+ return false;
551
+ }
552
+ return true;
553
+ };
554
+ var resolveStrategyPolicyProfile = (manifest, params) => {
555
+ const profiles = manifest?.policyProfiles ?? [];
556
+ if (!profiles.length) {
557
+ const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
558
+ if (!inferredId) return void 0;
559
+ if (inferredId !== "crypto" && inferredId !== "tradfi") {
560
+ throw new Error(
561
+ `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
562
+ );
563
+ }
564
+ if (params.universe && inferredId !== params.universe) {
565
+ throw new Error(
566
+ `Policy profile "${inferredId}" is not compatible with ${params.universe}`
567
+ );
568
+ }
569
+ return {
570
+ id: inferredId,
571
+ appliesTo: { universes: [inferredId] },
572
+ marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
573
+ ...manifest?.mlAdapter ? {
574
+ entryRuntimeDefaults: {
575
+ ml: {
576
+ modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
577
+ }
578
+ }
579
+ } : {}
580
+ };
581
+ }
582
+ if (params.profileId) {
583
+ const profile = profiles.find(({ id }) => id === params.profileId);
584
+ if (!profile) {
585
+ throw new Error(
586
+ `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
587
+ );
588
+ }
589
+ if (!profileMatches(profile, params.universe, params.assetClass)) {
590
+ throw new Error(
591
+ `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
592
+ );
593
+ }
594
+ return profile;
595
+ }
596
+ const matching = profiles.filter(
597
+ (profile) => profileMatches(profile, params.universe, params.assetClass)
598
+ );
599
+ const defaultProfile = matching.find(
600
+ ({ id }) => id === manifest?.defaultPolicyProfileId
601
+ );
602
+ return defaultProfile ?? matching[0];
603
+ };
604
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
605
+ var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
606
+
607
+ // src/strategyAdapters/ai.ts
608
+ var toRecord2 = (value) => {
609
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
610
+ return {};
611
+ }
612
+ return value;
613
+ };
614
+ var buildBaseAiPayload = (signal) => {
615
+ const additionalIndicators = {
616
+ ...toRecord2(signal.additionalIndicators),
617
+ marketContext: buildAiMarketContext(signal)
618
+ };
619
+ return {
620
+ signal: {
621
+ symbol: signal.symbol,
622
+ signalId: signal.signalId,
623
+ interval: signal.interval,
624
+ direction: signal.direction,
625
+ timestamp: signal.timestamp,
626
+ strategy: signal.strategy,
627
+ prices: {
628
+ currentPrice: signal.prices.currentPrice,
629
+ takeProfitPrice: signal.prices.takeProfitPrice,
630
+ stopLossPrice: signal.prices.stopLossPrice
631
+ }
632
+ },
633
+ figures: trimSeriesDeep(signal.figures ?? {}),
634
+ indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
635
+ additionalIndicators: trimSeriesDeep(additionalIndicators)
636
+ };
637
+ };
638
+ var defaultAiAdapter = {};
639
+ var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
640
+ var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
641
+ var buildAiPayloadByStrategy = (signal) => {
642
+ const basePayload = buildBaseAiPayload(signal);
643
+ const adapter = getSignalAiAdapter(signal);
644
+ return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
645
+ };
646
+ var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
647
+ var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
648
+ signal,
649
+ payload
650
+ }) ?? "";
651
+ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
652
+ signal,
653
+ payload,
654
+ analysis
655
+ }) ?? analysis;
656
+
657
+ // src/ai.ts
658
+ var parseAIResponse = (input) => {
659
+ try {
660
+ if (typeof input === "object" && input !== null) return input;
661
+ const match = input.match(/\{[\s\S]*\}/);
662
+ if (!match) throw new Error("JSON block not found");
663
+ return JSON.parse(match[0]);
664
+ } catch (err) {
665
+ console.error("Failed to parse AI response:", err);
666
+ console.log("Raw AI response:", input);
667
+ return {};
668
+ }
669
+ };
670
+ var normalizeResponseContent = (content) => {
671
+ if (typeof content === "string" || content && typeof content === "object") {
672
+ if (typeof content !== "object" || !Array.isArray(content)) {
673
+ return content;
674
+ }
675
+ }
676
+ if (Array.isArray(content)) {
677
+ const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
678
+ return text;
679
+ }
680
+ return String(content ?? "");
681
+ };
682
+ var normalizeAnalysis = (raw) => {
683
+ const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
684
+ const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
685
+ const toNumberOrNull = (value) => {
686
+ if (typeof value === "number" && Number.isFinite(value)) return value;
687
+ if (typeof value === "string" && value.trim()) {
688
+ const parsed = Number(value);
689
+ if (Number.isFinite(parsed)) return parsed;
690
+ }
691
+ return null;
692
+ };
693
+ const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
694
+ return {
695
+ direction,
696
+ quality: qualityNum,
697
+ needRetest: Boolean(raw?.needRetest),
698
+ retestPrice: toNumberOrNull(raw?.retestPrice),
699
+ takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
700
+ stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
701
+ setup: toText(raw?.setup),
702
+ confirmations: toText(raw?.confirmations),
703
+ btcContext: toText(raw?.btcContext),
704
+ retestPlan: toText(raw?.retestPlan),
705
+ riskLevels: toText(raw?.riskLevels),
706
+ qualityReason: toText(raw?.qualityReason),
707
+ triggerInvalidation: toText(raw?.triggerInvalidation),
708
+ comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
709
+ };
710
+ };
711
+ var asRecord = (value) => {
712
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
713
+ return null;
714
+ }
715
+ return value;
716
+ };
717
+ var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
718
+ var getDeterministicQuality = (gateContext) => {
719
+ const deterministicQuality = Number(gateContext?.deterministicQuality);
720
+ if (Number.isFinite(deterministicQuality)) {
721
+ return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
722
+ }
723
+ const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
724
+ if (Number.isFinite(maxAllowedQuality)) {
725
+ return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
726
+ }
727
+ return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
728
+ };
729
+ var buildAiSystemPrompt = (signal) => `
730
+ You are an internal market-structure classifier for an already computed system signal.
731
+ Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
732
+ Series data is already trimmed to the latest 5 values.
733
+
734
+ Important:
735
+ - Do not invent missing data.
736
+ - This is an internal audit/classification task, not user-facing trading advice.
737
+ - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
738
+ - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
739
+ - Respect the source strategy specified in \`signal.strategy\`.
740
+ - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
741
+ - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
742
+ - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
743
+ - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
744
+
745
+ Return exactly one JSON object and nothing else:
746
+
747
+ {
748
+ "direction": payload.signal.direction | null,
749
+ "quality": 1 | 2 | 3 | 4 | 5,
750
+ "needRetest": boolean,
751
+ "retestPrice": number | null,
752
+ "takeProfitPrice": number | null,
753
+ "stopLossPrice": number | null,
754
+ "setup": string,
755
+ "confirmations": string,
756
+ "btcContext": string,
757
+ "retestPlan": string,
758
+ "riskLevels": string,
759
+ "qualityReason": string,
760
+ "triggerInvalidation": string
761
+ }
762
+
763
+ - Do not add any other fields.
764
+ - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
765
+ - All text fields must be short strings with no line breaks and no markdown lists.
766
+ - \`direction\` is not a new trade idea. It is only a compatibility flag for the existing signal: either exactly \`payload.signal.direction\` or \`null\` if the current structure does not confirm that signal. Never propose the opposite direction.
767
+ - \`quality\` is the structural confirmation level of the current signal right now, including timing and confirmations. It is not a general attractiveness score and not investment advice.
768
+ - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
769
+ - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
770
+ - \`takeProfitPrice\` and \`stopLossPrice\` must not be newly invented levels. If the levels already supplied in \`payload.signal.prices\` still look internally coherent relative to the current price and the confirmed signal, you may return them as an audit of existing levels; otherwise return \`null\`.
771
+ - Use these fields as separate parts of the analysis:
772
+ - \`setup\`: the current structural setup or trendline state.
773
+ - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
774
+ - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
775
+ - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
776
+ - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
777
+ - \`qualityReason\`: why the quality score is what it is.
778
+ - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
779
+ - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
780
+
781
+ If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
782
+
783
+ Input payload structure:
784
+ - payload.signal:
785
+ symbol, signalId, interval, direction, timestamp, strategy, prices
786
+ - payload.signal.prices:
787
+ currentPrice, takeProfitPrice, stopLossPrice
788
+ - payload.figures:
789
+ strategy-specific figures or geometry when available. Fields vary by strategy.
790
+ - payload.indicators:
791
+ 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.
792
+ - payload.additionalIndicators:
793
+ strategy-specific summary/context fields plus the canonical current shared context snapshot.
794
+ This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
795
+ Examples: baseContext, helperFlags, structureContext, volatilitySummary.
796
+ Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
797
+ \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
798
+ \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
799
+ \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
800
+ \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, and Binance aggTrades trade-flow when available.
801
+ \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
802
+ \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
803
+ \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
804
+ \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.
805
+ Always inspect \`payload.additionalIndicators.marketContext\` when present:
806
+ \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
807
+ \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
808
+ \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
809
+ \u2022 \`marketContext.relative.marketBreadth\`: equal/volume-weighted alt-basket return, advance/decline ratio, and MA breadth for the configured Binance breadth universe.
810
+ \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.
811
+ \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.
812
+ \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\`.
813
+ \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\`.
814
+ \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
815
+ \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
816
+ \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
817
+ \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
818
+ 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.
819
+ 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.
820
+ Key patterns:
821
+ \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
822
+ \u2022 recent historical series: \`payload.indicators\`
823
+ \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
824
+
825
+ How to analyze, in order:
826
+ 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
827
+ 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
828
+ 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
829
+ 4. Then evaluate BTC context.
830
+ 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
831
+ 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
832
+
833
+ Explicit conflict rules:
834
+ - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
835
+ - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
836
+ - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
837
+ - 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.
838
+ - 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.
839
+ - 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.
840
+ - 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.
841
+ - 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\`.
842
+ - 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.
843
+ - 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.
844
+ - 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.
845
+ - 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.
846
+ - 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.
847
+ - 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.
848
+ - 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.
849
+ - 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.
850
+ - 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.
851
+ - 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.
852
+ - 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.
853
+ - 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.
854
+ - 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.
855
+ - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
856
+ If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
857
+
858
+ Rules for \`direction\` / TP / SL:
859
+ - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
860
+ - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
861
+ - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
862
+ - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
863
+ - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
864
+ - If \`needRetest = false\`, then \`retestPrice = null\`.
865
+ - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
866
+ - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
867
+
868
+ Quality scale:
869
+ - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
870
+ - 2: weak setup, few confirmations, more of a watch or reject
871
+ - 3: average setup, some structure exists, but notable conflicts remain
872
+ - 4: good setup, several confirmations, structure is mostly coherent
873
+ - 5: very strong setup, clean structure, confirmations, and internally coherent levels
874
+
875
+ Requirements for useful structured analysis:
876
+ - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
877
+ - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
878
+ - Explicitly mention BTC context as supportive, neutral, or conflicting.
879
+ - Explain why the quality score is what it is.
880
+ - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
881
+ - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
882
+ - Do not simply restate JSON fields; add interpretation and decision logic.
883
+
884
+ Rules for using trimmed series (last 5 values):
885
+ - Do not make strong long-term conclusions from only 5 points.
886
+ - Use 4h and 1d series as brief context, not full history.
887
+ - If the data is too limited for confidence, reduce quality and use cautious wording.
888
+
889
+ Short few-shot examples:
890
+ {"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"Likely trendline breakout upward, but the signal still needs a level check for confirmation.","confirmations":"The coin shows momentum support without obvious overheating, but confirmation is not fully clean yet.","btcContext":"BTC is neutral-to-supportive and does not conflict with the current LONG signal.","retestPlan":"The key level is 100.2; holding above it would confirm the signal structure.","riskLevels":"The supplied TP and SL remain on the correct sides of the current price and still look internally coherent.","qualityReason":"Quality=4 because the structure is solid, but an extra level confirmation is still preferable.","triggerInvalidation":"The structure confirms on a hold above the level and weakens on a move back under the line."}
891
+ {"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"Touch or noise around the trendline without a convincing breakout.","confirmations":"Indicators are mixed and do not provide strong structural support.","btcContext":"BTC is either conflicting or not supportive of the current thesis.","retestPlan":"It is too early to define an extra level because a quality breakout is not present yet.","riskLevels":"The supplied levels should not be treated as confirmed while the structure remains weak.","qualityReason":"Quality=2 because timing is weak and confirmations are limited.","triggerInvalidation":"Wait for a clear breakout and confirmation from both the coin and BTC."}
892
+
893
+ Return only the JSON object, with no extra characters.
894
+ ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
895
+ `;
896
+ var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
897
+ var getDeterministicAiGateContext = (payload) => {
898
+ const additionalIndicators = asRecord(payload.additionalIndicators);
899
+ const candidates = [
900
+ additionalIndicators,
901
+ ...Object.values(additionalIndicators ?? {}).map(asRecord)
902
+ ].filter((value) => Boolean(value));
903
+ return candidates.find(
904
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
905
+ ) ?? null;
906
+ };
907
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
908
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
909
+ This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
910
+
911
+ Trade payload:
912
+ ${JSON.stringify(payload)}
913
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
914
+ `;
915
+ var getAiInvocationError = (error) => {
916
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
917
+ const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
918
+ details
919
+ );
920
+ const wrapped = new Error(
921
+ isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
922
+ );
923
+ wrapped.cause = error;
924
+ return wrapped;
925
+ };
926
+ var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
927
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
928
+ var userSettingsCache = /* @__PURE__ */ new Map();
929
+ var aiModelCache = /* @__PURE__ */ new Map();
930
+ var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
931
+ var resolveAiModelName = (settings, requestedModelName) => {
932
+ const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
933
+ if (explicitModelName) {
934
+ return explicitModelName;
935
+ }
936
+ const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
937
+ return settingsModelName || DEFAULT_AI_MODEL;
938
+ };
939
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
940
+ const endpoint = String(apiEndpoint ?? "").trim();
941
+ if (!endpoint) {
942
+ return {};
943
+ }
944
+ let hostname = "";
945
+ try {
946
+ hostname = new URL(endpoint).hostname;
947
+ } catch {
948
+ hostname = endpoint;
949
+ }
950
+ if (!hostname.toLowerCase().includes("openrouter")) {
951
+ return {};
952
+ }
953
+ return {
954
+ provider: {
955
+ ignore: ["azure"]
956
+ }
957
+ };
958
+ };
959
+ var getAiSettings = async (userName = "root") => {
960
+ let settingsPromise = userSettingsCache.get(userName);
961
+ if (!settingsPromise) {
962
+ settingsPromise = getUserSettings(userName);
963
+ settingsPromise.catch(() => {
964
+ userSettingsCache.delete(userName);
965
+ });
966
+ userSettingsCache.set(userName, settingsPromise);
967
+ }
968
+ const settings = await settingsPromise;
969
+ if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
970
+ throw new Error(`AI settings are incomplete for user ${userName}`);
971
+ }
972
+ return settings;
973
+ };
974
+ var createAiModel = async (userName = "root", requestedModelName) => {
975
+ const settings = await getAiSettings(userName);
976
+ const modelName = resolveAiModelName(settings, requestedModelName);
977
+ const cacheKey = getAiModelCacheKey(userName, modelName);
978
+ let modelPromise = aiModelCache.get(cacheKey);
979
+ if (!modelPromise) {
980
+ modelPromise = (async () => {
981
+ const { ChatOpenAI } = await import("@langchain/openai");
982
+ const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
983
+ return new ChatOpenAI({
984
+ temperature: 0.2,
985
+ modelName,
986
+ apiKey: settings.AI_API_KEY,
987
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
988
+ configuration: {
989
+ baseURL: settings.AI_API_ENDPOINT,
990
+ defaultHeaders: {
991
+ "HTTP-Referer": "https://tradejs.dev",
992
+ "X-Title": "Inv"
993
+ }
994
+ }
995
+ });
996
+ })();
997
+ modelPromise.catch(() => {
998
+ aiModelCache.delete(cacheKey);
999
+ });
1000
+ aiModelCache.set(cacheKey, modelPromise);
1001
+ }
1002
+ return modelPromise;
1003
+ };
1004
+ var getAiModel = async (userName = "root", requestedModelName) => {
1005
+ const settings = await getAiSettings(userName);
1006
+ const resolvedModelName = resolveAiModelName(settings, requestedModelName);
1007
+ try {
1008
+ return await createAiModel(userName, resolvedModelName);
1009
+ } catch (error) {
1010
+ aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
1011
+ userSettingsCache.delete(userName);
1012
+ throw error;
1013
+ }
1014
+ };
1015
+ var resetAiRuntimeCache = () => {
1016
+ aiModelCache.clear();
1017
+ userSettingsCache.clear();
1018
+ };
1019
+ var ensureAiStrategyPluginsLoaded = async () => {
1020
+ await ensureStrategyPluginsLoaded();
1021
+ };
1022
+ var buildAiPrompts = (signal) => {
1023
+ const payload = buildAiPayload(signal);
1024
+ return {
1025
+ systemPrompt: buildAiSystemPrompt(signal),
1026
+ humanPrompt: buildAiHumanPrompt(signal, payload)
1027
+ };
1028
+ };
1029
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
1030
+ if (options.signal) {
1031
+ await ensureAiStrategyPluginsLoaded();
1032
+ }
1033
+ const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
1034
+ import("@langchain/core/messages"),
1035
+ getAiModel(options.userName, options.model),
1036
+ getAiSettings(options.userName)
1037
+ ]);
1038
+ const messages = [];
1039
+ const responseLanguage = getAiResponseLanguagePromptName(
1040
+ settings.AI_RESPONSE_LANGUAGE || DEFAULT_AI_RESPONSE_LANGUAGE
1041
+ );
1042
+ messages.push(new SystemMessage(systemPrompt));
1043
+ messages.push(
1044
+ new SystemMessage(
1045
+ `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
1046
+ )
1047
+ );
1048
+ messages.push(
1049
+ new HumanMessage({
1050
+ content: [
1051
+ {
1052
+ type: "text",
1053
+ text: humanPrompt
1054
+ }
1055
+ ]
1056
+ })
1057
+ );
1058
+ let response;
1059
+ try {
1060
+ response = await model.invoke(messages);
1061
+ } catch (error) {
1062
+ throw getAiInvocationError(error);
1063
+ }
1064
+ const responseContent = normalizeResponseContent(response?.content);
1065
+ if (isEmptyResponseContent(responseContent)) {
1066
+ throw new Error("AI provider returned an empty chat completion");
1067
+ }
1068
+ const parsed = parseAIResponse(responseContent);
1069
+ const normalized = normalizeAnalysis(parsed);
1070
+ if (!options.signal) {
1071
+ return normalized;
1072
+ }
1073
+ return postProcessAiAnalysisByStrategy(
1074
+ options.signal,
1075
+ normalized,
1076
+ options.payload
1077
+ );
1078
+ };
1079
+ var runAiPromptLocal = async (signal, options = {}) => {
1080
+ await ensureAiStrategyPluginsLoaded();
1081
+ const payload = options.payload ?? buildAiPayload(signal);
1082
+ const gateContext = getDeterministicAiGateContext(payload);
1083
+ const signalDirection = getSignalDirection(signal);
1084
+ const deterministicQuality = getDeterministicQuality(gateContext);
1085
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
1086
+ return postProcessAiAnalysisByStrategy(
1087
+ signal,
1088
+ {
1089
+ direction: approvalAllowedNow ? signalDirection : null,
1090
+ quality: deterministicQuality,
1091
+ needRetest: !approvalAllowedNow,
1092
+ retestPrice: null,
1093
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
1094
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
1095
+ },
1096
+ payload
1097
+ );
1098
+ };
1099
+ var askAI = async (signal, options = {}) => {
1100
+ const { symbol } = signal;
1101
+ await ensureAiStrategyPluginsLoaded();
1102
+ const payload = buildAiPayload(signal);
1103
+ const content = await runAiPrompt(
1104
+ {
1105
+ systemPrompt: buildAiSystemPrompt(signal),
1106
+ humanPrompt: buildAiHumanPrompt(signal, payload)
1107
+ },
1108
+ {
1109
+ ...options,
1110
+ signal,
1111
+ payload
1112
+ }
1113
+ );
1114
+ await setData(redisKeys.analysis(symbol, signal.signalId), content);
1115
+ return content;
1116
+ };
1117
+
1118
+ export {
1119
+ MAX_AI_SERIES_POINTS,
1120
+ trimSeriesDeep,
1121
+ buildCompactAiIndicatorsSnapshot,
1122
+ resolveStrategyPolicyProfile,
1123
+ getStrategyProfileMlAdapter,
1124
+ buildAiSystemPrompt,
1125
+ buildAiPayload,
1126
+ getDeterministicAiGateContext,
1127
+ buildAiHumanPrompt,
1128
+ DEFAULT_AI_MODEL,
1129
+ getOpenRouterModelKwargs,
1130
+ resetAiRuntimeCache,
1131
+ ensureAiStrategyPluginsLoaded,
1132
+ buildAiPrompts,
1133
+ runAiPrompt,
1134
+ runAiPromptLocal,
1135
+ askAI
1136
+ };