hedgequantx 2.6.150 → 2.6.151
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
CHANGED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Strategy - AI-Generated Trading Strategy
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. User describes strategy in natural language
|
|
6
|
+
* 2. Connected AI agent generates the code
|
|
7
|
+
* 3. Agent tests and validates the code
|
|
8
|
+
* 4. User confirms, enters TARGET/RISK
|
|
9
|
+
* 5. Execution same as one-account
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
|
|
20
|
+
const { connections } = require('../../services');
|
|
21
|
+
const { AlgoUI, renderSessionSummary, renderMultiSymbolSummary } = require('./ui');
|
|
22
|
+
const { prompts } = require('../../utils');
|
|
23
|
+
const { checkMarketHours } = require('../../services/projectx/market');
|
|
24
|
+
const { FAST_SCALPING } = require('../../config/settings');
|
|
25
|
+
const { PositionManager } = require('../../services/position-manager');
|
|
26
|
+
const { RithmicMarketDataFeed } = require('../../services/rithmic/market-data');
|
|
27
|
+
const { algoLogger } = require('./logger');
|
|
28
|
+
const { recoveryMath } = require('../../services/strategy/recovery-math');
|
|
29
|
+
const aiService = require('../../services/ai');
|
|
30
|
+
const { launchMultiSymbolRithmic } = require('./one-account');
|
|
31
|
+
|
|
32
|
+
// Strategy template that the AI will fill
|
|
33
|
+
const STRATEGY_TEMPLATE = `/**
|
|
34
|
+
* Custom Strategy: {{STRATEGY_NAME}}
|
|
35
|
+
* Generated by AI Agent
|
|
36
|
+
*
|
|
37
|
+
* Description: {{STRATEGY_DESCRIPTION}}
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const EventEmitter = require('events');
|
|
41
|
+
|
|
42
|
+
class CustomStrategy extends EventEmitter {
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
this.tickSize = null;
|
|
46
|
+
this.tickValue = null;
|
|
47
|
+
this.contractId = null;
|
|
48
|
+
this.initialized = false;
|
|
49
|
+
this.tickCount = 0;
|
|
50
|
+
this.lastSignalTime = 0;
|
|
51
|
+
this.cooldownMs = 5000;
|
|
52
|
+
|
|
53
|
+
// Strategy-specific state
|
|
54
|
+
{{STRATEGY_STATE}}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
initialize(contractId, tickSize, tickValue) {
|
|
58
|
+
if (!contractId || tickSize === undefined || tickValue === undefined) {
|
|
59
|
+
throw new Error('Strategy requires contractId, tickSize, and tickValue from API');
|
|
60
|
+
}
|
|
61
|
+
this.contractId = contractId;
|
|
62
|
+
this.tickSize = tickSize;
|
|
63
|
+
this.tickValue = tickValue;
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
processTick(tick) {
|
|
68
|
+
if (!this.initialized) return;
|
|
69
|
+
this.tickCount++;
|
|
70
|
+
|
|
71
|
+
const { price, bid, ask, volume, side, timestamp } = tick;
|
|
72
|
+
|
|
73
|
+
// Strategy logic
|
|
74
|
+
{{STRATEGY_LOGIC}}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_emitSignal(direction, confidence, entry) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (now - this.lastSignalTime < this.cooldownMs) return;
|
|
80
|
+
this.lastSignalTime = now;
|
|
81
|
+
|
|
82
|
+
const signal = {
|
|
83
|
+
id: \`custom-\${now}\`,
|
|
84
|
+
timestamp: now,
|
|
85
|
+
contractId: this.contractId,
|
|
86
|
+
direction,
|
|
87
|
+
confidence,
|
|
88
|
+
entry,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.emit('signal', signal);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getState() {
|
|
95
|
+
return {
|
|
96
|
+
tickCount: this.tickCount,
|
|
97
|
+
{{STRATEGY_GET_STATE}}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getModelValues() {
|
|
102
|
+
return this.getState();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
reset() {
|
|
106
|
+
this.tickCount = 0;
|
|
107
|
+
this.lastSignalTime = 0;
|
|
108
|
+
{{STRATEGY_RESET}}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { CustomStrategy };
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get strategy directory
|
|
117
|
+
*/
|
|
118
|
+
function getStrategyDir() {
|
|
119
|
+
const dir = path.join(os.homedir(), '.hqx', 'strategies');
|
|
120
|
+
if (!fs.existsSync(dir)) {
|
|
121
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
return dir;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate strategy code using AI agent
|
|
128
|
+
*/
|
|
129
|
+
async function generateStrategyCode(description, agentName) {
|
|
130
|
+
const agent = aiService.getAgents().find(a => a.name === agentName || a.provider === agentName);
|
|
131
|
+
if (!agent) {
|
|
132
|
+
return { success: false, error: 'No AI agent available' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const prompt = `You are a trading strategy code generator. Generate a trading strategy based on this description:
|
|
136
|
+
|
|
137
|
+
"${description}"
|
|
138
|
+
|
|
139
|
+
You must output ONLY valid JavaScript code that fills in these template sections:
|
|
140
|
+
|
|
141
|
+
1. STRATEGY_NAME: A short name for the strategy (string)
|
|
142
|
+
2. STRATEGY_DESCRIPTION: One line description (string)
|
|
143
|
+
3. STRATEGY_STATE: Variable declarations for strategy state (e.g., "this.ema = 0; this.prices = [];")
|
|
144
|
+
4. STRATEGY_LOGIC: The main logic that processes each tick and calls this._emitSignal(direction, confidence, price) when conditions are met
|
|
145
|
+
- direction: 'long' or 'short'
|
|
146
|
+
- confidence: number between 0 and 1
|
|
147
|
+
- price: the entry price
|
|
148
|
+
5. STRATEGY_GET_STATE: Return additional state variables (e.g., "ema: this.ema,")
|
|
149
|
+
6. STRATEGY_RESET: Reset strategy-specific state (e.g., "this.ema = 0; this.prices = [];")
|
|
150
|
+
|
|
151
|
+
Available in processTick:
|
|
152
|
+
- tick.price, tick.bid, tick.ask, tick.volume, tick.side, tick.timestamp
|
|
153
|
+
- this.tickSize, this.tickValue (contract specs)
|
|
154
|
+
- this.tickCount (number of ticks processed)
|
|
155
|
+
|
|
156
|
+
Output format (JSON):
|
|
157
|
+
{
|
|
158
|
+
"STRATEGY_NAME": "...",
|
|
159
|
+
"STRATEGY_DESCRIPTION": "...",
|
|
160
|
+
"STRATEGY_STATE": "...",
|
|
161
|
+
"STRATEGY_LOGIC": "...",
|
|
162
|
+
"STRATEGY_GET_STATE": "...",
|
|
163
|
+
"STRATEGY_RESET": "..."
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
IMPORTANT: Output ONLY the JSON, no markdown, no explanation.`;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const response = await aiService.chat(agentName, [
|
|
170
|
+
{ role: 'user', content: prompt }
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (!response.success) {
|
|
174
|
+
return { success: false, error: response.error || 'AI request failed' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Parse JSON response
|
|
178
|
+
let strategyParts;
|
|
179
|
+
try {
|
|
180
|
+
// Extract JSON from response (might have markdown)
|
|
181
|
+
let jsonStr = response.content || response.message || '';
|
|
182
|
+
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
183
|
+
if (jsonMatch) {
|
|
184
|
+
jsonStr = jsonMatch[0];
|
|
185
|
+
}
|
|
186
|
+
strategyParts = JSON.parse(jsonStr);
|
|
187
|
+
} catch (parseError) {
|
|
188
|
+
return { success: false, error: `Failed to parse AI response: ${parseError.message}` };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build the code
|
|
192
|
+
let code = STRATEGY_TEMPLATE;
|
|
193
|
+
code = code.replace('{{STRATEGY_NAME}}', strategyParts.STRATEGY_NAME || 'Custom Strategy');
|
|
194
|
+
code = code.replace('{{STRATEGY_DESCRIPTION}}', strategyParts.STRATEGY_DESCRIPTION || description);
|
|
195
|
+
code = code.replace('{{STRATEGY_STATE}}', strategyParts.STRATEGY_STATE || '');
|
|
196
|
+
code = code.replace('{{STRATEGY_LOGIC}}', strategyParts.STRATEGY_LOGIC || '// No logic generated');
|
|
197
|
+
code = code.replace('{{STRATEGY_GET_STATE}}', strategyParts.STRATEGY_GET_STATE || '');
|
|
198
|
+
code = code.replace('{{STRATEGY_RESET}}', strategyParts.STRATEGY_RESET || '');
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
code,
|
|
203
|
+
name: strategyParts.STRATEGY_NAME || 'Custom Strategy',
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return { success: false, error: error.message };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate generated strategy code
|
|
212
|
+
*/
|
|
213
|
+
async function validateStrategyCode(code, filepath) {
|
|
214
|
+
const errors = [];
|
|
215
|
+
|
|
216
|
+
// 1. Write to file
|
|
217
|
+
try {
|
|
218
|
+
fs.writeFileSync(filepath, code, 'utf8');
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { success: false, errors: [`Failed to write file: ${e.message}`] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 2. Syntax check - try to require it
|
|
224
|
+
try {
|
|
225
|
+
// Clear require cache
|
|
226
|
+
delete require.cache[require.resolve(filepath)];
|
|
227
|
+
const mod = require(filepath);
|
|
228
|
+
|
|
229
|
+
if (!mod.CustomStrategy) {
|
|
230
|
+
errors.push('Module does not export CustomStrategy class');
|
|
231
|
+
} else {
|
|
232
|
+
// 3. Instantiation test
|
|
233
|
+
const instance = new mod.CustomStrategy();
|
|
234
|
+
|
|
235
|
+
// 4. Check required methods
|
|
236
|
+
if (typeof instance.initialize !== 'function') {
|
|
237
|
+
errors.push('Missing initialize() method');
|
|
238
|
+
}
|
|
239
|
+
if (typeof instance.processTick !== 'function') {
|
|
240
|
+
errors.push('Missing processTick() method');
|
|
241
|
+
}
|
|
242
|
+
if (typeof instance.getState !== 'function') {
|
|
243
|
+
errors.push('Missing getState() method');
|
|
244
|
+
}
|
|
245
|
+
if (typeof instance.on !== 'function') {
|
|
246
|
+
errors.push('Strategy must extend EventEmitter');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 5. Dry-run with fake tick
|
|
250
|
+
try {
|
|
251
|
+
instance.initialize('TEST', 0.25, 12.50);
|
|
252
|
+
instance.processTick({
|
|
253
|
+
price: 5000,
|
|
254
|
+
bid: 4999.75,
|
|
255
|
+
ask: 5000.25,
|
|
256
|
+
volume: 1,
|
|
257
|
+
side: 'BUY',
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
// Process a few more ticks
|
|
261
|
+
for (let i = 0; i < 10; i++) {
|
|
262
|
+
instance.processTick({
|
|
263
|
+
price: 5000 + (Math.random() - 0.5) * 2,
|
|
264
|
+
bid: 4999.75,
|
|
265
|
+
ask: 5000.25,
|
|
266
|
+
volume: 1,
|
|
267
|
+
side: Math.random() > 0.5 ? 'BUY' : 'SELL',
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
errors.push(`Runtime error in processTick: ${e.message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
errors.push(`Syntax/Import error: ${e.message}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (errors.length > 0) {
|
|
280
|
+
return { success: false, errors };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { success: true, errors: [] };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Custom Strategy Menu
|
|
288
|
+
*/
|
|
289
|
+
const customStrategyMenu = async (service) => {
|
|
290
|
+
console.clear();
|
|
291
|
+
|
|
292
|
+
// Check if AI agent is connected
|
|
293
|
+
const agents = aiService.getAgents();
|
|
294
|
+
if (!agents || agents.length === 0) {
|
|
295
|
+
console.log(chalk.red('\n No AI agent connected.'));
|
|
296
|
+
console.log(chalk.gray(' Connect an AI agent in AI SETTINGS first.\n'));
|
|
297
|
+
await prompts.waitForEnter();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const agentName = agents[0].name || agents[0].provider;
|
|
302
|
+
console.log(chalk.cyan('\n ╔════════════════════════════════════════════════════════════╗'));
|
|
303
|
+
console.log(chalk.cyan(' ║') + chalk.yellow.bold(' CUSTOM STRATEGY - AI GENERATED ') + chalk.cyan('║'));
|
|
304
|
+
console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
|
|
305
|
+
console.log(chalk.cyan(' ║') + chalk.gray(` Agent: ${agentName.padEnd(51)}`) + chalk.cyan('║'));
|
|
306
|
+
console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
|
|
307
|
+
|
|
308
|
+
// Step 1: Get strategy description from user
|
|
309
|
+
console.log(chalk.yellow(' Describe your trading strategy in natural language:'));
|
|
310
|
+
console.log(chalk.gray(' (Example: "Buy when price crosses above 20-period EMA and RSI < 30")'));
|
|
311
|
+
console.log(chalk.gray(' (Example: "Scalp long when 3 consecutive green ticks with increasing volume")\n'));
|
|
312
|
+
|
|
313
|
+
const description = await prompts.textInput(chalk.cyan(' STRATEGY: '));
|
|
314
|
+
if (!description || description.trim().length < 10) {
|
|
315
|
+
console.log(chalk.red('\n Strategy description too short. Minimum 10 characters.\n'));
|
|
316
|
+
await prompts.waitForEnter();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Step 2: Generate strategy code
|
|
321
|
+
console.log(chalk.cyan('\n Generating strategy code...'));
|
|
322
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
323
|
+
let spinnerIdx = 0;
|
|
324
|
+
const spinnerInterval = setInterval(() => {
|
|
325
|
+
process.stdout.write(`\r ${spinner[spinnerIdx++ % spinner.length]} Generating...`);
|
|
326
|
+
}, 100);
|
|
327
|
+
|
|
328
|
+
const genResult = await generateStrategyCode(description, agentName);
|
|
329
|
+
clearInterval(spinnerInterval);
|
|
330
|
+
process.stdout.write('\r \r');
|
|
331
|
+
|
|
332
|
+
if (!genResult.success) {
|
|
333
|
+
console.log(chalk.red(`\n Failed to generate strategy: ${genResult.error}\n`));
|
|
334
|
+
await prompts.waitForEnter();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(chalk.green(` ✓ Strategy "${genResult.name}" generated`));
|
|
339
|
+
|
|
340
|
+
// Step 3: Save and validate
|
|
341
|
+
const timestamp = Date.now();
|
|
342
|
+
const filename = `custom_${timestamp}.js`;
|
|
343
|
+
const filepath = path.join(getStrategyDir(), filename);
|
|
344
|
+
|
|
345
|
+
console.log(chalk.cyan(' Validating strategy...'));
|
|
346
|
+
|
|
347
|
+
const validation = await validateStrategyCode(genResult.code, filepath);
|
|
348
|
+
|
|
349
|
+
if (!validation.success) {
|
|
350
|
+
console.log(chalk.red('\n Strategy validation FAILED:'));
|
|
351
|
+
for (const err of validation.errors) {
|
|
352
|
+
console.log(chalk.red(` - ${err}`));
|
|
353
|
+
}
|
|
354
|
+
console.log(chalk.gray(`\n Code saved to: ${filepath}`));
|
|
355
|
+
console.log(chalk.gray(' You can manually fix and retry.\n'));
|
|
356
|
+
await prompts.waitForEnter();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log(chalk.green(' ✓ Strategy validated successfully'));
|
|
361
|
+
console.log(chalk.gray(` Saved to: ${filepath}\n`));
|
|
362
|
+
|
|
363
|
+
// Step 4: Show strategy summary and confirm
|
|
364
|
+
console.log(chalk.cyan(' ╔════════════════════════════════════════════════════════════╗'));
|
|
365
|
+
console.log(chalk.cyan(' ║') + chalk.green.bold(' STRATEGY READY ') + chalk.cyan('║'));
|
|
366
|
+
console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
|
|
367
|
+
console.log(chalk.cyan(' ║') + chalk.white(` Name: ${genResult.name.substring(0, 51).padEnd(51)}`) + chalk.cyan('║'));
|
|
368
|
+
console.log(chalk.cyan(' ║') + chalk.gray(` File: ${filename.padEnd(51)}`) + chalk.cyan('║'));
|
|
369
|
+
console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
|
|
370
|
+
|
|
371
|
+
const confirm = await prompts.textInput(chalk.cyan(' Continue with this strategy? (Y/n): '));
|
|
372
|
+
if (confirm.toLowerCase() === 'n') {
|
|
373
|
+
console.log(chalk.yellow('\n Cancelled.\n'));
|
|
374
|
+
await prompts.waitForEnter();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Step 5: Continue with normal one-account flow
|
|
379
|
+
// Load the custom strategy
|
|
380
|
+
const CustomStrategyModule = require(filepath);
|
|
381
|
+
|
|
382
|
+
// Pass to execution with the custom strategy
|
|
383
|
+
await executeWithCustomStrategy(service, CustomStrategyModule.CustomStrategy, genResult.name);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Execute trading with custom strategy
|
|
388
|
+
* Same as one-account but uses the custom strategy class
|
|
389
|
+
*/
|
|
390
|
+
async function executeWithCustomStrategy(service, StrategyClass, strategyName) {
|
|
391
|
+
// Import one-account's configuration prompts
|
|
392
|
+
const { getLogoWidth, drawBoxHeaderContinue, drawBoxFooter, displayBanner } = require('../../ui');
|
|
393
|
+
|
|
394
|
+
// Get accounts
|
|
395
|
+
const accountsResult = await service.getTradingAccounts();
|
|
396
|
+
if (!accountsResult.success || !accountsResult.accounts?.length) {
|
|
397
|
+
console.log(chalk.red('\n No trading accounts available.\n'));
|
|
398
|
+
await prompts.waitForEnter();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Account selection
|
|
403
|
+
console.log(chalk.cyan('\n SELECT ACCOUNT:\n'));
|
|
404
|
+
accountsResult.accounts.forEach((acc, i) => {
|
|
405
|
+
const balance = acc.balance !== null ? `$${acc.balance.toLocaleString()}` : 'N/A';
|
|
406
|
+
console.log(chalk.cyan(` [${i + 1}]`) + chalk.white(` ${acc.name || acc.accountId}`) + chalk.gray(` - ${balance}`));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const accChoice = await prompts.numberInput('\n ACCOUNT #:', 1, 1, accountsResult.accounts.length);
|
|
410
|
+
if (accChoice === null) return;
|
|
411
|
+
const account = accountsResult.accounts[accChoice - 1];
|
|
412
|
+
|
|
413
|
+
// Symbol selection
|
|
414
|
+
const symbolInput = await prompts.textInput(chalk.cyan(' SYMBOL (e.g., MES, MNQ): '));
|
|
415
|
+
if (!symbolInput) return;
|
|
416
|
+
const symbols = symbolInput.toUpperCase().split(',').map(s => s.trim()).filter(Boolean);
|
|
417
|
+
|
|
418
|
+
// Target and Risk
|
|
419
|
+
const dailyTarget = await prompts.numberInput(' TARGET ($):', 1000, 100, 50000);
|
|
420
|
+
if (dailyTarget === null) return;
|
|
421
|
+
|
|
422
|
+
const maxRisk = await prompts.numberInput(' MAX RISK ($):', 500, 50, 10000);
|
|
423
|
+
if (maxRisk === null) return;
|
|
424
|
+
|
|
425
|
+
// Confirm
|
|
426
|
+
console.log(chalk.cyan('\n ╔════════════════════════════════════════════════════════════╗'));
|
|
427
|
+
console.log(chalk.cyan(' ║') + chalk.yellow.bold(' CONFIRM SETTINGS ') + chalk.cyan('║'));
|
|
428
|
+
console.log(chalk.cyan(' ╠════════════════════════════════════════════════════════════╣'));
|
|
429
|
+
console.log(chalk.cyan(' ║') + chalk.white(` Strategy: ${strategyName.substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
|
|
430
|
+
console.log(chalk.cyan(' ║') + chalk.white(` Account: ${(account.name || account.accountId).substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
|
|
431
|
+
console.log(chalk.cyan(' ║') + chalk.white(` Symbols: ${symbols.join(', ').substring(0, 47).padEnd(47)}`) + chalk.cyan('║'));
|
|
432
|
+
console.log(chalk.cyan(' ║') + chalk.green(` Target: $${dailyTarget.toLocaleString().padEnd(46)}`) + chalk.cyan('║'));
|
|
433
|
+
console.log(chalk.cyan(' ║') + chalk.red(` Risk: $${maxRisk.toLocaleString().padEnd(46)}`) + chalk.cyan('║'));
|
|
434
|
+
console.log(chalk.cyan(' ╚════════════════════════════════════════════════════════════╝\n'));
|
|
435
|
+
|
|
436
|
+
const startConfirm = await prompts.textInput(chalk.cyan(' START TRADING? (Y/n): '));
|
|
437
|
+
if (startConfirm.toLowerCase() === 'n') {
|
|
438
|
+
console.log(chalk.yellow('\n Cancelled.\n'));
|
|
439
|
+
await prompts.waitForEnter();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Launch with custom strategy
|
|
444
|
+
console.log(chalk.green('\n Starting custom strategy trading...\n'));
|
|
445
|
+
|
|
446
|
+
// Get front month contracts for each symbol
|
|
447
|
+
const contracts = [];
|
|
448
|
+
for (const symbol of symbols) {
|
|
449
|
+
// Get front month contract from Rithmic API
|
|
450
|
+
const frontMonth = await service.getFrontMonthContract(symbol);
|
|
451
|
+
if (frontMonth && frontMonth.success) {
|
|
452
|
+
contracts.push({
|
|
453
|
+
name: frontMonth.symbol || symbol,
|
|
454
|
+
symbol: frontMonth.symbol || symbol,
|
|
455
|
+
exchange: frontMonth.exchange || 'CME',
|
|
456
|
+
id: frontMonth.contractId || symbol,
|
|
457
|
+
tickSize: frontMonth.tickSize,
|
|
458
|
+
tickValue: frontMonth.tickValue,
|
|
459
|
+
qty: 1, // Default 1 contract per symbol
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
// Fallback - use symbol directly
|
|
463
|
+
contracts.push({
|
|
464
|
+
name: symbol,
|
|
465
|
+
symbol: symbol,
|
|
466
|
+
exchange: 'CME',
|
|
467
|
+
id: symbol,
|
|
468
|
+
tickSize: null,
|
|
469
|
+
tickValue: null,
|
|
470
|
+
qty: 1,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (contracts.length === 0) {
|
|
476
|
+
console.log(chalk.red('\n No valid contracts found.\n'));
|
|
477
|
+
await prompts.waitForEnter();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Config for launchMultiSymbolRithmic
|
|
482
|
+
const config = {
|
|
483
|
+
dailyTarget,
|
|
484
|
+
maxRisk,
|
|
485
|
+
showName: true,
|
|
486
|
+
enableAI: false, // Custom strategy doesn't need AI supervisor (it IS the AI strategy)
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Launch with custom strategy class
|
|
490
|
+
await launchMultiSymbolRithmic(service, account, contracts, config, StrategyClass);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = { customStrategyMenu, generateStrategyCode, validateStrategyCode };
|
package/src/pages/algo/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const { checkMarketHours } = require('../../services/projectx/market');
|
|
|
9
9
|
|
|
10
10
|
const { oneAccountMenu } = require('./one-account');
|
|
11
11
|
const { copyTradingMenu } = require('./copy-trading');
|
|
12
|
+
const { customStrategyMenu } = require('./custom-strategy');
|
|
13
|
+
const aiService = require('../../services/ai');
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Algo Trading Menu - Simplified
|
|
@@ -59,13 +61,23 @@ const algoTradingMenu = async (service) => {
|
|
|
59
61
|
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
60
62
|
};
|
|
61
63
|
|
|
64
|
+
// Check if AI agent is connected
|
|
65
|
+
const agents = aiService.getAgents();
|
|
66
|
+
const hasAgent = agents && agents.length > 0;
|
|
67
|
+
|
|
62
68
|
console.clear();
|
|
63
69
|
displayBanner();
|
|
64
70
|
drawBoxHeaderContinue('ALGO TRADING', boxWidth);
|
|
65
71
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Menu options - show CUSTOM STRATEGY only if agent connected
|
|
73
|
+
let menuText, plainLen;
|
|
74
|
+
if (hasAgent) {
|
|
75
|
+
menuText = chalk.cyan('[1]') + chalk.yellow(' ONE ACCOUNT ') + chalk.cyan('[2]') + chalk.yellow(' COPY TRADING ') + chalk.cyan('[3]') + chalk.green(' CUSTOM STRATEGY');
|
|
76
|
+
plainLen = '[1] ONE ACCOUNT [2] COPY TRADING [3] CUSTOM STRATEGY'.length;
|
|
77
|
+
} else {
|
|
78
|
+
menuText = chalk.cyan('[1]') + chalk.yellow(' ONE ACCOUNT ') + chalk.cyan('[2]') + chalk.yellow(' COPY TRADING');
|
|
79
|
+
plainLen = '[1] ONE ACCOUNT [2] COPY TRADING'.length;
|
|
80
|
+
}
|
|
69
81
|
const leftPad = Math.floor((W - plainLen) / 2);
|
|
70
82
|
const rightPad = W - plainLen - leftPad;
|
|
71
83
|
console.log(chalk.cyan('║') + ' '.repeat(leftPad) + menuText + ' '.repeat(rightPad) + chalk.cyan('║'));
|
|
@@ -92,6 +104,15 @@ const algoTradingMenu = async (service) => {
|
|
|
92
104
|
await prompts.waitForEnter();
|
|
93
105
|
}
|
|
94
106
|
return algoTradingMenu(service);
|
|
107
|
+
} else if (choice === '3' && hasAgent) {
|
|
108
|
+
try {
|
|
109
|
+
await customStrategyMenu(service);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.log(chalk.red(`\n ERROR: ${err.message}`));
|
|
112
|
+
console.log(chalk.gray(` ${err.stack}`));
|
|
113
|
+
await prompts.waitForEnter();
|
|
114
|
+
}
|
|
115
|
+
return algoTradingMenu(service);
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
// Empty or other = back
|
|
@@ -1428,8 +1428,9 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
1428
1428
|
* @param {Object} account - Trading account
|
|
1429
1429
|
* @param {Array} contracts - Array of contracts to trade
|
|
1430
1430
|
* @param {Object} config - Algo configuration
|
|
1431
|
+
* @param {Function} [CustomStrategyClass] - Optional custom strategy class (if not provided, uses hftStrategy)
|
|
1431
1432
|
*/
|
|
1432
|
-
const launchMultiSymbolRithmic = async (service, account, contracts, config) => {
|
|
1433
|
+
const launchMultiSymbolRithmic = async (service, account, contracts, config, CustomStrategyClass = null) => {
|
|
1433
1434
|
const { dailyTarget, maxRisk, showName, enableAI } = config;
|
|
1434
1435
|
|
|
1435
1436
|
const accountName = showName
|
|
@@ -1553,7 +1554,13 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
|
|
|
1553
1554
|
const { tickSize, tickValue, contractId } = contractInfoMap[symbolName];
|
|
1554
1555
|
|
|
1555
1556
|
// Create strategy instance for this symbol
|
|
1556
|
-
|
|
1557
|
+
// Use custom strategy class if provided, otherwise use default hftStrategy
|
|
1558
|
+
let strategy;
|
|
1559
|
+
if (CustomStrategyClass) {
|
|
1560
|
+
strategy = new CustomStrategyClass();
|
|
1561
|
+
} else {
|
|
1562
|
+
strategy = Object.create(hftStrategy);
|
|
1563
|
+
}
|
|
1557
1564
|
if (tickSize !== null && tickValue !== null) {
|
|
1558
1565
|
strategy.initialize(contractId, tickSize, tickValue);
|
|
1559
1566
|
}
|
|
@@ -2187,4 +2194,4 @@ const launchMultiSymbolRithmic = async (service, account, contracts, config) =>
|
|
|
2187
2194
|
await prompts.waitForEnter();
|
|
2188
2195
|
};
|
|
2189
2196
|
|
|
2190
|
-
module.exports = { oneAccountMenu };
|
|
2197
|
+
module.exports = { oneAccountMenu, launchMultiSymbolRithmic };
|