@tradejs/node 2.0.21 → 3.0.1
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.d.mts +1 -2
- package/dist/ai.d.ts +1 -2
- package/dist/ai.js +163 -5157
- package/dist/ai.mjs +1 -3
- package/dist/backtest.js +1085 -1051
- package/dist/backtest.mjs +9 -3
- package/dist/{chunk-4AVFJMQL.mjs → chunk-OGAWBO3Z.mjs} +1722 -3147
- package/dist/chunk-VRXU4E4O.mjs +1469 -0
- package/dist/cli.js +228 -4847
- package/dist/cli.mjs +5 -2
- package/dist/registry.js +1007 -977
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +1 -1
- package/dist/strategies.d.ts +1 -1
- package/dist/strategies.js +5139 -5111
- package/dist/strategies.mjs +22 -23
- package/package.json +5 -5
- package/dist/chunk-5YNMSWL3.mjs +0 -0
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTradejsProjectCwd,
|
|
3
|
+
importTradejsModule,
|
|
4
|
+
loadTradejsConfig,
|
|
5
|
+
resolvePluginModuleSpecifier
|
|
6
|
+
} from "./chunk-WS5DYEVZ.mjs";
|
|
7
|
+
|
|
8
|
+
// src/ai.ts
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_AI_RESPONSE_LANGUAGE,
|
|
11
|
+
getAiResponseLanguagePromptName,
|
|
12
|
+
normalizeAiResponseLanguage
|
|
13
|
+
} from "@tradejs/core/aiLanguages";
|
|
14
|
+
import { normalizeAiEndpoint } from "@tradejs/core/aiEndpoints";
|
|
15
|
+
import { normalizeAiModel } from "@tradejs/core/aiModels";
|
|
16
|
+
import { setData, redisKeys } from "@tradejs/infra/redis";
|
|
17
|
+
import {
|
|
18
|
+
getUserSettings
|
|
19
|
+
} from "@tradejs/infra/userSettings";
|
|
20
|
+
|
|
21
|
+
// src/aiShared.ts
|
|
22
|
+
var MAX_AI_SERIES_POINTS = 5;
|
|
23
|
+
var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
|
|
24
|
+
"tradejs.indicators.compactSnapshot"
|
|
25
|
+
);
|
|
26
|
+
var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
|
|
27
|
+
var trimSeriesDeep = (value) => {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
|
|
30
|
+
const isMatrix = trimmed.every((item) => Array.isArray(item));
|
|
31
|
+
if (isMatrix) {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
return trimmed.map(
|
|
35
|
+
(item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === "object") {
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(value).map(([key, nested]) => [
|
|
41
|
+
key,
|
|
42
|
+
trimSeriesDeep(nested)
|
|
43
|
+
])
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
var buildCompactAiIndicatorsSnapshot = (value) => {
|
|
49
|
+
const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
|
|
50
|
+
if (typeof compactSnapshot === "function") {
|
|
51
|
+
return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
|
|
52
|
+
}
|
|
53
|
+
return trimSeriesDeep(value);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/aiMarketContext.ts
|
|
57
|
+
var toRecord = (value) => {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
var toFiniteNumber = (value) => {
|
|
64
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
|
|
65
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
66
|
+
};
|
|
67
|
+
var roundTo = (value, decimals) => {
|
|
68
|
+
const factor = 10 ** decimals;
|
|
69
|
+
return Math.round(value * factor) / factor;
|
|
70
|
+
};
|
|
71
|
+
var buildMissingSpreadContext = () => ({
|
|
72
|
+
source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
73
|
+
indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
74
|
+
available: false,
|
|
75
|
+
value: null,
|
|
76
|
+
zScore: null,
|
|
77
|
+
bps: null,
|
|
78
|
+
absBps: null,
|
|
79
|
+
bias: null,
|
|
80
|
+
severity: null
|
|
81
|
+
});
|
|
82
|
+
var buildSpreadContextFromSignal = (signal) => {
|
|
83
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
84
|
+
const relative = toRecord(baseContext?.relative);
|
|
85
|
+
const execution = toRecord(relative?.execution);
|
|
86
|
+
const spread = toFiniteNumber(execution?.venueSpread);
|
|
87
|
+
const zScore = toFiniteNumber(execution?.venueSpreadZScore);
|
|
88
|
+
if (spread == null) {
|
|
89
|
+
return buildMissingSpreadContext();
|
|
90
|
+
}
|
|
91
|
+
const value = roundTo(spread, 8);
|
|
92
|
+
const bps = roundTo(value * 1e4, 2);
|
|
93
|
+
const absBps = Math.abs(bps);
|
|
94
|
+
const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
|
|
95
|
+
const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
|
|
96
|
+
return {
|
|
97
|
+
source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
98
|
+
indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
|
|
99
|
+
available: true,
|
|
100
|
+
value,
|
|
101
|
+
zScore,
|
|
102
|
+
bps,
|
|
103
|
+
absBps,
|
|
104
|
+
bias,
|
|
105
|
+
severity
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
var buildTrueDeltaContextFromSignal = (signal) => {
|
|
109
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
110
|
+
const participation = toRecord(baseContext?.participation);
|
|
111
|
+
const delta = toRecord(participation?.delta);
|
|
112
|
+
const source = String(delta?.source ?? "");
|
|
113
|
+
const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
|
|
114
|
+
if (!delta || !isTrueDeltaSource) {
|
|
115
|
+
return {
|
|
116
|
+
source: source || null,
|
|
117
|
+
available: false,
|
|
118
|
+
buyPressurePct: null,
|
|
119
|
+
buyVolume: null,
|
|
120
|
+
sellVolume: null,
|
|
121
|
+
netDelta: null,
|
|
122
|
+
deltaPct: null,
|
|
123
|
+
signedVolumeZScore: null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
source,
|
|
128
|
+
available: true,
|
|
129
|
+
buyPressurePct: toFiniteNumber(delta.buyPressurePct),
|
|
130
|
+
buyVolume: toFiniteNumber(delta.buyVolume),
|
|
131
|
+
sellVolume: toFiniteNumber(delta.sellVolume),
|
|
132
|
+
netDelta: toFiniteNumber(delta.netDelta),
|
|
133
|
+
deltaPct: toFiniteNumber(delta.deltaPct),
|
|
134
|
+
signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
var buildTradeFlowContextFromSignal = (signal) => {
|
|
138
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
139
|
+
const participation = toRecord(baseContext?.participation);
|
|
140
|
+
const tradeFlow = toRecord(participation?.tradeFlow);
|
|
141
|
+
if (!tradeFlow) {
|
|
142
|
+
return {
|
|
143
|
+
source: null,
|
|
144
|
+
available: false,
|
|
145
|
+
interval: null,
|
|
146
|
+
stale: null,
|
|
147
|
+
trades: null,
|
|
148
|
+
buyPressurePct: null,
|
|
149
|
+
netBaseDelta: null,
|
|
150
|
+
netQuoteDelta: null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
source: String(tradeFlow.source ?? ""),
|
|
155
|
+
available: true,
|
|
156
|
+
interval: String(tradeFlow.interval ?? ""),
|
|
157
|
+
stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
|
|
158
|
+
trades: toFiniteNumber(tradeFlow.trades),
|
|
159
|
+
buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
|
|
160
|
+
netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
|
|
161
|
+
netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
var buildMarketBreadthContextFromSignal = (signal) => {
|
|
165
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
166
|
+
const relative = toRecord(baseContext?.relative);
|
|
167
|
+
const breadth = toRecord(relative?.marketBreadth);
|
|
168
|
+
if (!breadth) {
|
|
169
|
+
return {
|
|
170
|
+
source: null,
|
|
171
|
+
available: false,
|
|
172
|
+
universe: null,
|
|
173
|
+
interval: null,
|
|
174
|
+
stale: null,
|
|
175
|
+
symbolsCount: null,
|
|
176
|
+
advanceDeclineRatio: null,
|
|
177
|
+
pctAboveMa20: null,
|
|
178
|
+
pctAboveMa50: null,
|
|
179
|
+
equalWeightedReturn: null,
|
|
180
|
+
volumeWeightedReturn: null,
|
|
181
|
+
dispersion: null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
source: String(breadth.source ?? ""),
|
|
186
|
+
available: true,
|
|
187
|
+
universe: String(breadth.universe ?? ""),
|
|
188
|
+
interval: String(breadth.interval ?? ""),
|
|
189
|
+
stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
|
|
190
|
+
symbolsCount: toFiniteNumber(breadth.symbolsCount),
|
|
191
|
+
advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
|
|
192
|
+
pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
|
|
193
|
+
pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
|
|
194
|
+
equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
|
|
195
|
+
volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
|
|
196
|
+
dispersion: toFiniteNumber(breadth.dispersion)
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
var buildMarketBreadthsContextFromSignal = (signal) => {
|
|
200
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
201
|
+
const relative = toRecord(baseContext?.relative);
|
|
202
|
+
const breadths = toRecord(relative?.marketBreadths);
|
|
203
|
+
const build = (key) => {
|
|
204
|
+
const breadth = toRecord(breadths?.[key]);
|
|
205
|
+
if (!breadth) {
|
|
206
|
+
return {
|
|
207
|
+
source: null,
|
|
208
|
+
available: false,
|
|
209
|
+
universe: null,
|
|
210
|
+
interval: null,
|
|
211
|
+
stale: null,
|
|
212
|
+
symbolsCount: null,
|
|
213
|
+
advanceDeclineRatio: null,
|
|
214
|
+
pctAboveMa20: null,
|
|
215
|
+
pctAboveMa50: null,
|
|
216
|
+
equalWeightedReturn: null,
|
|
217
|
+
volumeWeightedReturn: null,
|
|
218
|
+
dispersion: null
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
source: String(breadth.source ?? ""),
|
|
223
|
+
available: true,
|
|
224
|
+
universe: String(breadth.universe ?? ""),
|
|
225
|
+
interval: String(breadth.interval ?? ""),
|
|
226
|
+
stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
|
|
227
|
+
symbolsCount: toFiniteNumber(breadth.symbolsCount),
|
|
228
|
+
advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
|
|
229
|
+
pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
|
|
230
|
+
pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
|
|
231
|
+
equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
|
|
232
|
+
volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
|
|
233
|
+
dispersion: toFiniteNumber(breadth.dispersion)
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
top5: build("top5"),
|
|
238
|
+
top10: build("top10"),
|
|
239
|
+
top30: build("top30"),
|
|
240
|
+
top50: build("top50"),
|
|
241
|
+
top100: build("top100")
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
var buildTargetVsBtcContextFromSignal = (signal) => {
|
|
245
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
246
|
+
const relative = toRecord(baseContext?.relative);
|
|
247
|
+
const targetVsBtc = toRecord(relative?.targetVsBtc);
|
|
248
|
+
if (!targetVsBtc) {
|
|
249
|
+
return {
|
|
250
|
+
source: null,
|
|
251
|
+
available: false,
|
|
252
|
+
ratioReturn1h: null,
|
|
253
|
+
ratioReturn4h: null,
|
|
254
|
+
ratioReturn24h: null,
|
|
255
|
+
alphaVsBtc1h: null,
|
|
256
|
+
alphaVsBtc4h: null,
|
|
257
|
+
alphaVsBtc24h: null,
|
|
258
|
+
betaToBtc20: null,
|
|
259
|
+
correlationToBtc20: null,
|
|
260
|
+
ratioTrend: null
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
source: String(targetVsBtc.source ?? ""),
|
|
265
|
+
available: true,
|
|
266
|
+
ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
|
|
267
|
+
ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
|
|
268
|
+
ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
|
|
269
|
+
alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
|
|
270
|
+
alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
|
|
271
|
+
alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
|
|
272
|
+
betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
|
|
273
|
+
correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
|
|
274
|
+
ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
var buildBtcAltRegimeContextFromSignal = (signal) => {
|
|
278
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
279
|
+
const relative = toRecord(baseContext?.relative);
|
|
280
|
+
const btcAltRegime = toRecord(relative?.btcAltRegime);
|
|
281
|
+
if (!btcAltRegime) {
|
|
282
|
+
return {
|
|
283
|
+
source: null,
|
|
284
|
+
available: false,
|
|
285
|
+
universe: null,
|
|
286
|
+
interval: null,
|
|
287
|
+
stale: null,
|
|
288
|
+
regime: null,
|
|
289
|
+
btcReturn24h: null,
|
|
290
|
+
altBasketReturn24h: null,
|
|
291
|
+
btcVsAltReturn24h: null,
|
|
292
|
+
btcTurnoverShare24h: null,
|
|
293
|
+
btcTurnoverShareChange24h: null,
|
|
294
|
+
altVolToBtcVol24h: null,
|
|
295
|
+
altDispersion24h: null
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
source: String(btcAltRegime.source ?? ""),
|
|
300
|
+
available: true,
|
|
301
|
+
universe: String(btcAltRegime.universe ?? ""),
|
|
302
|
+
interval: String(btcAltRegime.interval ?? ""),
|
|
303
|
+
stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
|
|
304
|
+
regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
|
|
305
|
+
btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
|
|
306
|
+
altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
|
|
307
|
+
btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
|
|
308
|
+
btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
|
|
309
|
+
btcTurnoverShareChange24h: toFiniteNumber(
|
|
310
|
+
btcAltRegime.btcTurnoverShareChange24h
|
|
311
|
+
),
|
|
312
|
+
altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
|
|
313
|
+
altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
var buildCmcGlobalContextFromSignal = (signal) => {
|
|
317
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
318
|
+
const relative = toRecord(baseContext?.relative);
|
|
319
|
+
const cmcGlobal = toRecord(relative?.cmcGlobal);
|
|
320
|
+
if (!cmcGlobal) {
|
|
321
|
+
return {
|
|
322
|
+
source: null,
|
|
323
|
+
available: false,
|
|
324
|
+
interval: null,
|
|
325
|
+
asOfTs: null,
|
|
326
|
+
stale: null,
|
|
327
|
+
totalMarketCapUsd: null,
|
|
328
|
+
totalVolumeUsd: null,
|
|
329
|
+
totalVolumeReportedUsd: null,
|
|
330
|
+
altMarketCapUsd: null,
|
|
331
|
+
altVolumeUsd: null,
|
|
332
|
+
altVolumeReportedUsd: null,
|
|
333
|
+
btcDominancePct: null,
|
|
334
|
+
ethDominancePct: null,
|
|
335
|
+
btcDominanceChange24hPct: null,
|
|
336
|
+
ethDominanceChange24hPct: null,
|
|
337
|
+
altMarketCapChange24hPct: null,
|
|
338
|
+
altVolumeChange24hPct: null,
|
|
339
|
+
activeCryptocurrencies: null,
|
|
340
|
+
activeExchanges: null,
|
|
341
|
+
activeMarketPairs: null,
|
|
342
|
+
altLiquidityRegime: null
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
source: String(cmcGlobal.source ?? ""),
|
|
347
|
+
available: true,
|
|
348
|
+
interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
|
|
349
|
+
asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
|
|
350
|
+
stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
|
|
351
|
+
totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
|
|
352
|
+
totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
|
|
353
|
+
totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
|
|
354
|
+
altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
|
|
355
|
+
altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
|
|
356
|
+
altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
|
|
357
|
+
btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
|
|
358
|
+
ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
|
|
359
|
+
btcDominanceChange24hPct: toFiniteNumber(
|
|
360
|
+
cmcGlobal.btcDominanceChange24hPct
|
|
361
|
+
),
|
|
362
|
+
ethDominanceChange24hPct: toFiniteNumber(
|
|
363
|
+
cmcGlobal.ethDominanceChange24hPct
|
|
364
|
+
),
|
|
365
|
+
altMarketCapChange24hPct: toFiniteNumber(
|
|
366
|
+
cmcGlobal.altMarketCapChange24hPct
|
|
367
|
+
),
|
|
368
|
+
altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
|
|
369
|
+
activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
|
|
370
|
+
activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
|
|
371
|
+
activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
|
|
372
|
+
altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
var buildCmcReferenceAssetsContextFromSignal = (signal) => {
|
|
376
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
377
|
+
const relative = toRecord(baseContext?.relative);
|
|
378
|
+
const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
|
|
379
|
+
if (!cmcReferenceAssets) {
|
|
380
|
+
return {
|
|
381
|
+
source: null,
|
|
382
|
+
available: false,
|
|
383
|
+
interval: null,
|
|
384
|
+
asOfTs: null,
|
|
385
|
+
stale: null,
|
|
386
|
+
btcMarketCapUsd: null,
|
|
387
|
+
ethMarketCapUsd: null,
|
|
388
|
+
btcVolumeUsd: null,
|
|
389
|
+
ethVolumeUsd: null,
|
|
390
|
+
btcVolumeToMarketCap: null,
|
|
391
|
+
ethVolumeToMarketCap: null,
|
|
392
|
+
ethBtcMarketCapRatio: null,
|
|
393
|
+
ethBtcMarketCapRatioChange24hPct: null,
|
|
394
|
+
ethVsBtcVolumeRatio: null,
|
|
395
|
+
referenceLiquidityRegime: null
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
source: String(cmcReferenceAssets.source ?? ""),
|
|
400
|
+
available: true,
|
|
401
|
+
interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
|
|
402
|
+
asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
|
|
403
|
+
stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
|
|
404
|
+
btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
|
|
405
|
+
ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
|
|
406
|
+
btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
|
|
407
|
+
ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
|
|
408
|
+
btcVolumeToMarketCap: toFiniteNumber(
|
|
409
|
+
cmcReferenceAssets.btcVolumeToMarketCap
|
|
410
|
+
),
|
|
411
|
+
ethVolumeToMarketCap: toFiniteNumber(
|
|
412
|
+
cmcReferenceAssets.ethVolumeToMarketCap
|
|
413
|
+
),
|
|
414
|
+
ethBtcMarketCapRatio: toFiniteNumber(
|
|
415
|
+
cmcReferenceAssets.ethBtcMarketCapRatio
|
|
416
|
+
),
|
|
417
|
+
ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
|
|
418
|
+
cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
|
|
419
|
+
),
|
|
420
|
+
ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
|
|
421
|
+
referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
|
|
425
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
426
|
+
const relative = toRecord(baseContext?.relative);
|
|
427
|
+
const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
|
|
428
|
+
if (!cmcExchangeLiquidity) {
|
|
429
|
+
return {
|
|
430
|
+
source: null,
|
|
431
|
+
available: false,
|
|
432
|
+
interval: null,
|
|
433
|
+
asOfTs: null,
|
|
434
|
+
stale: null,
|
|
435
|
+
exchangesCount: null,
|
|
436
|
+
totalVolumeUsd: null,
|
|
437
|
+
totalVolumeChange24hPct: null,
|
|
438
|
+
binanceVolumeUsd: null,
|
|
439
|
+
binanceVolumeShare: null,
|
|
440
|
+
topExchangeVolumeShare: null,
|
|
441
|
+
liquidityRegime: null
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
source: String(cmcExchangeLiquidity.source ?? ""),
|
|
446
|
+
available: true,
|
|
447
|
+
interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
|
|
448
|
+
asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
|
|
449
|
+
stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
|
|
450
|
+
exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
|
|
451
|
+
totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
|
|
452
|
+
totalVolumeChange24hPct: toFiniteNumber(
|
|
453
|
+
cmcExchangeLiquidity.totalVolumeChange24hPct
|
|
454
|
+
),
|
|
455
|
+
binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
|
|
456
|
+
binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
|
|
457
|
+
topExchangeVolumeShare: toFiniteNumber(
|
|
458
|
+
cmcExchangeLiquidity.topExchangeVolumeShare
|
|
459
|
+
),
|
|
460
|
+
liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
var buildCmcFearGreedContextFromSignal = (signal) => {
|
|
464
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
465
|
+
const relative = toRecord(baseContext?.relative);
|
|
466
|
+
const cmcFearGreed = toRecord(relative?.cmcFearGreed);
|
|
467
|
+
if (!cmcFearGreed) {
|
|
468
|
+
return {
|
|
469
|
+
source: null,
|
|
470
|
+
available: false,
|
|
471
|
+
interval: null,
|
|
472
|
+
asOfTs: null,
|
|
473
|
+
stale: null,
|
|
474
|
+
value: null,
|
|
475
|
+
valueChange24h: null,
|
|
476
|
+
valueChange7d: null,
|
|
477
|
+
classification: null,
|
|
478
|
+
sentimentRegime: null
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
source: String(cmcFearGreed.source ?? ""),
|
|
483
|
+
available: true,
|
|
484
|
+
interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
|
|
485
|
+
asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
|
|
486
|
+
stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
|
|
487
|
+
value: toFiniteNumber(cmcFearGreed.value),
|
|
488
|
+
valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
|
|
489
|
+
valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
|
|
490
|
+
classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
|
|
491
|
+
sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
var buildCmcIndexesContextFromSignal = (signal) => {
|
|
495
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
496
|
+
const relative = toRecord(baseContext?.relative);
|
|
497
|
+
const cmcIndexes = toRecord(relative?.cmcIndexes);
|
|
498
|
+
if (!cmcIndexes) {
|
|
499
|
+
return {
|
|
500
|
+
source: null,
|
|
501
|
+
available: false,
|
|
502
|
+
interval: null,
|
|
503
|
+
asOfTs: null,
|
|
504
|
+
stale: null,
|
|
505
|
+
cmc100Value: null,
|
|
506
|
+
cmc100Change24hPct: null,
|
|
507
|
+
cmc100TopConstituentSymbol: null,
|
|
508
|
+
cmc100TopConstituentWeightPct: null,
|
|
509
|
+
cmc20Value: null,
|
|
510
|
+
cmc20Change24hPct: null,
|
|
511
|
+
cmc20TopConstituentSymbol: null,
|
|
512
|
+
cmc20TopConstituentWeightPct: null,
|
|
513
|
+
cmc20ToCmc100Ratio: null,
|
|
514
|
+
cmc20ToCmc100RatioChange24hPct: null,
|
|
515
|
+
indexRegime: null
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
source: String(cmcIndexes.source ?? ""),
|
|
520
|
+
available: true,
|
|
521
|
+
interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
|
|
522
|
+
asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
|
|
523
|
+
stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
|
|
524
|
+
cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
|
|
525
|
+
cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
|
|
526
|
+
cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
|
|
527
|
+
cmc100TopConstituentWeightPct: toFiniteNumber(
|
|
528
|
+
cmcIndexes.cmc100TopConstituentWeightPct
|
|
529
|
+
),
|
|
530
|
+
cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
|
|
531
|
+
cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
|
|
532
|
+
cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
|
|
533
|
+
cmc20TopConstituentWeightPct: toFiniteNumber(
|
|
534
|
+
cmcIndexes.cmc20TopConstituentWeightPct
|
|
535
|
+
),
|
|
536
|
+
cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
|
|
537
|
+
cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
|
|
538
|
+
cmcIndexes.cmc20ToCmc100RatioChange24hPct
|
|
539
|
+
),
|
|
540
|
+
indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
var buildReferenceTradeFlowContextFromSignal = (signal) => {
|
|
544
|
+
const baseContext = toRecord(signal.additionalIndicators?.baseContext);
|
|
545
|
+
const relative = toRecord(baseContext?.relative);
|
|
546
|
+
const refs = toRecord(relative?.referenceTradeFlow);
|
|
547
|
+
const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
|
|
548
|
+
const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
|
|
549
|
+
const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
|
|
550
|
+
if (!refs) {
|
|
551
|
+
return {
|
|
552
|
+
source: null,
|
|
553
|
+
available: false,
|
|
554
|
+
primaryReferenceSymbol: null,
|
|
555
|
+
referenceSymbols: [],
|
|
556
|
+
primaryTradeFlowBuyPressurePct: null,
|
|
557
|
+
primaryTradeFlowStale: null
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
source: String(refs.source ?? ""),
|
|
562
|
+
available: true,
|
|
563
|
+
primaryReferenceSymbol,
|
|
564
|
+
referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
|
|
565
|
+
primaryTradeFlowBuyPressurePct: toFiniteNumber(
|
|
566
|
+
primaryTradeFlow?.buyPressurePct
|
|
567
|
+
),
|
|
568
|
+
primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
var buildAiMarketContext = (signal) => ({
|
|
572
|
+
execution: {
|
|
573
|
+
binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
|
|
574
|
+
},
|
|
575
|
+
participation: {
|
|
576
|
+
trueDelta: buildTrueDeltaContextFromSignal(signal),
|
|
577
|
+
tradeFlow: buildTradeFlowContextFromSignal(signal)
|
|
578
|
+
},
|
|
579
|
+
relative: {
|
|
580
|
+
marketBreadth: buildMarketBreadthContextFromSignal(signal),
|
|
581
|
+
marketBreadths: buildMarketBreadthsContextFromSignal(signal),
|
|
582
|
+
targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
|
|
583
|
+
btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
|
|
584
|
+
cmcGlobal: buildCmcGlobalContextFromSignal(signal),
|
|
585
|
+
cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
|
|
586
|
+
cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
|
|
587
|
+
cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
|
|
588
|
+
cmcIndexes: buildCmcIndexesContextFromSignal(signal),
|
|
589
|
+
referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// src/strategy/manifests.ts
|
|
594
|
+
import {
|
|
595
|
+
registerIndicatorEntries,
|
|
596
|
+
resetIndicatorRegistryCache
|
|
597
|
+
} from "@tradejs/core/indicators";
|
|
598
|
+
import { logger } from "@tradejs/infra/logger";
|
|
599
|
+
var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
|
|
600
|
+
var sharedRegistryScope = globalThis;
|
|
601
|
+
var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
|
|
602
|
+
registryStateByProjectRoot: /* @__PURE__ */ new Map()
|
|
603
|
+
});
|
|
604
|
+
var createStrategyRegistryState = () => ({
|
|
605
|
+
strategyCreators: /* @__PURE__ */ new Map(),
|
|
606
|
+
strategyManifestsMap: /* @__PURE__ */ new Map(),
|
|
607
|
+
strategyEntriesMap: /* @__PURE__ */ new Map(),
|
|
608
|
+
pluginsLoadPromise: null
|
|
609
|
+
});
|
|
610
|
+
var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
|
|
611
|
+
var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
|
|
612
|
+
const projectRoot = getTradejsProjectCwd(cwd);
|
|
613
|
+
let state = registryStateByProjectRoot.get(projectRoot);
|
|
614
|
+
if (!state) {
|
|
615
|
+
state = createStrategyRegistryState();
|
|
616
|
+
registryStateByProjectRoot.set(projectRoot, state);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
projectRoot,
|
|
620
|
+
state
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
var toUniqueModules = (modules = []) => [
|
|
624
|
+
...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
|
|
625
|
+
];
|
|
626
|
+
var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
|
|
627
|
+
const config = await loadTradejsConfig(cwd);
|
|
628
|
+
return {
|
|
629
|
+
strategyModules: toUniqueModules(config.strategies),
|
|
630
|
+
indicatorModules: toUniqueModules(config.indicators)
|
|
631
|
+
};
|
|
632
|
+
};
|
|
633
|
+
var extractModuleEntries = (moduleExport, key) => {
|
|
634
|
+
if (!moduleExport || typeof moduleExport !== "object") {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
const candidate = moduleExport;
|
|
638
|
+
if (Array.isArray(candidate[key])) {
|
|
639
|
+
return candidate[key];
|
|
640
|
+
}
|
|
641
|
+
const defaultExport = candidate.default;
|
|
642
|
+
if (defaultExport && Array.isArray(defaultExport[key])) {
|
|
643
|
+
return defaultExport[key];
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
};
|
|
647
|
+
var extractStrategyPluginDefinition = (moduleExport) => {
|
|
648
|
+
const strategyEntries = extractModuleEntries(
|
|
649
|
+
moduleExport,
|
|
650
|
+
"strategyEntries"
|
|
651
|
+
);
|
|
652
|
+
return strategyEntries ? { strategyEntries } : null;
|
|
653
|
+
};
|
|
654
|
+
var extractIndicatorPluginDefinition = (moduleExport) => {
|
|
655
|
+
const indicatorEntries = extractModuleEntries(
|
|
656
|
+
moduleExport,
|
|
657
|
+
"indicatorEntries"
|
|
658
|
+
);
|
|
659
|
+
return indicatorEntries ? { indicatorEntries } : null;
|
|
660
|
+
};
|
|
661
|
+
var registerEntries = (entries, source, state) => {
|
|
662
|
+
for (const entry of entries) {
|
|
663
|
+
const strategyName = entry.manifest?.name;
|
|
664
|
+
if (!strategyName) {
|
|
665
|
+
logger.warn("Skip strategy entry without name from %s", source);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (state.strategyCreators.has(strategyName)) {
|
|
669
|
+
logger.warn(
|
|
670
|
+
'Skip duplicate strategy "%s" from %s: already registered',
|
|
671
|
+
strategyName,
|
|
672
|
+
source
|
|
673
|
+
);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
state.strategyManifestsMap.set(strategyName, entry.manifest);
|
|
677
|
+
state.strategyEntriesMap.set(strategyName, entry);
|
|
678
|
+
materializeStrategyCreator(strategyName, state);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var materializeStrategyCreator = (strategyName, state) => {
|
|
682
|
+
if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const entry = state.strategyEntriesMap.get(strategyName);
|
|
686
|
+
if (!entry) return;
|
|
687
|
+
state.strategyCreators.set(
|
|
688
|
+
strategyName,
|
|
689
|
+
sharedStrategyRegistry.strategyRuntimeFactory({
|
|
690
|
+
strategyName,
|
|
691
|
+
defaults: entry.defaults,
|
|
692
|
+
createCore: entry.createCore,
|
|
693
|
+
manifest: entry.manifest,
|
|
694
|
+
detectorKey: entry.detectorKey,
|
|
695
|
+
detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
|
|
696
|
+
resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
};
|
|
700
|
+
var setStrategyRuntimeFactory = (factory) => {
|
|
701
|
+
sharedStrategyRegistry.strategyRuntimeFactory = factory;
|
|
702
|
+
for (const state of registryStateByProjectRoot.values()) {
|
|
703
|
+
for (const strategyName of state.strategyEntriesMap.keys()) {
|
|
704
|
+
materializeStrategyCreator(strategyName, state);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
|
|
709
|
+
if (typeof importTradejsModule === "function") {
|
|
710
|
+
return importTradejsModule(moduleName, cwd);
|
|
711
|
+
}
|
|
712
|
+
return import(
|
|
713
|
+
/* webpackIgnore: true */
|
|
714
|
+
moduleName
|
|
715
|
+
);
|
|
716
|
+
};
|
|
717
|
+
var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
|
|
718
|
+
const { projectRoot, state } = getStrategyRegistryState(cwd);
|
|
719
|
+
if (!state.pluginsLoadPromise) {
|
|
720
|
+
resetIndicatorRegistryCache(projectRoot);
|
|
721
|
+
state.pluginsLoadPromise = (async () => {
|
|
722
|
+
const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
|
|
723
|
+
const strategySet = new Set(strategyModules);
|
|
724
|
+
const indicatorSet = new Set(indicatorModules);
|
|
725
|
+
const pluginModuleNames = [
|
|
726
|
+
.../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
|
|
727
|
+
];
|
|
728
|
+
if (!pluginModuleNames.length) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
for (const moduleName of pluginModuleNames) {
|
|
732
|
+
try {
|
|
733
|
+
const resolvedModuleName = resolvePluginModuleSpecifier(
|
|
734
|
+
moduleName,
|
|
735
|
+
projectRoot
|
|
736
|
+
);
|
|
737
|
+
const moduleExport = await importStrategyPluginModule(
|
|
738
|
+
resolvedModuleName,
|
|
739
|
+
projectRoot
|
|
740
|
+
);
|
|
741
|
+
if (strategySet.has(moduleName)) {
|
|
742
|
+
const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
|
|
743
|
+
if (!pluginDefinition) {
|
|
744
|
+
logger.warn(
|
|
745
|
+
'Skip strategy plugin "%s": export { strategyEntries } is missing',
|
|
746
|
+
moduleName
|
|
747
|
+
);
|
|
748
|
+
} else {
|
|
749
|
+
registerEntries(
|
|
750
|
+
pluginDefinition.strategyEntries,
|
|
751
|
+
moduleName,
|
|
752
|
+
state
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (indicatorSet.has(moduleName)) {
|
|
757
|
+
const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
|
|
758
|
+
if (!indicatorPluginDefinition) {
|
|
759
|
+
logger.warn(
|
|
760
|
+
'Skip indicator plugin "%s": export { indicatorEntries } is missing',
|
|
761
|
+
moduleName
|
|
762
|
+
);
|
|
763
|
+
} else {
|
|
764
|
+
registerIndicatorEntries(
|
|
765
|
+
indicatorPluginDefinition.indicatorEntries,
|
|
766
|
+
moduleName,
|
|
767
|
+
projectRoot
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
|
|
772
|
+
logger.warn(
|
|
773
|
+
'Skip plugin "%s": no strategy/indicator sections requested in config',
|
|
774
|
+
moduleName
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
} catch (error) {
|
|
778
|
+
logger.warn(
|
|
779
|
+
'Failed to load plugin "%s": %s',
|
|
780
|
+
moduleName,
|
|
781
|
+
String(error)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
})();
|
|
786
|
+
}
|
|
787
|
+
await state.pluginsLoadPromise;
|
|
788
|
+
};
|
|
789
|
+
var ensureIndicatorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => ensureStrategyPluginsLoaded(cwd);
|
|
790
|
+
var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
|
|
791
|
+
await ensureStrategyPluginsLoaded(cwd);
|
|
792
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
793
|
+
return state.strategyCreators.get(name);
|
|
794
|
+
};
|
|
795
|
+
var getAvailableStrategyNames = async (cwd = getTradejsProjectCwd()) => {
|
|
796
|
+
await ensureStrategyPluginsLoaded(cwd);
|
|
797
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
798
|
+
return [...state.strategyCreators.keys()].sort((a, b) => a.localeCompare(b));
|
|
799
|
+
};
|
|
800
|
+
var getRegisteredStrategies = (cwd = getTradejsProjectCwd()) => {
|
|
801
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
802
|
+
return Object.fromEntries(state.strategyCreators.entries());
|
|
803
|
+
};
|
|
804
|
+
var getRegisteredManifests = (cwd = getTradejsProjectCwd()) => {
|
|
805
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
806
|
+
return [...state.strategyManifestsMap.values()];
|
|
807
|
+
};
|
|
808
|
+
var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
|
|
809
|
+
if (!name) {
|
|
810
|
+
return void 0;
|
|
811
|
+
}
|
|
812
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
813
|
+
return state.strategyManifestsMap.get(name);
|
|
814
|
+
};
|
|
815
|
+
var isKnownStrategy = (name, cwd = getTradejsProjectCwd()) => {
|
|
816
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
817
|
+
return state.strategyCreators.has(name);
|
|
818
|
+
};
|
|
819
|
+
var registerStrategyEntries = (entries, cwd = getTradejsProjectCwd()) => {
|
|
820
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
821
|
+
registerEntries(entries, "runtime", state);
|
|
822
|
+
};
|
|
823
|
+
var resetStrategyRegistryCache = (cwd) => {
|
|
824
|
+
const normalizedCwd = String(cwd ?? "").trim();
|
|
825
|
+
if (!normalizedCwd) {
|
|
826
|
+
registryStateByProjectRoot.clear();
|
|
827
|
+
resetIndicatorRegistryCache();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const projectRoot = getTradejsProjectCwd(normalizedCwd);
|
|
831
|
+
registryStateByProjectRoot.delete(projectRoot);
|
|
832
|
+
resetIndicatorRegistryCache(projectRoot);
|
|
833
|
+
};
|
|
834
|
+
var strategies = new Proxy(
|
|
835
|
+
{},
|
|
836
|
+
{
|
|
837
|
+
get: (_target, property) => {
|
|
838
|
+
if (typeof property !== "string") {
|
|
839
|
+
return void 0;
|
|
840
|
+
}
|
|
841
|
+
return getStrategyRegistryState().state.strategyCreators.get(property);
|
|
842
|
+
},
|
|
843
|
+
ownKeys: () => {
|
|
844
|
+
return [...getStrategyRegistryState().state.strategyCreators.keys()];
|
|
845
|
+
},
|
|
846
|
+
getOwnPropertyDescriptor: () => ({
|
|
847
|
+
enumerable: true,
|
|
848
|
+
configurable: true
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
// src/strategy/policyProfiles.ts
|
|
854
|
+
var profileMatches = (profile, universe, assetClass) => {
|
|
855
|
+
const { appliesTo } = profile;
|
|
856
|
+
if (!appliesTo) return true;
|
|
857
|
+
if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
return true;
|
|
864
|
+
};
|
|
865
|
+
var resolveStrategyPolicyProfile = (manifest, params) => {
|
|
866
|
+
const profiles = manifest?.policyProfiles ?? [];
|
|
867
|
+
if (!profiles.length) {
|
|
868
|
+
const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
|
|
869
|
+
if (!inferredId) return void 0;
|
|
870
|
+
if (inferredId !== "crypto" && inferredId !== "tradfi") {
|
|
871
|
+
throw new Error(
|
|
872
|
+
`Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (params.universe && inferredId !== params.universe) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Policy profile "${inferredId}" is not compatible with ${params.universe}`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
id: inferredId,
|
|
882
|
+
appliesTo: { universes: [inferredId] },
|
|
883
|
+
marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
|
|
884
|
+
...manifest?.mlAdapter ? {
|
|
885
|
+
entryRuntimeDefaults: {
|
|
886
|
+
ml: {
|
|
887
|
+
modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} : {}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (params.profileId) {
|
|
894
|
+
const profile = profiles.find(({ id }) => id === params.profileId);
|
|
895
|
+
if (!profile) {
|
|
896
|
+
throw new Error(
|
|
897
|
+
`Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
if (!profileMatches(profile, params.universe, params.assetClass)) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
return profile;
|
|
906
|
+
}
|
|
907
|
+
const matching = profiles.filter(
|
|
908
|
+
(profile) => profileMatches(profile, params.universe, params.assetClass)
|
|
909
|
+
);
|
|
910
|
+
const defaultProfile = matching.find(
|
|
911
|
+
({ id }) => id === manifest?.defaultPolicyProfileId
|
|
912
|
+
);
|
|
913
|
+
return defaultProfile ?? matching[0];
|
|
914
|
+
};
|
|
915
|
+
var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
|
|
916
|
+
var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
|
|
917
|
+
|
|
918
|
+
// src/strategyAdapters/ai.ts
|
|
919
|
+
var toRecord2 = (value) => {
|
|
920
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
921
|
+
return {};
|
|
922
|
+
}
|
|
923
|
+
return value;
|
|
924
|
+
};
|
|
925
|
+
var buildBaseAiPayload = (signal) => {
|
|
926
|
+
const additionalIndicators = {
|
|
927
|
+
...toRecord2(signal.additionalIndicators),
|
|
928
|
+
marketContext: buildAiMarketContext(signal)
|
|
929
|
+
};
|
|
930
|
+
return {
|
|
931
|
+
signal: {
|
|
932
|
+
symbol: signal.symbol,
|
|
933
|
+
signalId: signal.signalId,
|
|
934
|
+
interval: signal.interval,
|
|
935
|
+
direction: signal.direction,
|
|
936
|
+
timestamp: signal.timestamp,
|
|
937
|
+
strategy: signal.strategy,
|
|
938
|
+
prices: {
|
|
939
|
+
currentPrice: signal.prices.currentPrice,
|
|
940
|
+
takeProfitPrice: signal.prices.takeProfitPrice,
|
|
941
|
+
stopLossPrice: signal.prices.stopLossPrice
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
figures: trimSeriesDeep(signal.figures ?? {}),
|
|
945
|
+
indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
|
|
946
|
+
additionalIndicators: trimSeriesDeep(additionalIndicators)
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
var defaultAiAdapter = {};
|
|
950
|
+
var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
|
|
951
|
+
var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
|
|
952
|
+
var buildAiPayloadByStrategy = (signal) => {
|
|
953
|
+
const basePayload = buildBaseAiPayload(signal);
|
|
954
|
+
const adapter = getSignalAiAdapter(signal);
|
|
955
|
+
return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
|
|
956
|
+
};
|
|
957
|
+
var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
|
|
958
|
+
var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
|
|
959
|
+
signal,
|
|
960
|
+
payload
|
|
961
|
+
}) ?? "";
|
|
962
|
+
var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
|
|
963
|
+
signal,
|
|
964
|
+
payload,
|
|
965
|
+
analysis
|
|
966
|
+
}) ?? analysis;
|
|
967
|
+
var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
|
|
968
|
+
const adapter = getSignalAiAdapter(signal);
|
|
969
|
+
const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
|
|
970
|
+
return adapter.postProcessLocalAnalysis?.({
|
|
971
|
+
signal,
|
|
972
|
+
payload,
|
|
973
|
+
analysis: strategyAnalysis
|
|
974
|
+
}) ?? strategyAnalysis;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// src/ai.ts
|
|
978
|
+
var parseAIResponse = (input) => {
|
|
979
|
+
try {
|
|
980
|
+
if (typeof input === "object" && input !== null) return input;
|
|
981
|
+
const match = input.match(/\{[\s\S]*\}/);
|
|
982
|
+
if (!match) throw new Error("JSON block not found");
|
|
983
|
+
return JSON.parse(match[0]);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
console.error("Failed to parse AI response:", err);
|
|
986
|
+
console.log("Raw AI response:", input);
|
|
987
|
+
return {};
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
var normalizeResponseContent = (content) => {
|
|
991
|
+
if (typeof content === "string" || content && typeof content === "object") {
|
|
992
|
+
if (typeof content !== "object" || !Array.isArray(content)) {
|
|
993
|
+
return content;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (Array.isArray(content)) {
|
|
997
|
+
const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
|
|
998
|
+
return text;
|
|
999
|
+
}
|
|
1000
|
+
return String(content ?? "");
|
|
1001
|
+
};
|
|
1002
|
+
var normalizeAnalysis = (raw) => {
|
|
1003
|
+
const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
|
|
1004
|
+
const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
|
|
1005
|
+
const toNumberOrNull = (value) => {
|
|
1006
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1007
|
+
if (typeof value === "string" && value.trim()) {
|
|
1008
|
+
const parsed = Number(value);
|
|
1009
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1010
|
+
}
|
|
1011
|
+
return null;
|
|
1012
|
+
};
|
|
1013
|
+
const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
|
|
1014
|
+
return {
|
|
1015
|
+
direction,
|
|
1016
|
+
quality: qualityNum,
|
|
1017
|
+
needRetest: Boolean(raw?.needRetest),
|
|
1018
|
+
retestPrice: toNumberOrNull(raw?.retestPrice),
|
|
1019
|
+
takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
|
|
1020
|
+
stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
|
|
1021
|
+
setup: toText(raw?.setup),
|
|
1022
|
+
confirmations: toText(raw?.confirmations),
|
|
1023
|
+
btcContext: toText(raw?.btcContext),
|
|
1024
|
+
retestPlan: toText(raw?.retestPlan),
|
|
1025
|
+
riskLevels: toText(raw?.riskLevels),
|
|
1026
|
+
qualityReason: toText(raw?.qualityReason),
|
|
1027
|
+
triggerInvalidation: toText(raw?.triggerInvalidation),
|
|
1028
|
+
comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
|
|
1029
|
+
};
|
|
1030
|
+
};
|
|
1031
|
+
var asRecord = (value) => {
|
|
1032
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
};
|
|
1037
|
+
var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
|
|
1038
|
+
var getDeterministicQuality = (gateContext) => {
|
|
1039
|
+
const deterministicQuality = Number(gateContext?.deterministicQuality);
|
|
1040
|
+
if (Number.isFinite(deterministicQuality)) {
|
|
1041
|
+
return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
|
|
1042
|
+
}
|
|
1043
|
+
const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
|
|
1044
|
+
if (Number.isFinite(maxAllowedQuality)) {
|
|
1045
|
+
return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
|
|
1046
|
+
}
|
|
1047
|
+
return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
|
|
1048
|
+
};
|
|
1049
|
+
var buildAiSystemPrompt = (signal) => `
|
|
1050
|
+
You are an internal market-structure classifier for an already computed system signal.
|
|
1051
|
+
Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
|
|
1052
|
+
Series data is already trimmed to the latest 5 values.
|
|
1053
|
+
|
|
1054
|
+
Important:
|
|
1055
|
+
- Do not invent missing data.
|
|
1056
|
+
- This is an internal audit/classification task, not user-facing trading advice.
|
|
1057
|
+
- Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
|
|
1058
|
+
- Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
|
|
1059
|
+
- Respect the source strategy specified in \`signal.strategy\`.
|
|
1060
|
+
- Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
|
|
1061
|
+
- Do not write vague statements like "there is momentum/slope" without tying them to the decision.
|
|
1062
|
+
- Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
|
|
1063
|
+
- If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
|
|
1064
|
+
|
|
1065
|
+
Return exactly one JSON object and nothing else:
|
|
1066
|
+
|
|
1067
|
+
{
|
|
1068
|
+
"direction": payload.signal.direction | null,
|
|
1069
|
+
"quality": 1 | 2 | 3 | 4 | 5,
|
|
1070
|
+
"needRetest": boolean,
|
|
1071
|
+
"retestPrice": number | null,
|
|
1072
|
+
"takeProfitPrice": number | null,
|
|
1073
|
+
"stopLossPrice": number | null,
|
|
1074
|
+
"setup": string,
|
|
1075
|
+
"confirmations": string,
|
|
1076
|
+
"btcContext": string,
|
|
1077
|
+
"retestPlan": string,
|
|
1078
|
+
"riskLevels": string,
|
|
1079
|
+
"qualityReason": string,
|
|
1080
|
+
"triggerInvalidation": string
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
- Do not add any other fields.
|
|
1084
|
+
- All numbers must be finite, with no \`NaN\` or \`Infinity\`.
|
|
1085
|
+
- All text fields must be short strings with no line breaks and no markdown lists.
|
|
1086
|
+
- \`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.
|
|
1087
|
+
- \`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.
|
|
1088
|
+
- \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
|
|
1089
|
+
- \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
|
|
1090
|
+
- \`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\`.
|
|
1091
|
+
- Use these fields as separate parts of the analysis:
|
|
1092
|
+
- \`setup\`: the current structural setup or trendline state.
|
|
1093
|
+
- \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
|
|
1094
|
+
- \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
|
|
1095
|
+
- \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
|
|
1096
|
+
- \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
|
|
1097
|
+
- \`qualityReason\`: why the quality score is what it is.
|
|
1098
|
+
- \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
|
|
1099
|
+
- \`comment\` is optional. If you include it, do not just duplicate the structured fields.
|
|
1100
|
+
|
|
1101
|
+
If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
|
|
1102
|
+
|
|
1103
|
+
Input payload structure:
|
|
1104
|
+
- payload.signal:
|
|
1105
|
+
symbol, signalId, interval, direction, timestamp, strategy, prices
|
|
1106
|
+
- payload.signal.prices:
|
|
1107
|
+
currentPrice, takeProfitPrice, stopLossPrice
|
|
1108
|
+
- payload.figures:
|
|
1109
|
+
strategy-specific figures or geometry when available. Fields vary by strategy.
|
|
1110
|
+
- payload.indicators:
|
|
1111
|
+
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.
|
|
1112
|
+
- payload.additionalIndicators:
|
|
1113
|
+
strategy-specific summary/context fields plus the canonical current shared context snapshot.
|
|
1114
|
+
This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
|
|
1115
|
+
Examples: baseContext, helperFlags, structureContext, volatilitySummary.
|
|
1116
|
+
Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
|
|
1117
|
+
\u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
|
|
1118
|
+
\u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
|
|
1119
|
+
\u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
|
|
1120
|
+
\u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, Binance aggTrades, and fingerprinted Hyperliquid whale-flow when available.
|
|
1121
|
+
\u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
|
|
1122
|
+
\u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
|
|
1123
|
+
\u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
|
|
1124
|
+
\u2022 \`baseContext.gateFeatures\`: compact direction-aware fields derived from baseContext; prefer \`setup\`, \`scores\`, \`conflicts\`, \`risk\`, \`decisionHints\`, \`mtf\`, \`volatility\`, \`participation\`, and \`relative\` for quick gate checks before inspecting raw nested context.
|
|
1125
|
+
Always inspect \`payload.additionalIndicators.marketContext\` when present:
|
|
1126
|
+
\u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
|
|
1127
|
+
\u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
|
|
1128
|
+
\u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
|
|
1129
|
+
\u2022 \`marketContext.relative.marketBreadths.top5|top10|top30|top50|top100\`: equal/volume-weighted alt-basket return, advance/decline ratio, and MA breadth for the five versioned Binance breadth universes. \`marketBreadth\` remains the top30 primary view used by existing gates.
|
|
1130
|
+
\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.
|
|
1131
|
+
\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.
|
|
1132
|
+
\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\`.
|
|
1133
|
+
\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\`.
|
|
1134
|
+
\u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
|
|
1135
|
+
\u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
|
|
1136
|
+
\u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
|
|
1137
|
+
\u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
|
|
1138
|
+
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.
|
|
1139
|
+
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.
|
|
1140
|
+
Key patterns:
|
|
1141
|
+
\u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
|
|
1142
|
+
\u2022 recent historical series: \`payload.indicators\`
|
|
1143
|
+
\u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
|
|
1144
|
+
|
|
1145
|
+
How to analyze, in order:
|
|
1146
|
+
1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
|
|
1147
|
+
2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
|
|
1148
|
+
3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
|
|
1149
|
+
4. Then evaluate BTC context.
|
|
1150
|
+
5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
|
|
1151
|
+
6. If strong conflicts exist, reduce quality or set direction to \`null\`.
|
|
1152
|
+
|
|
1153
|
+
Explicit conflict rules:
|
|
1154
|
+
- If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
|
|
1155
|
+
- If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
|
|
1156
|
+
- If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
|
|
1157
|
+
- 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.
|
|
1158
|
+
- 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.
|
|
1159
|
+
- 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.
|
|
1160
|
+
- 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.
|
|
1161
|
+
- 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\`.
|
|
1162
|
+
- 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.
|
|
1163
|
+
- 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.
|
|
1164
|
+
- 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.
|
|
1165
|
+
- 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.
|
|
1166
|
+
- 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.
|
|
1167
|
+
- 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.
|
|
1168
|
+
- 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.
|
|
1169
|
+
- 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.
|
|
1170
|
+
- 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.
|
|
1171
|
+
- 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.
|
|
1172
|
+
- 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.
|
|
1173
|
+
- 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.
|
|
1174
|
+
- 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.
|
|
1175
|
+
- If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
|
|
1176
|
+
If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
|
|
1177
|
+
|
|
1178
|
+
Rules for \`direction\` / TP / SL:
|
|
1179
|
+
- \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
|
|
1180
|
+
- For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
|
|
1181
|
+
- For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
|
|
1182
|
+
- Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
|
|
1183
|
+
- If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
|
|
1184
|
+
- If \`needRetest = false\`, then \`retestPrice = null\`.
|
|
1185
|
+
- If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
|
|
1186
|
+
- Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
|
|
1187
|
+
|
|
1188
|
+
Quality scale:
|
|
1189
|
+
- 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
|
|
1190
|
+
- 2: weak setup, few confirmations, more of a watch or reject
|
|
1191
|
+
- 3: average setup, some structure exists, but notable conflicts remain
|
|
1192
|
+
- 4: good setup, several confirmations, structure is mostly coherent
|
|
1193
|
+
- 5: very strong setup, clean structure, confirmations, and internally coherent levels
|
|
1194
|
+
|
|
1195
|
+
Requirements for useful structured analysis:
|
|
1196
|
+
- Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
|
|
1197
|
+
- Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
|
|
1198
|
+
- Explicitly mention BTC context as supportive, neutral, or conflicting.
|
|
1199
|
+
- Explain why the quality score is what it is.
|
|
1200
|
+
- If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
|
|
1201
|
+
- In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
|
|
1202
|
+
- Do not simply restate JSON fields; add interpretation and decision logic.
|
|
1203
|
+
|
|
1204
|
+
Rules for using trimmed series (last 5 values):
|
|
1205
|
+
- Do not make strong long-term conclusions from only 5 points.
|
|
1206
|
+
- Use 4h and 1d series as brief context, not full history.
|
|
1207
|
+
- If the data is too limited for confidence, reduce quality and use cautious wording.
|
|
1208
|
+
|
|
1209
|
+
Short few-shot examples:
|
|
1210
|
+
{"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."}
|
|
1211
|
+
{"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."}
|
|
1212
|
+
|
|
1213
|
+
Return only the JSON object, with no extra characters.
|
|
1214
|
+
${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
|
|
1215
|
+
`;
|
|
1216
|
+
var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
|
|
1217
|
+
var getDeterministicAiGateContext = (payload) => {
|
|
1218
|
+
const additionalIndicators = asRecord(payload.additionalIndicators);
|
|
1219
|
+
const candidates = [
|
|
1220
|
+
additionalIndicators,
|
|
1221
|
+
...Object.values(additionalIndicators ?? {}).map(asRecord)
|
|
1222
|
+
].filter((value) => Boolean(value));
|
|
1223
|
+
return candidates.find(
|
|
1224
|
+
(candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
|
|
1225
|
+
) ?? null;
|
|
1226
|
+
};
|
|
1227
|
+
var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
|
|
1228
|
+
Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
|
|
1229
|
+
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.
|
|
1230
|
+
|
|
1231
|
+
Trade payload:
|
|
1232
|
+
${JSON.stringify(payload)}
|
|
1233
|
+
${buildAiHumanPromptAddonByStrategy(signal, payload)}
|
|
1234
|
+
`;
|
|
1235
|
+
var getAiInvocationError = (error) => {
|
|
1236
|
+
const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
|
|
1237
|
+
const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
|
|
1238
|
+
details
|
|
1239
|
+
);
|
|
1240
|
+
const wrapped = new Error(
|
|
1241
|
+
isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
|
|
1242
|
+
);
|
|
1243
|
+
wrapped.cause = error;
|
|
1244
|
+
return wrapped;
|
|
1245
|
+
};
|
|
1246
|
+
var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
|
|
1247
|
+
var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
|
|
1248
|
+
var userSettingsCache = /* @__PURE__ */ new Map();
|
|
1249
|
+
var aiModelCache = /* @__PURE__ */ new Map();
|
|
1250
|
+
var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
|
|
1251
|
+
var resolveAiModelName = (settings, requestedModelName) => {
|
|
1252
|
+
const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
|
|
1253
|
+
if (explicitModelName) {
|
|
1254
|
+
return explicitModelName;
|
|
1255
|
+
}
|
|
1256
|
+
const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
|
|
1257
|
+
return settingsModelName || DEFAULT_AI_MODEL;
|
|
1258
|
+
};
|
|
1259
|
+
var getOpenRouterModelKwargs = (apiEndpoint) => {
|
|
1260
|
+
const endpoint = String(apiEndpoint ?? "").trim();
|
|
1261
|
+
if (!endpoint) {
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
let hostname = "";
|
|
1265
|
+
try {
|
|
1266
|
+
hostname = new URL(endpoint).hostname;
|
|
1267
|
+
} catch {
|
|
1268
|
+
hostname = endpoint;
|
|
1269
|
+
}
|
|
1270
|
+
if (!hostname.toLowerCase().includes("openrouter")) {
|
|
1271
|
+
return {};
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
provider: {
|
|
1275
|
+
ignore: ["azure"]
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
};
|
|
1279
|
+
var getAiSettings = async (userName = "root") => {
|
|
1280
|
+
let settingsPromise = userSettingsCache.get(userName);
|
|
1281
|
+
if (!settingsPromise) {
|
|
1282
|
+
settingsPromise = getUserSettings(userName).then((settings2) => {
|
|
1283
|
+
const endpoint = normalizeAiEndpoint(settings2.AI_API_ENDPOINT);
|
|
1284
|
+
return {
|
|
1285
|
+
...settings2,
|
|
1286
|
+
AI_API_ENDPOINT: endpoint,
|
|
1287
|
+
AI_MODEL: normalizeAiModel(settings2.AI_MODEL, endpoint),
|
|
1288
|
+
AI_RESPONSE_LANGUAGE: normalizeAiResponseLanguage(
|
|
1289
|
+
settings2.AI_RESPONSE_LANGUAGE
|
|
1290
|
+
)
|
|
1291
|
+
};
|
|
1292
|
+
});
|
|
1293
|
+
settingsPromise.catch(() => {
|
|
1294
|
+
userSettingsCache.delete(userName);
|
|
1295
|
+
});
|
|
1296
|
+
userSettingsCache.set(userName, settingsPromise);
|
|
1297
|
+
}
|
|
1298
|
+
const settings = await settingsPromise;
|
|
1299
|
+
if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
|
|
1300
|
+
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
1301
|
+
}
|
|
1302
|
+
return settings;
|
|
1303
|
+
};
|
|
1304
|
+
var createAiModel = async (userName = "root", requestedModelName) => {
|
|
1305
|
+
const settings = await getAiSettings(userName);
|
|
1306
|
+
const modelName = resolveAiModelName(settings, requestedModelName);
|
|
1307
|
+
const cacheKey = getAiModelCacheKey(userName, modelName);
|
|
1308
|
+
let modelPromise = aiModelCache.get(cacheKey);
|
|
1309
|
+
if (!modelPromise) {
|
|
1310
|
+
modelPromise = (async () => {
|
|
1311
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
1312
|
+
const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
|
|
1313
|
+
return new ChatOpenAI({
|
|
1314
|
+
temperature: 0.2,
|
|
1315
|
+
modelName,
|
|
1316
|
+
apiKey: settings.AI_API_KEY,
|
|
1317
|
+
...Object.keys(modelKwargs).length ? { modelKwargs } : {},
|
|
1318
|
+
configuration: {
|
|
1319
|
+
baseURL: settings.AI_API_ENDPOINT,
|
|
1320
|
+
defaultHeaders: {
|
|
1321
|
+
"HTTP-Referer": "https://tradejs.dev",
|
|
1322
|
+
"X-Title": "Inv"
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
})();
|
|
1327
|
+
modelPromise.catch(() => {
|
|
1328
|
+
aiModelCache.delete(cacheKey);
|
|
1329
|
+
});
|
|
1330
|
+
aiModelCache.set(cacheKey, modelPromise);
|
|
1331
|
+
}
|
|
1332
|
+
return modelPromise;
|
|
1333
|
+
};
|
|
1334
|
+
var getAiModel = async (userName = "root", requestedModelName) => {
|
|
1335
|
+
const settings = await getAiSettings(userName);
|
|
1336
|
+
const resolvedModelName = resolveAiModelName(settings, requestedModelName);
|
|
1337
|
+
try {
|
|
1338
|
+
return await createAiModel(userName, resolvedModelName);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
|
|
1341
|
+
userSettingsCache.delete(userName);
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
var resetAiRuntimeCache = () => {
|
|
1346
|
+
aiModelCache.clear();
|
|
1347
|
+
userSettingsCache.clear();
|
|
1348
|
+
};
|
|
1349
|
+
var buildAiPrompts = (signal) => {
|
|
1350
|
+
const payload = buildAiPayload(signal);
|
|
1351
|
+
return {
|
|
1352
|
+
systemPrompt: buildAiSystemPrompt(signal),
|
|
1353
|
+
humanPrompt: buildAiHumanPrompt(signal, payload)
|
|
1354
|
+
};
|
|
1355
|
+
};
|
|
1356
|
+
var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
|
|
1357
|
+
const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
|
|
1358
|
+
import("@langchain/core/messages"),
|
|
1359
|
+
getAiModel(options.userName, options.model),
|
|
1360
|
+
getAiSettings(options.userName)
|
|
1361
|
+
]);
|
|
1362
|
+
const messages = [];
|
|
1363
|
+
const responseLanguage = getAiResponseLanguagePromptName(
|
|
1364
|
+
settings.AI_RESPONSE_LANGUAGE || DEFAULT_AI_RESPONSE_LANGUAGE
|
|
1365
|
+
);
|
|
1366
|
+
messages.push(new SystemMessage(systemPrompt));
|
|
1367
|
+
messages.push(
|
|
1368
|
+
new SystemMessage(
|
|
1369
|
+
`Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
|
|
1370
|
+
)
|
|
1371
|
+
);
|
|
1372
|
+
messages.push(
|
|
1373
|
+
new HumanMessage({
|
|
1374
|
+
content: [
|
|
1375
|
+
{
|
|
1376
|
+
type: "text",
|
|
1377
|
+
text: humanPrompt
|
|
1378
|
+
}
|
|
1379
|
+
]
|
|
1380
|
+
})
|
|
1381
|
+
);
|
|
1382
|
+
let response;
|
|
1383
|
+
try {
|
|
1384
|
+
response = await model.invoke(messages);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
throw getAiInvocationError(error);
|
|
1387
|
+
}
|
|
1388
|
+
const responseContent = normalizeResponseContent(response?.content);
|
|
1389
|
+
if (isEmptyResponseContent(responseContent)) {
|
|
1390
|
+
throw new Error("AI provider returned an empty chat completion");
|
|
1391
|
+
}
|
|
1392
|
+
const parsed = parseAIResponse(responseContent);
|
|
1393
|
+
const normalized = normalizeAnalysis(parsed);
|
|
1394
|
+
if (!options.signal) {
|
|
1395
|
+
return normalized;
|
|
1396
|
+
}
|
|
1397
|
+
return postProcessAiAnalysisByStrategy(
|
|
1398
|
+
options.signal,
|
|
1399
|
+
normalized,
|
|
1400
|
+
options.payload
|
|
1401
|
+
);
|
|
1402
|
+
};
|
|
1403
|
+
var runAiPromptLocal = async (signal, options = {}) => {
|
|
1404
|
+
const payload = options.payload ?? buildAiPayload(signal);
|
|
1405
|
+
const gateContext = getDeterministicAiGateContext(payload);
|
|
1406
|
+
const signalDirection = getSignalDirection(signal);
|
|
1407
|
+
const deterministicQuality = getDeterministicQuality(gateContext);
|
|
1408
|
+
const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
|
|
1409
|
+
return postProcessLocalAiAnalysisByStrategy(
|
|
1410
|
+
signal,
|
|
1411
|
+
{
|
|
1412
|
+
direction: approvalAllowedNow ? signalDirection : null,
|
|
1413
|
+
quality: deterministicQuality,
|
|
1414
|
+
needRetest: !approvalAllowedNow,
|
|
1415
|
+
retestPrice: null,
|
|
1416
|
+
takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
|
|
1417
|
+
stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
|
|
1418
|
+
},
|
|
1419
|
+
payload
|
|
1420
|
+
);
|
|
1421
|
+
};
|
|
1422
|
+
var askAI = async (signal, options = {}) => {
|
|
1423
|
+
const { symbol } = signal;
|
|
1424
|
+
const payload = buildAiPayload(signal);
|
|
1425
|
+
const content = await runAiPrompt(
|
|
1426
|
+
{
|
|
1427
|
+
systemPrompt: buildAiSystemPrompt(signal),
|
|
1428
|
+
humanPrompt: buildAiHumanPrompt(signal, payload)
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
...options,
|
|
1432
|
+
signal,
|
|
1433
|
+
payload
|
|
1434
|
+
}
|
|
1435
|
+
);
|
|
1436
|
+
await setData(redisKeys.analysis(symbol, signal.signalId), content);
|
|
1437
|
+
return content;
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
export {
|
|
1441
|
+
MAX_AI_SERIES_POINTS,
|
|
1442
|
+
trimSeriesDeep,
|
|
1443
|
+
buildCompactAiIndicatorsSnapshot,
|
|
1444
|
+
setStrategyRuntimeFactory,
|
|
1445
|
+
ensureStrategyPluginsLoaded,
|
|
1446
|
+
ensureIndicatorPluginsLoaded,
|
|
1447
|
+
getStrategyCreator,
|
|
1448
|
+
getAvailableStrategyNames,
|
|
1449
|
+
getRegisteredStrategies,
|
|
1450
|
+
getRegisteredManifests,
|
|
1451
|
+
getStrategyManifest,
|
|
1452
|
+
isKnownStrategy,
|
|
1453
|
+
registerStrategyEntries,
|
|
1454
|
+
resetStrategyRegistryCache,
|
|
1455
|
+
strategies,
|
|
1456
|
+
resolveStrategyPolicyProfile,
|
|
1457
|
+
getStrategyProfileMlAdapter,
|
|
1458
|
+
buildAiSystemPrompt,
|
|
1459
|
+
buildAiPayload,
|
|
1460
|
+
getDeterministicAiGateContext,
|
|
1461
|
+
buildAiHumanPrompt,
|
|
1462
|
+
DEFAULT_AI_MODEL,
|
|
1463
|
+
getOpenRouterModelKwargs,
|
|
1464
|
+
resetAiRuntimeCache,
|
|
1465
|
+
buildAiPrompts,
|
|
1466
|
+
runAiPrompt,
|
|
1467
|
+
runAiPromptLocal,
|
|
1468
|
+
askAI
|
|
1469
|
+
};
|