@tradejs/infra 1.0.6 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };
@@ -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) => {
@@ -120,6 +122,31 @@ var parseJsonOrDeleteKey = async (redis, key, raw, fallback) => {
120
122
  return fallback;
121
123
  }
122
124
  };
125
+ var parseHashMap = (value) => {
126
+ if (!value) {
127
+ return {};
128
+ }
129
+ if (Array.isArray(value)) {
130
+ const result = {};
131
+ for (let index = 0; index < value.length; index += 2) {
132
+ const field = value[index];
133
+ const fieldValue = value[index + 1];
134
+ if (field == null || fieldValue == null) {
135
+ continue;
136
+ }
137
+ result[String(field)] = toResultString(fieldValue) ?? "";
138
+ }
139
+ return result;
140
+ }
141
+ if (typeof value === "object") {
142
+ return Object.fromEntries(
143
+ Object.entries(value).map(
144
+ ([field, fieldValue]) => [field, toResultString(fieldValue) ?? ""]
145
+ )
146
+ );
147
+ }
148
+ return {};
149
+ };
123
150
  var getKeys = async (prefix) => {
124
151
  if (redisUnavailable) return [];
125
152
  const redis = await getReadyRedis();
@@ -256,6 +283,126 @@ var setData = async (key, data, options = {}) => {
256
283
  }
257
284
  }
258
285
  };
286
+ var setHashJsonField = async (key, field, data, options = {}) => {
287
+ if (redisUnavailable) return;
288
+ const { expire } = { ...DEFAULT_OPTIONS, ...options };
289
+ const redis = await getReadyRedis();
290
+ if (!redis) return;
291
+ try {
292
+ await redis.call("HSET", key, field, toJson(data));
293
+ if (expire) {
294
+ await redis.expire(key, expire);
295
+ }
296
+ } catch (e) {
297
+ if (e instanceof Error && isRedisConnectivityError(e)) {
298
+ markRedisUnavailable(e);
299
+ return;
300
+ }
301
+ logger.log("error", "failed HSET %s[%s]: %s", key, field, String(e));
302
+ }
303
+ };
304
+ var getHashJsonValues = async (key) => {
305
+ if (redisUnavailable) return [];
306
+ const redis = await getReadyRedis();
307
+ if (!redis) return [];
308
+ try {
309
+ const raw = await redis.call("HGETALL", key);
310
+ const values = Object.values(parseHashMap(raw));
311
+ const parsed = [];
312
+ for (const value of values) {
313
+ if (!value) {
314
+ continue;
315
+ }
316
+ try {
317
+ parsed.push(JSON.parse(value));
318
+ } catch (e) {
319
+ logger.log(
320
+ "error",
321
+ "failed JSON.parse(HGETALL %s value): %s",
322
+ key,
323
+ String(e)
324
+ );
325
+ }
326
+ }
327
+ return parsed;
328
+ } catch (e) {
329
+ if (e instanceof Error && isRedisConnectivityError(e)) {
330
+ markRedisUnavailable(e);
331
+ return [];
332
+ }
333
+ logger.log("error", "failed HGETALL %s: %s", key, String(e));
334
+ return [];
335
+ }
336
+ };
337
+ var incrHashFields = async (key, increments, options = {}) => {
338
+ if (redisUnavailable) return;
339
+ const { expire } = { ...DEFAULT_OPTIONS, ...options };
340
+ const redis = await getReadyRedis();
341
+ if (!redis) return;
342
+ try {
343
+ for (const [field, increment] of Object.entries(increments)) {
344
+ if (!Number.isFinite(increment) || increment === 0) {
345
+ continue;
346
+ }
347
+ await redis.call("HINCRBY", key, field, Math.trunc(increment));
348
+ }
349
+ if (expire) {
350
+ await redis.expire(key, expire);
351
+ }
352
+ } catch (e) {
353
+ if (e instanceof Error && isRedisConnectivityError(e)) {
354
+ markRedisUnavailable(e);
355
+ return;
356
+ }
357
+ logger.log("error", "failed HINCRBY %s: %s", key, String(e));
358
+ }
359
+ };
360
+ var getHashData = async (key) => {
361
+ if (redisUnavailable) return {};
362
+ const redis = await getReadyRedis();
363
+ if (!redis) return {};
364
+ try {
365
+ const raw = await redis.call("HGETALL", key);
366
+ return parseHashMap(raw);
367
+ } catch (e) {
368
+ if (e instanceof Error && isRedisConnectivityError(e)) {
369
+ markRedisUnavailable(e);
370
+ return {};
371
+ }
372
+ logger.log("error", "failed HGETALL %s: %s", key, String(e));
373
+ return {};
374
+ }
375
+ };
376
+ var createScreenshotSessionToken = async (userName) => {
377
+ const token = randomBytes(24).toString("hex");
378
+ await setData(
379
+ redisKeys.screenshotSessionToken(token),
380
+ {
381
+ userName,
382
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
383
+ },
384
+ {
385
+ expire: SCREENSHOT_TOKEN_TTL_SECONDS
386
+ }
387
+ );
388
+ return token;
389
+ };
390
+ var consumeScreenshotSessionToken = async (token) => {
391
+ const normalizedToken = token.trim();
392
+ if (!normalizedToken) {
393
+ return null;
394
+ }
395
+ const key = redisKeys.screenshotSessionToken(normalizedToken);
396
+ const payload = await getData(
397
+ key,
398
+ null
399
+ );
400
+ if (!payload?.userName) {
401
+ return null;
402
+ }
403
+ await delKey(key);
404
+ return payload.userName;
405
+ };
259
406
  var redisKeys = {
260
407
  users: () => "users:index:",
261
408
  user: (userName) => `users:index:${userName}`,
@@ -270,14 +417,34 @@ var redisKeys = {
270
417
  testOrders: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:orders`,
271
418
  testConfig: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:config`,
272
419
  testStat: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:stat`,
420
+ testSummaries: (userName) => `users:${userName}:tests:index:summary`,
273
421
  cacheChunk: (userName, chunkId) => `users:${userName}:cache:tests:chunks:${chunkId}`,
274
422
  cacheOrders: (userName, orderLogId) => `users:${userName}:cache:tests:orders:${orderLogId}`,
275
423
  cachePositions: (userName, orderLogId) => `users:${userName}:cache:tests:positions:${orderLogId}`,
276
424
  signal: (symbol, signalId) => `signals:${symbol}:${signalId}`,
277
425
  signalsBySymbol: (symbol) => `signals:${symbol}:`,
278
426
  storeSignal: (symbol, signalId) => `store:signals:${symbol}:${signalId}`,
427
+ runtimeSignals: (userName) => `users:${userName}:runtime:signals:`,
428
+ runtimeSignal: (userName, signalId) => `users:${userName}:runtime:signals:${signalId}`,
429
+ runtimeSignalBuckets: (userName) => `users:${userName}:runtime:signals:days:`,
430
+ runtimeSignalBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signals:days:${dayKey}:${strategyName}`,
431
+ runtimeSignalEvaluations: (userName) => `users:${userName}:runtime:signal-evaluations:`,
432
+ runtimeSignalEvaluation: (userName, evaluationId) => `users:${userName}:runtime:signal-evaluations:${evaluationId}`,
433
+ runtimeSignalEvaluationBuckets: (userName) => `users:${userName}:runtime:signal-evaluations:days:`,
434
+ runtimeSignalEvaluationBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signal-evaluations:days:${dayKey}:${strategyName}`,
435
+ runtimeSignalEvaluationStatsBuckets: (userName) => `users:${userName}:runtime:signal-evaluation-stats:days:`,
436
+ runtimeSignalEvaluationStatsBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signal-evaluation-stats:days:${dayKey}:${strategyName}`,
437
+ runtimeTrades: (userName) => `users:${userName}:runtime:trade-records:`,
438
+ runtimeTrade: (userName, orderId) => `users:${userName}:runtime:trade-records:${orderId}`,
439
+ runtimeActiveTrades: (userName) => `users:${userName}:runtime:active-trades:`,
440
+ runtimeActiveTrade: (userName, symbol) => `users:${userName}:runtime:active-trades:${symbol}`,
441
+ aiChatHistory: (userName, symbolKey) => `users:${userName}:ai:chats:${symbolKey}`,
279
442
  analysis: (symbol, signalId) => `analysis:${symbol}:${signalId}`,
443
+ screenshotSessionToken: (token) => `auth:screenshot:${token}`,
280
444
  backtestResults: (userName, config, timestamp) => `users:${userName}:backtests:results:${config}:${timestamp}`,
445
+ researchRuns: (userName) => `users:${userName}:research:runs:`,
446
+ researchRun: (userName, runId) => `users:${userName}:research:runs:${runId}`,
447
+ researchLatestRun: (userName, strategyName) => `users:${userName}:research:latest:${strategyName}`,
281
448
  mlSignalsByStrategy: (strategyName) => `ml:${strategyName}:signals:`,
282
449
  mlSignals: () => "ml:",
283
450
  mlSignal: (strategyName, signalId) => `ml:${strategyName}:signals:${signalId}`,
@@ -293,5 +460,11 @@ export {
293
460
  RedisWriteBlockedError,
294
461
  delKeyWithOptions,
295
462
  setData,
463
+ setHashJsonField,
464
+ getHashJsonValues,
465
+ incrHashFields,
466
+ getHashData,
467
+ createScreenshotSessionToken,
468
+ consumeScreenshotSessionToken,
296
469
  redisKeys
297
470
  };
@@ -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
  };