hedgequantx 2.9.18 → 2.9.20
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/package.json +1 -1
- package/src/app.js +42 -64
- package/src/lib/m/hqx-2b.js +7 -0
- package/src/lib/m/index.js +138 -0
- package/src/lib/m/ultra-scalping.js +7 -0
- package/src/menus/connect.js +14 -17
- package/src/menus/dashboard.js +58 -76
- package/src/pages/accounts.js +38 -49
- package/src/pages/algo/copy-trading.js +546 -178
- package/src/pages/algo/index.js +18 -75
- package/src/pages/algo/one-account.js +322 -57
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +19 -22
- package/src/pages/positions.js +19 -22
- package/src/pages/stats/index.js +15 -16
- package/src/pages/user.js +7 -11
- package/src/services/ai-supervision/health.js +35 -47
- package/src/services/index.js +1 -9
- package/src/services/rithmic/accounts.js +8 -6
- package/src/ui/box.js +9 -5
- package/src/ui/index.js +5 -18
- package/src/ui/menu.js +4 -4
- package/src/pages/ai-agents-ui.js +0 -388
- package/src/pages/ai-agents.js +0 -494
- package/src/pages/ai-models.js +0 -389
- package/src/pages/algo/algo-executor.js +0 -307
- package/src/pages/algo/copy-executor.js +0 -331
- package/src/pages/algo/custom-strategy.js +0 -313
- package/src/services/ai-supervision/consensus.js +0 -284
- package/src/services/ai-supervision/context.js +0 -275
- package/src/services/ai-supervision/directive.js +0 -167
- package/src/services/ai-supervision/index.js +0 -309
- package/src/services/ai-supervision/parser.js +0 -278
- package/src/services/ai-supervision/symbols.js +0 -259
- package/src/services/cliproxy/index.js +0 -256
- package/src/services/cliproxy/installer.js +0 -111
- package/src/services/cliproxy/manager.js +0 -387
- package/src/services/llmproxy/index.js +0 -166
- package/src/services/llmproxy/manager.js +0 -411
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI Response Parser
|
|
3
|
-
*
|
|
4
|
-
* Parses responses from AI agents (JSON or text)
|
|
5
|
-
* and normalizes them to a standard format.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Default response when parsing fails
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_RESPONSE = {
|
|
12
|
-
decision: 'approve',
|
|
13
|
-
confidence: 50,
|
|
14
|
-
optimizations: null,
|
|
15
|
-
reason: 'Parse failed - defaulting to approve',
|
|
16
|
-
alerts: null,
|
|
17
|
-
parseSuccess: false
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract JSON from a string that may contain markdown or extra text
|
|
22
|
-
*/
|
|
23
|
-
const extractJSON = (text) => {
|
|
24
|
-
if (!text || typeof text !== 'string') return null;
|
|
25
|
-
|
|
26
|
-
// Try direct parse first
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(text.trim());
|
|
29
|
-
} catch (e) { /* continue */ }
|
|
30
|
-
|
|
31
|
-
// Try to find JSON in markdown code blocks
|
|
32
|
-
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
33
|
-
if (codeBlockMatch) {
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(codeBlockMatch[1].trim());
|
|
36
|
-
} catch (e) { /* continue */ }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Try to find JSON object pattern
|
|
40
|
-
const jsonMatch = text.match(/\{[\s\S]*"decision"[\s\S]*\}/);
|
|
41
|
-
if (jsonMatch) {
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(jsonMatch[0]);
|
|
44
|
-
} catch (e) { /* continue */ }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Validate and normalize the decision field
|
|
52
|
-
*/
|
|
53
|
-
const normalizeDecision = (decision) => {
|
|
54
|
-
if (!decision) return 'approve';
|
|
55
|
-
|
|
56
|
-
const d = String(decision).toLowerCase().trim();
|
|
57
|
-
|
|
58
|
-
if (d === 'approve' || d === 'yes' || d === 'accept' || d === 'go') return 'approve';
|
|
59
|
-
if (d === 'reject' || d === 'no' || d === 'deny' || d === 'stop') return 'reject';
|
|
60
|
-
if (d === 'modify' || d === 'adjust' || d === 'optimize') return 'modify';
|
|
61
|
-
|
|
62
|
-
return 'approve';
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Validate and normalize confidence score
|
|
67
|
-
*/
|
|
68
|
-
const normalizeConfidence = (confidence) => {
|
|
69
|
-
if (confidence === undefined || confidence === null) return 50;
|
|
70
|
-
|
|
71
|
-
const c = Number(confidence);
|
|
72
|
-
if (isNaN(c)) return 50;
|
|
73
|
-
|
|
74
|
-
// Handle percentage strings like "85%"
|
|
75
|
-
if (typeof confidence === 'string' && confidence.includes('%')) {
|
|
76
|
-
const parsed = parseFloat(confidence);
|
|
77
|
-
if (!isNaN(parsed)) return Math.min(100, Math.max(0, parsed));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Normalize to 0-100 range
|
|
81
|
-
if (c >= 0 && c <= 1) return Math.round(c * 100);
|
|
82
|
-
return Math.min(100, Math.max(0, Math.round(c)));
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Validate and normalize optimizations
|
|
87
|
-
*/
|
|
88
|
-
const normalizeOptimizations = (opts, signal) => {
|
|
89
|
-
if (!opts) return null;
|
|
90
|
-
|
|
91
|
-
const normalized = {
|
|
92
|
-
entry: null,
|
|
93
|
-
stopLoss: null,
|
|
94
|
-
takeProfit: null,
|
|
95
|
-
size: null,
|
|
96
|
-
timing: 'now'
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// Entry price
|
|
100
|
-
if (opts.entry !== undefined && opts.entry !== null) {
|
|
101
|
-
const entry = Number(opts.entry);
|
|
102
|
-
if (!isNaN(entry) && entry > 0) normalized.entry = entry;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stop loss
|
|
106
|
-
if (opts.stopLoss !== undefined && opts.stopLoss !== null) {
|
|
107
|
-
const sl = Number(opts.stopLoss);
|
|
108
|
-
if (!isNaN(sl) && sl > 0) normalized.stopLoss = sl;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Take profit
|
|
112
|
-
if (opts.takeProfit !== undefined && opts.takeProfit !== null) {
|
|
113
|
-
const tp = Number(opts.takeProfit);
|
|
114
|
-
if (!isNaN(tp) && tp > 0) normalized.takeProfit = tp;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Size adjustment (-0.5 to +0.5)
|
|
118
|
-
if (opts.size !== undefined && opts.size !== null) {
|
|
119
|
-
const size = Number(opts.size);
|
|
120
|
-
if (!isNaN(size)) {
|
|
121
|
-
normalized.size = Math.min(0.5, Math.max(-0.5, size));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Timing
|
|
126
|
-
if (opts.timing) {
|
|
127
|
-
const t = String(opts.timing).toLowerCase().trim();
|
|
128
|
-
if (t === 'now' || t === 'immediate') normalized.timing = 'now';
|
|
129
|
-
else if (t === 'wait' || t === 'delay') normalized.timing = 'wait';
|
|
130
|
-
else if (t === 'cancel' || t === 'abort') normalized.timing = 'cancel';
|
|
131
|
-
else normalized.timing = 'now';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return normalized;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Normalize reason string
|
|
139
|
-
*/
|
|
140
|
-
const normalizeReason = (reason) => {
|
|
141
|
-
if (!reason) return 'No reason provided';
|
|
142
|
-
|
|
143
|
-
const r = String(reason).trim();
|
|
144
|
-
if (r.length > 100) return r.substring(0, 97) + '...';
|
|
145
|
-
return r;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Normalize alerts array
|
|
150
|
-
*/
|
|
151
|
-
const normalizeAlerts = (alerts) => {
|
|
152
|
-
if (!alerts) return null;
|
|
153
|
-
if (!Array.isArray(alerts)) {
|
|
154
|
-
if (typeof alerts === 'string') return [alerts];
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
return alerts.filter(a => a && typeof a === 'string').slice(0, 5);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Parse text response when JSON parsing fails
|
|
162
|
-
* Attempts to extract decision from natural language
|
|
163
|
-
*/
|
|
164
|
-
const parseTextResponse = (text, signal) => {
|
|
165
|
-
if (!text) return DEFAULT_RESPONSE;
|
|
166
|
-
|
|
167
|
-
const lower = text.toLowerCase();
|
|
168
|
-
|
|
169
|
-
// Determine decision from keywords
|
|
170
|
-
let decision = 'approve';
|
|
171
|
-
if (lower.includes('reject') || lower.includes('do not') || lower.includes("don't") ||
|
|
172
|
-
lower.includes('avoid') || lower.includes('skip') || lower.includes('no trade')) {
|
|
173
|
-
decision = 'reject';
|
|
174
|
-
} else if (lower.includes('modify') || lower.includes('adjust') || lower.includes('optimize') ||
|
|
175
|
-
lower.includes('tighten') || lower.includes('widen')) {
|
|
176
|
-
decision = 'modify';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Try to extract confidence
|
|
180
|
-
let confidence = 60;
|
|
181
|
-
const confMatch = lower.match(/confidence[:\s]*(\d+)/i) ||
|
|
182
|
-
lower.match(/(\d+)%?\s*confiden/i) ||
|
|
183
|
-
lower.match(/score[:\s]*(\d+)/i);
|
|
184
|
-
if (confMatch) {
|
|
185
|
-
confidence = normalizeConfidence(confMatch[1]);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Extract reason (first sentence or up to 100 chars)
|
|
189
|
-
let reason = text.split(/[.!?\n]/)[0]?.trim() || 'Parsed from text response';
|
|
190
|
-
reason = normalizeReason(reason);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
decision,
|
|
194
|
-
confidence,
|
|
195
|
-
optimizations: decision === 'modify' ? {
|
|
196
|
-
entry: signal?.entry || null,
|
|
197
|
-
stopLoss: signal?.stopLoss || null,
|
|
198
|
-
takeProfit: signal?.takeProfit || null,
|
|
199
|
-
size: null,
|
|
200
|
-
timing: 'now'
|
|
201
|
-
} : null,
|
|
202
|
-
reason,
|
|
203
|
-
alerts: null,
|
|
204
|
-
parseSuccess: false,
|
|
205
|
-
parsedFromText: true
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Main parser function - parse AI response to standard format
|
|
211
|
-
*/
|
|
212
|
-
const parseAgentResponse = (response, signal = null) => {
|
|
213
|
-
// Handle empty response
|
|
214
|
-
if (!response) {
|
|
215
|
-
return { ...DEFAULT_RESPONSE, reason: 'Empty response from agent' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Handle response object with content field (common API format)
|
|
219
|
-
let text = response;
|
|
220
|
-
if (typeof response === 'object') {
|
|
221
|
-
if (response.content) text = response.content;
|
|
222
|
-
else if (response.text) text = response.text;
|
|
223
|
-
else if (response.message) text = response.message;
|
|
224
|
-
else text = JSON.stringify(response);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Try to extract and parse JSON
|
|
228
|
-
const json = extractJSON(text);
|
|
229
|
-
|
|
230
|
-
if (json && json.decision) {
|
|
231
|
-
// Successfully parsed JSON
|
|
232
|
-
return {
|
|
233
|
-
decision: normalizeDecision(json.decision),
|
|
234
|
-
confidence: normalizeConfidence(json.confidence),
|
|
235
|
-
optimizations: normalizeOptimizations(json.optimizations, signal),
|
|
236
|
-
reason: normalizeReason(json.reason),
|
|
237
|
-
alerts: normalizeAlerts(json.alerts),
|
|
238
|
-
parseSuccess: true
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Fallback to text parsing
|
|
243
|
-
return parseTextResponse(text, signal);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Validate a parsed response
|
|
248
|
-
*/
|
|
249
|
-
const validateResponse = (parsed) => {
|
|
250
|
-
const errors = [];
|
|
251
|
-
|
|
252
|
-
if (!['approve', 'reject', 'modify'].includes(parsed.decision)) {
|
|
253
|
-
errors.push(`Invalid decision: ${parsed.decision}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (parsed.confidence < 0 || parsed.confidence > 100) {
|
|
257
|
-
errors.push(`Invalid confidence: ${parsed.confidence}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (parsed.decision === 'modify' && !parsed.optimizations) {
|
|
261
|
-
errors.push('Modify decision requires optimizations');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
valid: errors.length === 0,
|
|
266
|
-
errors
|
|
267
|
-
};
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
module.exports = {
|
|
271
|
-
parseAgentResponse,
|
|
272
|
-
validateResponse,
|
|
273
|
-
extractJSON,
|
|
274
|
-
normalizeDecision,
|
|
275
|
-
normalizeConfidence,
|
|
276
|
-
normalizeOptimizations,
|
|
277
|
-
DEFAULT_RESPONSE
|
|
278
|
-
};
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Symbol Data for AI Supervision
|
|
3
|
-
*
|
|
4
|
-
* Contains detailed information about tradeable symbols
|
|
5
|
-
* including tick sizes, sessions, correlations, and characteristics.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const SYMBOLS = {
|
|
9
|
-
NQ: {
|
|
10
|
-
id: 'NQ',
|
|
11
|
-
name: 'E-mini Nasdaq 100',
|
|
12
|
-
exchange: 'CME',
|
|
13
|
-
tickSize: 0.25,
|
|
14
|
-
tickValue: 5.00,
|
|
15
|
-
pointValue: 20.00,
|
|
16
|
-
margin: 15000,
|
|
17
|
-
characteristics: {
|
|
18
|
-
volatility: 'high',
|
|
19
|
-
sector: 'technology',
|
|
20
|
-
behavior: 'momentum-driven, gap-prone',
|
|
21
|
-
avgRange: '150-300 points'
|
|
22
|
-
},
|
|
23
|
-
correlations: {
|
|
24
|
-
positive: ['ES', 'YM'],
|
|
25
|
-
negative: ['VIX'],
|
|
26
|
-
related: ['QQQ', 'AAPL', 'MSFT', 'NVDA']
|
|
27
|
-
},
|
|
28
|
-
sessions: {
|
|
29
|
-
most_active: ['us_open', 'us_close'],
|
|
30
|
-
avoid: ['asia_lunch', 'low_volume']
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
ES: {
|
|
35
|
-
id: 'ES',
|
|
36
|
-
name: 'E-mini S&P 500',
|
|
37
|
-
exchange: 'CME',
|
|
38
|
-
tickSize: 0.25,
|
|
39
|
-
tickValue: 12.50,
|
|
40
|
-
pointValue: 50.00,
|
|
41
|
-
margin: 12000,
|
|
42
|
-
characteristics: {
|
|
43
|
-
volatility: 'medium',
|
|
44
|
-
sector: 'broad_market',
|
|
45
|
-
behavior: 'reference index, institutional flow',
|
|
46
|
-
avgRange: '30-60 points'
|
|
47
|
-
},
|
|
48
|
-
correlations: {
|
|
49
|
-
positive: ['NQ', 'YM', 'RTY'],
|
|
50
|
-
negative: ['VIX', 'ZB'],
|
|
51
|
-
related: ['SPY', 'SPX']
|
|
52
|
-
},
|
|
53
|
-
sessions: {
|
|
54
|
-
most_active: ['us_open', 'us_close', 'london_us_overlap'],
|
|
55
|
-
avoid: ['asia_session']
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
YM: {
|
|
60
|
-
id: 'YM',
|
|
61
|
-
name: 'E-mini Dow',
|
|
62
|
-
exchange: 'CME',
|
|
63
|
-
tickSize: 1.00,
|
|
64
|
-
tickValue: 5.00,
|
|
65
|
-
pointValue: 5.00,
|
|
66
|
-
margin: 10000,
|
|
67
|
-
characteristics: {
|
|
68
|
-
volatility: 'medium-low',
|
|
69
|
-
sector: 'value_stocks',
|
|
70
|
-
behavior: 'slower than ES/NQ, less spiky',
|
|
71
|
-
avgRange: '200-400 points'
|
|
72
|
-
},
|
|
73
|
-
correlations: {
|
|
74
|
-
positive: ['ES', 'NQ'],
|
|
75
|
-
negative: ['VIX'],
|
|
76
|
-
related: ['DIA', 'DJIA']
|
|
77
|
-
},
|
|
78
|
-
sessions: {
|
|
79
|
-
most_active: ['us_open', 'us_close'],
|
|
80
|
-
avoid: ['overnight']
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
RTY: {
|
|
85
|
-
id: 'RTY',
|
|
86
|
-
name: 'E-mini Russell 2000',
|
|
87
|
-
exchange: 'CME',
|
|
88
|
-
tickSize: 0.10,
|
|
89
|
-
tickValue: 5.00,
|
|
90
|
-
pointValue: 50.00,
|
|
91
|
-
margin: 8000,
|
|
92
|
-
characteristics: {
|
|
93
|
-
volatility: 'high',
|
|
94
|
-
sector: 'small_caps',
|
|
95
|
-
behavior: 'more volatile than ES, liquidity gaps',
|
|
96
|
-
avgRange: '20-40 points'
|
|
97
|
-
},
|
|
98
|
-
correlations: {
|
|
99
|
-
positive: ['ES', 'NQ'],
|
|
100
|
-
negative: ['VIX'],
|
|
101
|
-
related: ['IWM', 'RUT']
|
|
102
|
-
},
|
|
103
|
-
sessions: {
|
|
104
|
-
most_active: ['us_open'],
|
|
105
|
-
avoid: ['overnight', 'low_volume']
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
GC: {
|
|
110
|
-
id: 'GC',
|
|
111
|
-
name: 'Gold Futures',
|
|
112
|
-
exchange: 'COMEX',
|
|
113
|
-
tickSize: 0.10,
|
|
114
|
-
tickValue: 10.00,
|
|
115
|
-
pointValue: 100.00,
|
|
116
|
-
margin: 11000,
|
|
117
|
-
characteristics: {
|
|
118
|
-
volatility: 'medium',
|
|
119
|
-
sector: 'precious_metals',
|
|
120
|
-
behavior: 'safe haven, inverse USD, central bank sensitive',
|
|
121
|
-
avgRange: '15-30 points'
|
|
122
|
-
},
|
|
123
|
-
correlations: {
|
|
124
|
-
positive: ['SI', 'EURUSD'],
|
|
125
|
-
negative: ['DXY', 'US10Y'],
|
|
126
|
-
related: ['GLD', 'XAUUSD']
|
|
127
|
-
},
|
|
128
|
-
sessions: {
|
|
129
|
-
most_active: ['london', 'us_open', 'asia_open'],
|
|
130
|
-
avoid: ['us_afternoon']
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
SI: {
|
|
135
|
-
id: 'SI',
|
|
136
|
-
name: 'Silver Futures',
|
|
137
|
-
exchange: 'COMEX',
|
|
138
|
-
tickSize: 0.005,
|
|
139
|
-
tickValue: 25.00,
|
|
140
|
-
pointValue: 5000.00,
|
|
141
|
-
margin: 9000,
|
|
142
|
-
characteristics: {
|
|
143
|
-
volatility: 'high',
|
|
144
|
-
sector: 'precious_metals',
|
|
145
|
-
behavior: 'follows gold with more volatility, industrial demand',
|
|
146
|
-
avgRange: '0.30-0.60 points'
|
|
147
|
-
},
|
|
148
|
-
correlations: {
|
|
149
|
-
positive: ['GC'],
|
|
150
|
-
negative: ['DXY'],
|
|
151
|
-
related: ['SLV', 'XAGUSD']
|
|
152
|
-
},
|
|
153
|
-
sessions: {
|
|
154
|
-
most_active: ['london', 'us_open'],
|
|
155
|
-
avoid: ['asia_lunch']
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
CL: {
|
|
160
|
-
id: 'CL',
|
|
161
|
-
name: 'Crude Oil Futures',
|
|
162
|
-
exchange: 'NYMEX',
|
|
163
|
-
tickSize: 0.01,
|
|
164
|
-
tickValue: 10.00,
|
|
165
|
-
pointValue: 1000.00,
|
|
166
|
-
margin: 7000,
|
|
167
|
-
characteristics: {
|
|
168
|
-
volatility: 'high',
|
|
169
|
-
sector: 'energy',
|
|
170
|
-
behavior: 'news-driven, inventories, geopolitical, OPEC',
|
|
171
|
-
avgRange: '1.50-3.00 points'
|
|
172
|
-
},
|
|
173
|
-
correlations: {
|
|
174
|
-
positive: ['BZ', 'XLE'],
|
|
175
|
-
negative: [],
|
|
176
|
-
related: ['USO', 'WTI']
|
|
177
|
-
},
|
|
178
|
-
sessions: {
|
|
179
|
-
most_active: ['us_open', 'inventory_report'],
|
|
180
|
-
avoid: ['overnight_thin']
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Trading sessions with times (Eastern Time)
|
|
187
|
-
*/
|
|
188
|
-
const SESSIONS = {
|
|
189
|
-
asia_open: { start: '18:00', end: '20:00', description: 'Asia market open' },
|
|
190
|
-
asia_session: { start: '20:00', end: '03:00', description: 'Asia main session' },
|
|
191
|
-
asia_lunch: { start: '00:00', end: '01:00', description: 'Asia lunch (low volume)' },
|
|
192
|
-
london: { start: '03:00', end: '08:00', description: 'London session' },
|
|
193
|
-
london_us_overlap: { start: '08:00', end: '11:30', description: 'London/US overlap' },
|
|
194
|
-
us_open: { start: '09:30', end: '11:30', description: 'US market open (high volume)' },
|
|
195
|
-
us_midday: { start: '11:30', end: '14:00', description: 'US midday (lower volume)' },
|
|
196
|
-
us_afternoon: { start: '14:00', end: '15:00', description: 'US afternoon' },
|
|
197
|
-
us_close: { start: '15:00', end: '16:00', description: 'US close (rebalancing)' },
|
|
198
|
-
overnight: { start: '16:00', end: '18:00', description: 'Overnight transition' }
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Get symbol data by ID
|
|
203
|
-
*/
|
|
204
|
-
const getSymbol = (symbolId) => {
|
|
205
|
-
const key = symbolId?.toUpperCase?.()?.replace(/[0-9]/g, '') || '';
|
|
206
|
-
return SYMBOLS[key] || null;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get current session based on time
|
|
211
|
-
*/
|
|
212
|
-
const getCurrentSession = (date = new Date()) => {
|
|
213
|
-
const et = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' }));
|
|
214
|
-
const hours = et.getHours();
|
|
215
|
-
const minutes = et.getMinutes();
|
|
216
|
-
const time = hours * 60 + minutes;
|
|
217
|
-
|
|
218
|
-
for (const [name, session] of Object.entries(SESSIONS)) {
|
|
219
|
-
const [startH, startM] = session.start.split(':').map(Number);
|
|
220
|
-
const [endH, endM] = session.end.split(':').map(Number);
|
|
221
|
-
const start = startH * 60 + startM;
|
|
222
|
-
const end = endH * 60 + endM;
|
|
223
|
-
|
|
224
|
-
if (start <= end) {
|
|
225
|
-
if (time >= start && time < end) return { name, ...session };
|
|
226
|
-
} else {
|
|
227
|
-
if (time >= start || time < end) return { name, ...session };
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return { name: 'unknown', description: 'Unknown session' };
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Check if current time is good for trading a symbol
|
|
235
|
-
*/
|
|
236
|
-
const isGoodSessionForSymbol = (symbolId, date = new Date()) => {
|
|
237
|
-
const symbol = getSymbol(symbolId);
|
|
238
|
-
if (!symbol) return { good: true, reason: 'Unknown symbol' };
|
|
239
|
-
|
|
240
|
-
const session = getCurrentSession(date);
|
|
241
|
-
|
|
242
|
-
if (symbol.sessions.avoid?.includes(session.name)) {
|
|
243
|
-
return { good: false, reason: `${session.description} - typically low volume for ${symbolId}` };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (symbol.sessions.most_active?.includes(session.name)) {
|
|
247
|
-
return { good: true, reason: `${session.description} - optimal for ${symbolId}` };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return { good: true, reason: session.description };
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
module.exports = {
|
|
254
|
-
SYMBOLS,
|
|
255
|
-
SESSIONS,
|
|
256
|
-
getSymbol,
|
|
257
|
-
getCurrentSession,
|
|
258
|
-
isGoodSessionForSymbol
|
|
259
|
-
};
|