@tradejs/infra 1.0.6 → 1.0.8

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.
@@ -1,6 +1,8 @@
1
1
  // src/redis.ts
2
+ import { randomBytes } from "crypto";
2
3
  import Redis from "ioredis";
3
4
  var TTL_1D = 86400;
5
+ var SCREENSHOT_TOKEN_TTL_SECONDS = 15 * 60;
4
6
  var toJson = (value) => JSON.stringify(value);
5
7
  var logger = {
6
8
  log: (level, message, ...args) => {
@@ -256,6 +258,36 @@ var setData = async (key, data, options = {}) => {
256
258
  }
257
259
  }
258
260
  };
261
+ var createScreenshotSessionToken = async (userName) => {
262
+ const token = randomBytes(24).toString("hex");
263
+ await setData(
264
+ redisKeys.screenshotSessionToken(token),
265
+ {
266
+ userName,
267
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
268
+ },
269
+ {
270
+ expire: SCREENSHOT_TOKEN_TTL_SECONDS
271
+ }
272
+ );
273
+ return token;
274
+ };
275
+ var consumeScreenshotSessionToken = async (token) => {
276
+ const normalizedToken = token.trim();
277
+ if (!normalizedToken) {
278
+ return null;
279
+ }
280
+ const key = redisKeys.screenshotSessionToken(normalizedToken);
281
+ const payload = await getData(
282
+ key,
283
+ null
284
+ );
285
+ if (!payload?.userName) {
286
+ return null;
287
+ }
288
+ await delKey(key);
289
+ return payload.userName;
290
+ };
259
291
  var redisKeys = {
260
292
  users: () => "users:index:",
261
293
  user: (userName) => `users:index:${userName}`,
@@ -270,14 +302,28 @@ var redisKeys = {
270
302
  testOrders: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:orders`,
271
303
  testConfig: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:config`,
272
304
  testStat: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:stat`,
305
+ testSummaries: (userName) => `users:${userName}:tests:index:summary`,
273
306
  cacheChunk: (userName, chunkId) => `users:${userName}:cache:tests:chunks:${chunkId}`,
274
307
  cacheOrders: (userName, orderLogId) => `users:${userName}:cache:tests:orders:${orderLogId}`,
275
308
  cachePositions: (userName, orderLogId) => `users:${userName}:cache:tests:positions:${orderLogId}`,
276
309
  signal: (symbol, signalId) => `signals:${symbol}:${signalId}`,
277
310
  signalsBySymbol: (symbol) => `signals:${symbol}:`,
278
311
  storeSignal: (symbol, signalId) => `store:signals:${symbol}:${signalId}`,
312
+ runtimeSignals: (userName) => `users:${userName}:runtime:signals:`,
313
+ runtimeSignal: (userName, signalId) => `users:${userName}:runtime:signals:${signalId}`,
314
+ runtimeSignalEvaluations: (userName) => `users:${userName}:runtime:signal-evaluations:`,
315
+ runtimeSignalEvaluation: (userName, evaluationId) => `users:${userName}:runtime:signal-evaluations:${evaluationId}`,
316
+ runtimeTrades: (userName) => `users:${userName}:runtime:trade-records:`,
317
+ runtimeTrade: (userName, orderId) => `users:${userName}:runtime:trade-records:${orderId}`,
318
+ runtimeActiveTrades: (userName) => `users:${userName}:runtime:active-trades:`,
319
+ runtimeActiveTrade: (userName, symbol) => `users:${userName}:runtime:active-trades:${symbol}`,
320
+ aiChatHistory: (userName, symbolKey) => `users:${userName}:ai:chats:${symbolKey}`,
279
321
  analysis: (symbol, signalId) => `analysis:${symbol}:${signalId}`,
322
+ screenshotSessionToken: (token) => `auth:screenshot:${token}`,
280
323
  backtestResults: (userName, config, timestamp) => `users:${userName}:backtests:results:${config}:${timestamp}`,
324
+ researchRuns: (userName) => `users:${userName}:research:runs:`,
325
+ researchRun: (userName, runId) => `users:${userName}:research:runs:${runId}`,
326
+ researchLatestRun: (userName, strategyName) => `users:${userName}:research:latest:${strategyName}`,
281
327
  mlSignalsByStrategy: (strategyName) => `ml:${strategyName}:signals:`,
282
328
  mlSignals: () => "ml:",
283
329
  mlSignal: (strategyName, signalId) => `ml:${strategyName}:signals:${signalId}`,
@@ -293,5 +339,7 @@ export {
293
339
  RedisWriteBlockedError,
294
340
  delKeyWithOptions,
295
341
  setData,
342
+ createScreenshotSessionToken,
343
+ consumeScreenshotSessionToken,
296
344
  redisKeys
297
345
  };
@@ -0,0 +1,163 @@
1
+ import {
2
+ AI_CUSTOM_ENDPOINT_VALUE
3
+ } from "./chunk-XQ3YBULV.mjs";
4
+
5
+ // src/aiModels.ts
6
+ var AI_CUSTOM_MODEL_VALUE = "__custom_model__";
7
+ var OPENAI_ENDPOINT = "https://api.openai.com/v1";
8
+ var ANTHROPIC_ENDPOINT = "https://api.anthropic.com/v1";
9
+ var OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1";
10
+ var GEMINI_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/openai";
11
+ var TOGETHER_ENDPOINT = "https://api.together.xyz/v1";
12
+ var GROQ_ENDPOINT = "https://api.groq.com/openai/v1";
13
+ var XAI_ENDPOINT = "https://api.x.ai/v1";
14
+ var QWEN_INTL_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
15
+ var QWEN_CN_ENDPOINT = "https://dashscope.aliyuncs.com/compatible-mode/v1";
16
+ var QWEN_US_ENDPOINT = "https://dashscope-us.aliyuncs.com/compatible-mode/v1";
17
+ var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai";
18
+ var KIMI_ENDPOINT = "https://api.moonshot.ai/v1";
19
+ var PROXY_API_ENDPOINT = "https://openai.api.proxyapi.ru/v1";
20
+ var AI_MODEL_OPTIONS_BY_ENDPOINT = {
21
+ [OPENAI_ENDPOINT]: [
22
+ { label: "GPT-5 mini", value: "gpt-5-mini" },
23
+ { label: "GPT-5", value: "gpt-5" },
24
+ { label: "GPT-5.2", value: "gpt-5.2" },
25
+ { label: "GPT-4.1", value: "gpt-4.1" },
26
+ { label: "GPT-4o", value: "gpt-4o" }
27
+ ],
28
+ [ANTHROPIC_ENDPOINT]: [
29
+ { label: "Claude Sonnet 4", value: "claude-sonnet-4-20250514" },
30
+ { label: "Claude Opus 4.1", value: "claude-opus-4-1-20250805" },
31
+ { label: "Claude Opus 4", value: "claude-opus-4-20250514" },
32
+ { label: "Claude 3.7 Sonnet", value: "claude-3-7-sonnet-20250219" },
33
+ { label: "Claude 3.5 Haiku", value: "claude-3-5-haiku-20241022" }
34
+ ],
35
+ [OPENROUTER_ENDPOINT]: [
36
+ { label: "OpenAI GPT-5", value: "openai/gpt-5" },
37
+ { label: "Anthropic Claude Sonnet 4", value: "anthropic/claude-sonnet-4" },
38
+ { label: "Google Gemini 2.5 Pro", value: "google/gemini-2.5-pro" },
39
+ { label: "OpenAI GPT-5 mini", value: "openai/gpt-5-mini" },
40
+ {
41
+ label: "DeepSeek V3.1",
42
+ value: "deepseek/deepseek-chat-v3.1"
43
+ }
44
+ ],
45
+ [GEMINI_ENDPOINT]: [
46
+ { label: "Gemini 2.5 Pro", value: "gemini-2.5-pro" },
47
+ { label: "Gemini 2.5 Flash", value: "gemini-2.5-flash" },
48
+ { label: "Gemini 2.5 Flash-Lite", value: "gemini-2.5-flash-lite" },
49
+ { label: "Gemini 2.0 Flash", value: "gemini-2.0-flash" },
50
+ { label: "Gemini 2.0 Flash-Lite", value: "gemini-2.0-flash-lite" }
51
+ ],
52
+ [TOGETHER_ENDPOINT]: [
53
+ { label: "DeepSeek V3.1", value: "deepseek-ai/DeepSeek-V3.1" },
54
+ {
55
+ label: "Qwen3 Coder 480B",
56
+ value: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8"
57
+ },
58
+ {
59
+ label: "Llama 4 Maverick",
60
+ value: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
61
+ },
62
+ { label: "DeepSeek V3", value: "deepseek-ai/DeepSeek-V3" },
63
+ {
64
+ label: "Llama 3.3 70B Turbo",
65
+ value: "meta-llama/Llama-3.3-70B-Instruct-Turbo"
66
+ }
67
+ ],
68
+ [GROQ_ENDPOINT]: [
69
+ { label: "GPT-OSS 120B", value: "openai/gpt-oss-120b" },
70
+ {
71
+ label: "Llama 4 Scout",
72
+ value: "meta-llama/llama-4-scout-17b-16e-instruct"
73
+ },
74
+ { label: "Qwen3 32B", value: "qwen/qwen3-32b" },
75
+ { label: "Llama 3.3 70B", value: "llama-3.3-70b-versatile" },
76
+ { label: "Llama 3.1 8B Instant", value: "llama-3.1-8b-instant" }
77
+ ],
78
+ [XAI_ENDPOINT]: [
79
+ { label: "Grok 4 Fast Reasoning", value: "grok-4-fast-reasoning" },
80
+ { label: "Grok 4 Fast Non-Reasoning", value: "grok-4-fast-non-reasoning" },
81
+ { label: "Grok Code Fast 1", value: "grok-code-fast-1" },
82
+ { label: "Grok 4", value: "grok-4-0709" },
83
+ { label: "Grok 3 Mini", value: "grok-3-mini" }
84
+ ],
85
+ [QWEN_INTL_ENDPOINT]: [
86
+ { label: "Qwen Plus Latest", value: "qwen-plus-latest" },
87
+ { label: "Qwen Turbo Latest", value: "qwen-turbo-latest" },
88
+ { label: "Qwen Max Latest", value: "qwen-max-latest" },
89
+ { label: "Qwen3 Coder Plus", value: "qwen3-coder-plus" },
90
+ { label: "Qwen3 Coder Next", value: "qwen3-coder-next" }
91
+ ],
92
+ [QWEN_CN_ENDPOINT]: [
93
+ { label: "Qwen Plus Latest", value: "qwen-plus-latest" },
94
+ { label: "Qwen Turbo Latest", value: "qwen-turbo-latest" },
95
+ { label: "Qwen Max Latest", value: "qwen-max-latest" },
96
+ { label: "Qwen3 Coder Plus", value: "qwen3-coder-plus" },
97
+ { label: "Qwen3 Coder Next", value: "qwen3-coder-next" }
98
+ ],
99
+ [QWEN_US_ENDPOINT]: [
100
+ { label: "Qwen Plus Latest", value: "qwen-plus-latest" },
101
+ { label: "Qwen Turbo Latest", value: "qwen-turbo-latest" },
102
+ { label: "Qwen Max Latest", value: "qwen-max-latest" },
103
+ { label: "Qwen3 Coder Plus", value: "qwen3-coder-plus" },
104
+ { label: "Qwen3 Coder Next", value: "qwen3-coder-next" }
105
+ ],
106
+ [PERPLEXITY_ENDPOINT]: [
107
+ { label: "Sonar Pro", value: "sonar-pro" },
108
+ { label: "Sonar", value: "sonar" },
109
+ { label: "Sonar Reasoning Pro", value: "sonar-reasoning-pro" },
110
+ { label: "Sonar Deep Research", value: "sonar-deep-research" }
111
+ ],
112
+ [KIMI_ENDPOINT]: [
113
+ { label: "Kimi K2.5", value: "kimi-k2.5" },
114
+ { label: "Kimi K2 Thinking", value: "kimi-k2-thinking" },
115
+ { label: "Kimi K2 Turbo Preview", value: "kimi-k2-turbo-preview" },
116
+ { label: "Kimi K2 0905 Preview", value: "kimi-k2-0905-preview" },
117
+ { label: "Kimi K2", value: "kimi-k2" }
118
+ ],
119
+ [PROXY_API_ENDPOINT]: [
120
+ {
121
+ label: "Anthropic Claude Sonnet 4",
122
+ value: "anthropic/claude-sonnet-4-20250514"
123
+ },
124
+ { label: "Gemini 2.5 Flash", value: "gemini/gemini-2.5-flash" },
125
+ { label: "OpenAI GPT-5 mini", value: "openai/gpt-5-mini" },
126
+ { label: "OpenAI GPT-4o", value: "openai/gpt-4o" },
127
+ {
128
+ label: "OpenRouter DeepSeek Chat V3.1",
129
+ value: "openrouter/deepseek/deepseek-chat-v3.1"
130
+ }
131
+ ]
132
+ };
133
+ var DEFAULT_AI_MODEL_BY_ENDPOINT = Object.fromEntries(
134
+ Object.entries(AI_MODEL_OPTIONS_BY_ENDPOINT).map(([endpoint, options]) => [
135
+ endpoint,
136
+ options[0]?.value ?? ""
137
+ ])
138
+ );
139
+ var normalizeModel = (value) => value.trim();
140
+ var getAiModelOptionsForEndpoint = (endpoint) => AI_MODEL_OPTIONS_BY_ENDPOINT[endpoint] ?? [];
141
+ var getDefaultAiModelForEndpoint = (endpoint) => DEFAULT_AI_MODEL_BY_ENDPOINT[endpoint] ?? "";
142
+ var normalizeAiModel = (value, endpoint) => {
143
+ if (typeof value === "string") {
144
+ const trimmed = normalizeModel(value);
145
+ if (trimmed) {
146
+ return trimmed;
147
+ }
148
+ }
149
+ return getDefaultAiModelForEndpoint(endpoint);
150
+ };
151
+ var isKnownAiModelForEndpoint = (value, endpoint) => getAiModelOptionsForEndpoint(endpoint).some(
152
+ (option) => option.value === normalizeAiModel(value, endpoint)
153
+ );
154
+ var hasPresetAiModelsForEndpoint = (endpoint) => getAiModelOptionsForEndpoint(endpoint).length > 0 && endpoint !== AI_CUSTOM_ENDPOINT_VALUE;
155
+
156
+ export {
157
+ AI_CUSTOM_MODEL_VALUE,
158
+ getAiModelOptionsForEndpoint,
159
+ getDefaultAiModelForEndpoint,
160
+ normalizeAiModel,
161
+ isKnownAiModelForEndpoint,
162
+ hasPresetAiModelsForEndpoint
163
+ };
@@ -0,0 +1,132 @@
1
+ // src/aiEndpoints.ts
2
+ var AI_CUSTOM_ENDPOINT_VALUE = "__custom__";
3
+ var AI_ENDPOINT_OPTIONS = [
4
+ {
5
+ label: "OpenAI",
6
+ value: "https://api.openai.com/v1"
7
+ },
8
+ {
9
+ label: "Claude",
10
+ value: "https://api.anthropic.com/v1"
11
+ },
12
+ {
13
+ label: "OpenRouter",
14
+ value: "https://openrouter.ai/api/v1"
15
+ },
16
+ {
17
+ label: "Gemini",
18
+ value: "https://generativelanguage.googleapis.com/v1beta/openai"
19
+ },
20
+ {
21
+ label: "Together AI",
22
+ value: "https://api.together.xyz/v1"
23
+ },
24
+ {
25
+ label: "Groq",
26
+ value: "https://api.groq.com/openai/v1"
27
+ },
28
+ {
29
+ label: "DeepInfra",
30
+ value: "https://api.deepinfra.com/v1/openai"
31
+ },
32
+ {
33
+ label: "xAI",
34
+ value: "https://api.x.ai/v1"
35
+ },
36
+ {
37
+ label: "Qwen (DashScope Intl)",
38
+ value: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
39
+ },
40
+ {
41
+ label: "Qwen (DashScope CN)",
42
+ value: "https://dashscope.aliyuncs.com/compatible-mode/v1"
43
+ },
44
+ {
45
+ label: "Qwen (DashScope US)",
46
+ value: "https://dashscope-us.aliyuncs.com/compatible-mode/v1"
47
+ },
48
+ {
49
+ label: "Perplexity",
50
+ value: "https://api.perplexity.ai"
51
+ },
52
+ {
53
+ label: "Fireworks",
54
+ value: "https://api.fireworks.ai/inference/v1"
55
+ },
56
+ {
57
+ label: "SambaNova",
58
+ value: "https://api.sambanova.ai/v1"
59
+ },
60
+ {
61
+ label: "Hyperbolic",
62
+ value: "https://api.hyperbolic.xyz/v1"
63
+ },
64
+ {
65
+ label: "Kimi",
66
+ value: "https://api.moonshot.ai/v1"
67
+ },
68
+ {
69
+ label: "ProxyAPI",
70
+ value: "https://openai.api.proxyapi.ru/v1"
71
+ },
72
+ {
73
+ label: "Custom",
74
+ value: AI_CUSTOM_ENDPOINT_VALUE
75
+ }
76
+ ];
77
+ var KNOWN_AI_ENDPOINTS = new Set(
78
+ AI_ENDPOINT_OPTIONS.map((option) => option.value).filter(
79
+ (value) => value !== AI_CUSTOM_ENDPOINT_VALUE
80
+ )
81
+ );
82
+ var normalizeUrl = (value) => value.replace(/\/+$/, "");
83
+ var isIpv4Address = (value) => /^(?:\d{1,3}\.){3}\d{1,3}$/.test(value.trim());
84
+ var parseIpv4Address = (value) => value.trim().split(".").map((part) => Number(part));
85
+ var isPrivateIpv4Address = (value) => {
86
+ if (!isIpv4Address(value)) {
87
+ return false;
88
+ }
89
+ const [a, b, c, d] = parseIpv4Address(value);
90
+ if ([a, b, c, d].some(
91
+ (part) => !Number.isInteger(part) || part < 0 || part > 255
92
+ )) {
93
+ return false;
94
+ }
95
+ return a === 10 || a === 127 || a === 0 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
96
+ };
97
+ var isPrivateHostname = (hostname) => {
98
+ const normalized = hostname.trim().toLowerCase();
99
+ if (!normalized) {
100
+ return true;
101
+ }
102
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized.endsWith(".lan") || normalized === "::1" || normalized === "[::1]" || isPrivateIpv4Address(normalized);
103
+ };
104
+ var isValidAiEndpointUrl = (value) => {
105
+ try {
106
+ const url = new URL(value);
107
+ return url.protocol === "https:" && !isPrivateHostname(url.hostname);
108
+ } catch {
109
+ return false;
110
+ }
111
+ };
112
+ var normalizeAiEndpoint = (value) => {
113
+ if (typeof value !== "string") {
114
+ return "";
115
+ }
116
+ const trimmed = normalizeUrl(value.trim());
117
+ if (!trimmed) {
118
+ return "";
119
+ }
120
+ if (KNOWN_AI_ENDPOINTS.has(trimmed)) {
121
+ return trimmed;
122
+ }
123
+ return isValidAiEndpointUrl(trimmed) ? trimmed : "";
124
+ };
125
+ var isKnownAiEndpoint = (value) => KNOWN_AI_ENDPOINTS.has(normalizeAiEndpoint(value));
126
+
127
+ export {
128
+ AI_CUSTOM_ENDPOINT_VALUE,
129
+ AI_ENDPOINT_OPTIONS,
130
+ normalizeAiEndpoint,
131
+ isKnownAiEndpoint
132
+ };
package/dist/ml.js CHANGED
@@ -637,6 +637,44 @@ var INDICATOR_TIMEFRAMES = [
637
637
  { label: "TF4H", suffix: "4h" },
638
638
  { label: "TF1D", suffix: "1d" }
639
639
  ];
640
+ var DERIVATIVES_INTERVALS = [
641
+ { label: "15M", key: "15m" },
642
+ { label: "1H", key: "1h" }
643
+ ];
644
+ var DERIVATIVES_PRESSURES = [
645
+ "neutral",
646
+ "crowded_long",
647
+ "crowded_short",
648
+ "long_flush",
649
+ "short_flush"
650
+ ];
651
+ var DERIVATIVES_PRESSURE_LABELS = {
652
+ neutral: "Neutral",
653
+ crowded_long: "CrowdedLong",
654
+ crowded_short: "CrowdedShort",
655
+ long_flush: "LongFlush",
656
+ short_flush: "ShortFlush"
657
+ };
658
+ var DERIVATIVES_RISK_FLAGS = [
659
+ "missing_derivatives",
660
+ "stale_derivatives",
661
+ "crowded_long",
662
+ "crowded_short",
663
+ "oi_falling",
664
+ "oi_not_confirming",
665
+ "long_liquidation_spike",
666
+ "short_liquidation_spike"
667
+ ];
668
+ var DERIVATIVES_RISK_FLAG_LABELS = {
669
+ missing_derivatives: "Missing",
670
+ stale_derivatives: "Stale",
671
+ crowded_long: "CrowdedLong",
672
+ crowded_short: "CrowdedShort",
673
+ oi_falling: "OiFalling",
674
+ oi_not_confirming: "OiNotConfirming",
675
+ long_liquidation_spike: "LongLiqSpike",
676
+ short_liquidation_spike: "ShortLiqSpike"
677
+ };
640
678
  var toNumber = (value, fallback = 0) => {
641
679
  if (typeof value === "number") {
642
680
  return Number.isFinite(value) ? value : fallback;
@@ -757,6 +795,7 @@ var sliceWindow = (values, endIndex, windowSize) => {
757
795
  return values.slice(start, endIndex + 1);
758
796
  };
759
797
  var asArray = (value) => Array.isArray(value) ? value : [];
798
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
760
799
  var dropLastFromIndicatorSeries = (indicators) => {
761
800
  if (!ML_WINDOW_POLICY.dropLastIndicatorElement) {
762
801
  return indicators;
@@ -1478,6 +1517,85 @@ var buildMlTrainingRow = (signalRecord, resultRecord) => {
1478
1517
  row.TrendLine_Slope = null;
1479
1518
  }
1480
1519
  };
1520
+ const applyDerivativesPhase = () => {
1521
+ const context2 = asRecord(signal?.additionalIndicators?.derivativesContext);
1522
+ const summary = asRecord(context2?.summary);
1523
+ const intervals = asRecord(context2?.intervals);
1524
+ const pressure = typeof summary?.pressure === "string" ? summary.pressure : "";
1525
+ const riskFlags = new Set(
1526
+ asArray(summary?.riskFlags).map((flag) => String(flag))
1527
+ );
1528
+ row.Deriv_HasContext = context2 ? 1 : 0;
1529
+ row.Deriv_Source_Coinalyze = context2?.source === "coinalyze" ? 1 : 0;
1530
+ row.Deriv_DirectionAligned = summary?.directionAligned === true ? 1 : summary?.directionAligned === false ? -1 : 0;
1531
+ for (const pressureName of DERIVATIVES_PRESSURES) {
1532
+ row[`Deriv_Pressure_${DERIVATIVES_PRESSURE_LABELS[pressureName]}`] = pressure === pressureName ? 1 : 0;
1533
+ }
1534
+ for (const riskFlag of DERIVATIVES_RISK_FLAGS) {
1535
+ row[`Deriv_Flag_${DERIVATIVES_RISK_FLAG_LABELS[riskFlag]}`] = riskFlags.has(riskFlag) ? 1 : 0;
1536
+ }
1537
+ for (const { label, key: intervalKey } of DERIVATIVES_INTERVALS) {
1538
+ const intervalContext = asRecord(intervals?.[intervalKey]);
1539
+ const prefix = `Deriv_${label}`;
1540
+ const asOfTs = toNumber(intervalContext?.asOfTs, entryTimestamp);
1541
+ const ageHours = intervalContext ? Math.max(0, safeDiv2(entryTimestamp - asOfTs, 60 * 60 * 1e3)) : 0;
1542
+ row[`${prefix}_Present`] = intervalContext ? 1 : 0;
1543
+ row[`${prefix}_Stale`] = intervalContext?.stale === true ? 1 : 0;
1544
+ row[`${prefix}_AgeHours_Log`] = safeLog1pPositive(ageHours);
1545
+ row[`${prefix}_Points_Log`] = safeLog1pPositive(
1546
+ toNumber(intervalContext?.points, 0)
1547
+ );
1548
+ row[`${prefix}_OpenInterest_Log`] = safeLog1pPositive(
1549
+ toNumber(intervalContext?.openInterest, 0)
1550
+ );
1551
+ row[`${prefix}_OiChange1h`] = clamp2(
1552
+ toNumber(intervalContext?.oiChangePct1h, 0) / 100,
1553
+ -5,
1554
+ 5
1555
+ );
1556
+ row[`${prefix}_OiChange4h`] = clamp2(
1557
+ toNumber(intervalContext?.oiChangePct4h, 0) / 100,
1558
+ -5,
1559
+ 5
1560
+ );
1561
+ row[`${prefix}_OiChange24h`] = clamp2(
1562
+ toNumber(intervalContext?.oiChangePct24h, 0) / 100,
1563
+ -5,
1564
+ 5
1565
+ );
1566
+ row[`${prefix}_FundingBps`] = clamp2(
1567
+ toNumber(intervalContext?.fundingRate, 0) * 1e4,
1568
+ -100,
1569
+ 100
1570
+ );
1571
+ row[`${prefix}_FundingZ`] = clamp2(
1572
+ toNumber(intervalContext?.fundingZScore, 0),
1573
+ -8,
1574
+ 8
1575
+ );
1576
+ row[`${prefix}_LiqLong_Log`] = safeLog1pPositive(
1577
+ toNumber(intervalContext?.liqLong, 0)
1578
+ );
1579
+ row[`${prefix}_LiqShort_Log`] = safeLog1pPositive(
1580
+ toNumber(intervalContext?.liqShort, 0)
1581
+ );
1582
+ row[`${prefix}_LiqTotal_Log`] = safeLog1pPositive(
1583
+ toNumber(intervalContext?.liqTotal, 0)
1584
+ );
1585
+ row[`${prefix}_LiqImbalance`] = clamp2(
1586
+ toNumber(intervalContext?.liqImbalance, 0),
1587
+ -1,
1588
+ 1
1589
+ );
1590
+ row[`${prefix}_LiqSpike_Log`] = safeLog1pPositive(
1591
+ toNumber(intervalContext?.liqSpikeRatio, 0)
1592
+ );
1593
+ row[`${prefix}_LiqSpike_Squashed`] = squash(
1594
+ toNumber(intervalContext?.liqSpikeRatio, 0),
1595
+ 3
1596
+ );
1597
+ }
1598
+ };
1481
1599
  const applyLabelPhase = () => {
1482
1600
  const profit = toNumber(resultRecord?.profit, NaN);
1483
1601
  row.label = Number.isFinite(profit) ? profit > 0 ? 1 : 0 : null;
@@ -1487,6 +1605,7 @@ var buildMlTrainingRow = (signalRecord, resultRecord) => {
1487
1605
  applyRegimePhase();
1488
1606
  applySeriesAnalysisPhase();
1489
1607
  applyTrendlinePhase();
1608
+ applyDerivativesPhase();
1490
1609
  applyLabelPhase();
1491
1610
  return row;
1492
1611
  };
package/dist/ml.mjs CHANGED
@@ -425,6 +425,44 @@ var INDICATOR_TIMEFRAMES = [
425
425
  { label: "TF4H", suffix: "4h" },
426
426
  { label: "TF1D", suffix: "1d" }
427
427
  ];
428
+ var DERIVATIVES_INTERVALS = [
429
+ { label: "15M", key: "15m" },
430
+ { label: "1H", key: "1h" }
431
+ ];
432
+ var DERIVATIVES_PRESSURES = [
433
+ "neutral",
434
+ "crowded_long",
435
+ "crowded_short",
436
+ "long_flush",
437
+ "short_flush"
438
+ ];
439
+ var DERIVATIVES_PRESSURE_LABELS = {
440
+ neutral: "Neutral",
441
+ crowded_long: "CrowdedLong",
442
+ crowded_short: "CrowdedShort",
443
+ long_flush: "LongFlush",
444
+ short_flush: "ShortFlush"
445
+ };
446
+ var DERIVATIVES_RISK_FLAGS = [
447
+ "missing_derivatives",
448
+ "stale_derivatives",
449
+ "crowded_long",
450
+ "crowded_short",
451
+ "oi_falling",
452
+ "oi_not_confirming",
453
+ "long_liquidation_spike",
454
+ "short_liquidation_spike"
455
+ ];
456
+ var DERIVATIVES_RISK_FLAG_LABELS = {
457
+ missing_derivatives: "Missing",
458
+ stale_derivatives: "Stale",
459
+ crowded_long: "CrowdedLong",
460
+ crowded_short: "CrowdedShort",
461
+ oi_falling: "OiFalling",
462
+ oi_not_confirming: "OiNotConfirming",
463
+ long_liquidation_spike: "LongLiqSpike",
464
+ short_liquidation_spike: "ShortLiqSpike"
465
+ };
428
466
  var toNumber = (value, fallback = 0) => {
429
467
  if (typeof value === "number") {
430
468
  return Number.isFinite(value) ? value : fallback;
@@ -545,6 +583,7 @@ var sliceWindow = (values, endIndex, windowSize) => {
545
583
  return values.slice(start, endIndex + 1);
546
584
  };
547
585
  var asArray = (value) => Array.isArray(value) ? value : [];
586
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
548
587
  var dropLastFromIndicatorSeries = (indicators) => {
549
588
  if (!ML_WINDOW_POLICY.dropLastIndicatorElement) {
550
589
  return indicators;
@@ -1266,6 +1305,85 @@ var buildMlTrainingRow = (signalRecord, resultRecord) => {
1266
1305
  row.TrendLine_Slope = null;
1267
1306
  }
1268
1307
  };
1308
+ const applyDerivativesPhase = () => {
1309
+ const context2 = asRecord(signal?.additionalIndicators?.derivativesContext);
1310
+ const summary = asRecord(context2?.summary);
1311
+ const intervals = asRecord(context2?.intervals);
1312
+ const pressure = typeof summary?.pressure === "string" ? summary.pressure : "";
1313
+ const riskFlags = new Set(
1314
+ asArray(summary?.riskFlags).map((flag) => String(flag))
1315
+ );
1316
+ row.Deriv_HasContext = context2 ? 1 : 0;
1317
+ row.Deriv_Source_Coinalyze = context2?.source === "coinalyze" ? 1 : 0;
1318
+ row.Deriv_DirectionAligned = summary?.directionAligned === true ? 1 : summary?.directionAligned === false ? -1 : 0;
1319
+ for (const pressureName of DERIVATIVES_PRESSURES) {
1320
+ row[`Deriv_Pressure_${DERIVATIVES_PRESSURE_LABELS[pressureName]}`] = pressure === pressureName ? 1 : 0;
1321
+ }
1322
+ for (const riskFlag of DERIVATIVES_RISK_FLAGS) {
1323
+ row[`Deriv_Flag_${DERIVATIVES_RISK_FLAG_LABELS[riskFlag]}`] = riskFlags.has(riskFlag) ? 1 : 0;
1324
+ }
1325
+ for (const { label, key: intervalKey } of DERIVATIVES_INTERVALS) {
1326
+ const intervalContext = asRecord(intervals?.[intervalKey]);
1327
+ const prefix = `Deriv_${label}`;
1328
+ const asOfTs = toNumber(intervalContext?.asOfTs, entryTimestamp);
1329
+ const ageHours = intervalContext ? Math.max(0, safeDiv2(entryTimestamp - asOfTs, 60 * 60 * 1e3)) : 0;
1330
+ row[`${prefix}_Present`] = intervalContext ? 1 : 0;
1331
+ row[`${prefix}_Stale`] = intervalContext?.stale === true ? 1 : 0;
1332
+ row[`${prefix}_AgeHours_Log`] = safeLog1pPositive(ageHours);
1333
+ row[`${prefix}_Points_Log`] = safeLog1pPositive(
1334
+ toNumber(intervalContext?.points, 0)
1335
+ );
1336
+ row[`${prefix}_OpenInterest_Log`] = safeLog1pPositive(
1337
+ toNumber(intervalContext?.openInterest, 0)
1338
+ );
1339
+ row[`${prefix}_OiChange1h`] = clamp2(
1340
+ toNumber(intervalContext?.oiChangePct1h, 0) / 100,
1341
+ -5,
1342
+ 5
1343
+ );
1344
+ row[`${prefix}_OiChange4h`] = clamp2(
1345
+ toNumber(intervalContext?.oiChangePct4h, 0) / 100,
1346
+ -5,
1347
+ 5
1348
+ );
1349
+ row[`${prefix}_OiChange24h`] = clamp2(
1350
+ toNumber(intervalContext?.oiChangePct24h, 0) / 100,
1351
+ -5,
1352
+ 5
1353
+ );
1354
+ row[`${prefix}_FundingBps`] = clamp2(
1355
+ toNumber(intervalContext?.fundingRate, 0) * 1e4,
1356
+ -100,
1357
+ 100
1358
+ );
1359
+ row[`${prefix}_FundingZ`] = clamp2(
1360
+ toNumber(intervalContext?.fundingZScore, 0),
1361
+ -8,
1362
+ 8
1363
+ );
1364
+ row[`${prefix}_LiqLong_Log`] = safeLog1pPositive(
1365
+ toNumber(intervalContext?.liqLong, 0)
1366
+ );
1367
+ row[`${prefix}_LiqShort_Log`] = safeLog1pPositive(
1368
+ toNumber(intervalContext?.liqShort, 0)
1369
+ );
1370
+ row[`${prefix}_LiqTotal_Log`] = safeLog1pPositive(
1371
+ toNumber(intervalContext?.liqTotal, 0)
1372
+ );
1373
+ row[`${prefix}_LiqImbalance`] = clamp2(
1374
+ toNumber(intervalContext?.liqImbalance, 0),
1375
+ -1,
1376
+ 1
1377
+ );
1378
+ row[`${prefix}_LiqSpike_Log`] = safeLog1pPositive(
1379
+ toNumber(intervalContext?.liqSpikeRatio, 0)
1380
+ );
1381
+ row[`${prefix}_LiqSpike_Squashed`] = squash(
1382
+ toNumber(intervalContext?.liqSpikeRatio, 0),
1383
+ 3
1384
+ );
1385
+ }
1386
+ };
1269
1387
  const applyLabelPhase = () => {
1270
1388
  const profit = toNumber(resultRecord?.profit, NaN);
1271
1389
  row.label = Number.isFinite(profit) ? profit > 0 ? 1 : 0 : null;
@@ -1275,6 +1393,7 @@ var buildMlTrainingRow = (signalRecord, resultRecord) => {
1275
1393
  applyRegimePhase();
1276
1394
  applySeriesAnalysisPhase();
1277
1395
  applyTrendlinePhase();
1396
+ applyDerivativesPhase();
1278
1397
  applyLabelPhase();
1279
1398
  return row;
1280
1399
  };