hedgequantx 2.7.89 → 2.7.91
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 +1 -1
- package/src/pages/algo/algo-executor.js +265 -0
- package/src/pages/algo/custom-strategy.js +93 -184
- package/src/pages/algo/one-account.js +7 -289
- package/src/pages/algo/ui.js +2 -2
- package/src/ui/menu.js +1 -1
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -133,7 +133,7 @@ const banner = async () => {
|
|
|
133
133
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
134
134
|
|
|
135
135
|
const tagline = isMobile ? `HQX V${version}` : `PROP FUTURES ALGO TRADING V${version}`;
|
|
136
|
-
console.log(chalk.cyan('║') + chalk.
|
|
136
|
+
console.log(chalk.cyan('║') + chalk.yellow(centerText(tagline, innerWidth)) + chalk.cyan('║'));
|
|
137
137
|
|
|
138
138
|
// ALWAYS close the banner
|
|
139
139
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algo Executor - Shared execution engine for all algo modes
|
|
3
|
+
* Handles market data, signals, orders, and P&L tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
8
|
+
const { M1 } = require('../../lib/m/s1');
|
|
9
|
+
const { MarketDataFeed } = require('../../lib/data');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute algo strategy with market data
|
|
13
|
+
* @param {Object} params - Execution parameters
|
|
14
|
+
* @param {Object} params.service - Trading service (Rithmic/ProjectX)
|
|
15
|
+
* @param {Object} params.account - Account object
|
|
16
|
+
* @param {Object} params.contract - Contract object
|
|
17
|
+
* @param {Object} params.config - Algo config (contracts, target, risk, showName)
|
|
18
|
+
* @param {Object} params.options - Optional: aiSupervision, aiProvider, askAI function
|
|
19
|
+
*/
|
|
20
|
+
const executeAlgo = async ({ service, account, contract, config, options = {} }) => {
|
|
21
|
+
const { contracts, dailyTarget, maxRisk, showName } = config;
|
|
22
|
+
const { aiSupervision, aiProvider, askAI, subtitle } = options;
|
|
23
|
+
|
|
24
|
+
const accountName = showName
|
|
25
|
+
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
26
|
+
: 'HQX *****';
|
|
27
|
+
const symbolName = contract.name;
|
|
28
|
+
const contractId = contract.id;
|
|
29
|
+
const tickSize = contract.tickSize || 0.25;
|
|
30
|
+
|
|
31
|
+
const ui = new AlgoUI({
|
|
32
|
+
subtitle: subtitle || (aiSupervision ? 'CUSTOM STRATEGY + AI' : 'HQX Ultra Scalping'),
|
|
33
|
+
mode: 'one-account'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const stats = {
|
|
37
|
+
accountName,
|
|
38
|
+
symbol: symbolName,
|
|
39
|
+
qty: contracts,
|
|
40
|
+
target: dailyTarget,
|
|
41
|
+
risk: maxRisk,
|
|
42
|
+
propfirm: account.propfirm || 'Unknown',
|
|
43
|
+
platform: account.platform || 'Rithmic',
|
|
44
|
+
pnl: 0,
|
|
45
|
+
trades: 0,
|
|
46
|
+
wins: 0,
|
|
47
|
+
losses: 0,
|
|
48
|
+
latency: 0,
|
|
49
|
+
connected: false,
|
|
50
|
+
startTime: Date.now()
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let running = true;
|
|
54
|
+
let stopReason = null;
|
|
55
|
+
let startingPnL = null;
|
|
56
|
+
let currentPosition = 0;
|
|
57
|
+
let pendingOrder = false;
|
|
58
|
+
let tickCount = 0;
|
|
59
|
+
|
|
60
|
+
// AI context for supervision
|
|
61
|
+
const aiContext = { recentTicks: [], recentSignals: [], maxTicks: 100 };
|
|
62
|
+
|
|
63
|
+
// Initialize Strategy
|
|
64
|
+
const strategy = new M1({ tickSize });
|
|
65
|
+
strategy.initialize(contractId, tickSize);
|
|
66
|
+
|
|
67
|
+
// Initialize Market Data Feed
|
|
68
|
+
const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
|
|
69
|
+
|
|
70
|
+
// Log startup
|
|
71
|
+
ui.addLog('info', `Strategy: ${aiSupervision ? 'CUSTOM + AI' : 'HQX Ultra Scalping'}`);
|
|
72
|
+
ui.addLog('info', `Account: ${accountName}`);
|
|
73
|
+
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
|
|
74
|
+
ui.addLog('info', `Target: $${dailyTarget} | Risk: $${maxRisk}`);
|
|
75
|
+
if (aiSupervision && aiProvider) ui.addLog('info', `AI: ${aiProvider.name} supervision`);
|
|
76
|
+
ui.addLog('info', 'Connecting to market data...');
|
|
77
|
+
|
|
78
|
+
// Handle strategy signals
|
|
79
|
+
strategy.on('signal', async (signal) => {
|
|
80
|
+
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
81
|
+
|
|
82
|
+
const { direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
83
|
+
|
|
84
|
+
aiContext.recentSignals.push({ ...signal, timestamp: Date.now() });
|
|
85
|
+
if (aiContext.recentSignals.length > 10) aiContext.recentSignals.shift();
|
|
86
|
+
|
|
87
|
+
ui.addLog('info', `Signal: ${direction.toUpperCase()} @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
88
|
+
|
|
89
|
+
// AI Supervision check
|
|
90
|
+
if (aiSupervision && askAI) {
|
|
91
|
+
const aiDecision = await askAI(aiContext, signal, { symbolName, currentPosition, stats, dailyTarget, maxRisk });
|
|
92
|
+
if (!aiDecision.approve) {
|
|
93
|
+
ui.addLog('info', `AI rejected: ${aiDecision.reason || 'No reason'}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
ui.addLog('info', `AI approved: ${aiDecision.reason || 'OK'}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Place order
|
|
100
|
+
pendingOrder = true;
|
|
101
|
+
try {
|
|
102
|
+
const orderSide = direction === 'long' ? 0 : 1;
|
|
103
|
+
const orderResult = await service.placeOrder({
|
|
104
|
+
accountId: account.accountId,
|
|
105
|
+
contractId: contractId,
|
|
106
|
+
type: 2,
|
|
107
|
+
side: orderSide,
|
|
108
|
+
size: contracts
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (orderResult.success) {
|
|
112
|
+
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
113
|
+
stats.trades++;
|
|
114
|
+
ui.addLog('fill_' + (direction === 'long' ? 'buy' : 'sell'),
|
|
115
|
+
`OPENED ${direction.toUpperCase()} ${contracts}x @ market`);
|
|
116
|
+
|
|
117
|
+
// Bracket orders
|
|
118
|
+
if (stopLoss && takeProfit) {
|
|
119
|
+
await service.placeOrder({
|
|
120
|
+
accountId: account.accountId, contractId, type: 4,
|
|
121
|
+
side: direction === 'long' ? 1 : 0, size: contracts, stopPrice: stopLoss
|
|
122
|
+
});
|
|
123
|
+
await service.placeOrder({
|
|
124
|
+
accountId: account.accountId, contractId, type: 1,
|
|
125
|
+
side: direction === 'long' ? 1 : 0, size: contracts, limitPrice: takeProfit
|
|
126
|
+
});
|
|
127
|
+
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
ui.addLog('error', `Order failed: ${orderResult.error}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
ui.addLog('error', `Order error: ${e.message}`);
|
|
134
|
+
}
|
|
135
|
+
pendingOrder = false;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle market data ticks
|
|
139
|
+
marketFeed.on('tick', (tick) => {
|
|
140
|
+
tickCount++;
|
|
141
|
+
const latencyStart = Date.now();
|
|
142
|
+
|
|
143
|
+
aiContext.recentTicks.push(tick);
|
|
144
|
+
if (aiContext.recentTicks.length > aiContext.maxTicks) aiContext.recentTicks.shift();
|
|
145
|
+
|
|
146
|
+
strategy.processTick({
|
|
147
|
+
contractId: tick.contractId || contractId,
|
|
148
|
+
price: tick.price, bid: tick.bid, ask: tick.ask,
|
|
149
|
+
volume: tick.volume || 1, side: tick.lastTradeSide || 'unknown',
|
|
150
|
+
timestamp: tick.timestamp || Date.now()
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
stats.latency = Date.now() - latencyStart;
|
|
154
|
+
if (tickCount % 100 === 0) ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
marketFeed.on('connected', () => { stats.connected = true; ui.addLog('connected', 'Market data connected!'); });
|
|
158
|
+
marketFeed.on('error', (err) => ui.addLog('error', `Market: ${err.message}`));
|
|
159
|
+
marketFeed.on('disconnected', () => { stats.connected = false; ui.addLog('error', 'Market disconnected'); });
|
|
160
|
+
|
|
161
|
+
// Connect to market data
|
|
162
|
+
try {
|
|
163
|
+
const token = service.token || service.getToken?.();
|
|
164
|
+
const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
165
|
+
await marketFeed.connect(token, propfirmKey, contractId);
|
|
166
|
+
await marketFeed.subscribe(symbolName, contractId);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Poll P&L
|
|
172
|
+
const pollPnL = async () => {
|
|
173
|
+
try {
|
|
174
|
+
const accountResult = await service.getTradingAccounts();
|
|
175
|
+
if (accountResult.success && accountResult.accounts) {
|
|
176
|
+
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
177
|
+
if (acc && acc.profitAndLoss !== undefined) {
|
|
178
|
+
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
179
|
+
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
180
|
+
if (stats.pnl !== 0) strategy.recordTradeResult(stats.pnl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const posResult = await service.getPositions(account.accountId);
|
|
185
|
+
if (posResult.success && posResult.positions) {
|
|
186
|
+
const pos = posResult.positions.find(p => {
|
|
187
|
+
const sym = p.contractId || p.symbol || '';
|
|
188
|
+
return sym.includes(contract.name) || sym.includes(contractId);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (pos && pos.quantity !== 0) {
|
|
192
|
+
currentPosition = pos.quantity;
|
|
193
|
+
const pnl = pos.profitAndLoss || 0;
|
|
194
|
+
if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
|
|
195
|
+
else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
|
|
196
|
+
} else {
|
|
197
|
+
currentPosition = 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (stats.pnl >= dailyTarget) {
|
|
202
|
+
stopReason = 'target'; running = false;
|
|
203
|
+
ui.addLog('fill_win', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
204
|
+
} else if (stats.pnl <= -maxRisk) {
|
|
205
|
+
stopReason = 'risk'; running = false;
|
|
206
|
+
ui.addLog('fill_loss', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (e) { /* silent */ }
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Start loops
|
|
212
|
+
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
213
|
+
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
214
|
+
pollPnL();
|
|
215
|
+
|
|
216
|
+
// Keyboard handler
|
|
217
|
+
const setupKeyHandler = () => {
|
|
218
|
+
if (!process.stdin.isTTY) return;
|
|
219
|
+
readline.emitKeypressEvents(process.stdin);
|
|
220
|
+
process.stdin.setRawMode(true);
|
|
221
|
+
process.stdin.resume();
|
|
222
|
+
|
|
223
|
+
const onKey = (str, key) => {
|
|
224
|
+
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
225
|
+
running = false; stopReason = 'manual';
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
process.stdin.on('keypress', onKey);
|
|
229
|
+
return () => {
|
|
230
|
+
process.stdin.removeListener('keypress', onKey);
|
|
231
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const cleanupKeys = setupKeyHandler();
|
|
236
|
+
|
|
237
|
+
// Wait for stop
|
|
238
|
+
await new Promise(resolve => {
|
|
239
|
+
const check = setInterval(() => { if (!running) { clearInterval(check); resolve(); } }, 100);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Cleanup
|
|
243
|
+
clearInterval(refreshInterval);
|
|
244
|
+
clearInterval(pnlInterval);
|
|
245
|
+
await marketFeed.disconnect();
|
|
246
|
+
if (cleanupKeys) cleanupKeys();
|
|
247
|
+
ui.cleanup();
|
|
248
|
+
|
|
249
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
250
|
+
process.stdin.resume();
|
|
251
|
+
|
|
252
|
+
// Duration
|
|
253
|
+
const durationMs = Date.now() - stats.startTime;
|
|
254
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
255
|
+
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
256
|
+
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
257
|
+
stats.duration = hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
258
|
+
|
|
259
|
+
renderSessionSummary(stats, stopReason);
|
|
260
|
+
|
|
261
|
+
console.log('\n Returning to menu in 3 seconds...');
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
module.exports = { executeAlgo };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Strategy - AI-powered strategy builder
|
|
3
|
-
*
|
|
3
|
+
* Config flow + AI chat to create strategy, then execute with AI supervision
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const chalk = require('chalk');
|
|
@@ -8,7 +8,6 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const ora = require('ora');
|
|
11
|
-
const readline = require('readline');
|
|
12
11
|
|
|
13
12
|
const { getLogoWidth, centerText, displayBanner } = require('../../ui');
|
|
14
13
|
const { prompts } = require('../../utils');
|
|
@@ -16,18 +15,16 @@ const { connections } = require('../../services');
|
|
|
16
15
|
const { getActiveProvider } = require('../ai-agents');
|
|
17
16
|
const cliproxy = require('../../services/cliproxy');
|
|
18
17
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
18
|
+
const { executeAlgo } = require('./algo-executor');
|
|
19
19
|
|
|
20
|
-
// Strategies directory
|
|
21
20
|
const STRATEGIES_DIR = path.join(os.homedir(), '.hqx', 'strategies');
|
|
22
21
|
|
|
23
|
-
/** Ensure strategies directory exists */
|
|
24
22
|
const ensureStrategiesDir = () => {
|
|
25
23
|
if (!fs.existsSync(STRATEGIES_DIR)) fs.mkdirSync(STRATEGIES_DIR, { recursive: true });
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
/** Custom Strategy Menu */
|
|
29
27
|
const customStrategyMenu = async (service) => {
|
|
30
|
-
// Check AI provider first
|
|
31
28
|
const aiProvider = getActiveProvider();
|
|
32
29
|
if (!aiProvider) {
|
|
33
30
|
console.log(chalk.red('\n No AI Agent connected. Go to AI Agents menu first.'));
|
|
@@ -35,47 +32,30 @@ const customStrategyMenu = async (service) => {
|
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
// Check market hours
|
|
39
35
|
const market = checkMarketHours();
|
|
40
36
|
if (!market.isOpen && !market.message.includes('early')) {
|
|
41
|
-
console.log();
|
|
42
|
-
console.log(chalk.
|
|
43
|
-
console.log(chalk.gray(' Custom strategy requires market to be open'));
|
|
44
|
-
console.log();
|
|
37
|
+
console.log(chalk.red(`\n ${market.message}`));
|
|
38
|
+
console.log(chalk.gray(' Custom strategy requires market to be open\n'));
|
|
45
39
|
await prompts.waitForEnter();
|
|
46
40
|
return;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
// Step 1: Fetch accounts
|
|
50
43
|
const spinner = ora({ text: 'Fetching active accounts...', color: 'yellow' }).start();
|
|
51
44
|
const allAccounts = await connections.getAllAccounts();
|
|
52
45
|
|
|
53
|
-
if (!allAccounts?.length) {
|
|
54
|
-
spinner.fail('No accounts found');
|
|
55
|
-
await prompts.waitForEnter();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
46
|
+
if (!allAccounts?.length) { spinner.fail('No accounts found'); await prompts.waitForEnter(); return; }
|
|
58
47
|
|
|
59
48
|
const activeAccounts = allAccounts.filter(acc => acc.status === 0);
|
|
60
|
-
if (!activeAccounts.length) {
|
|
61
|
-
spinner.fail('No active accounts');
|
|
62
|
-
await prompts.waitForEnter();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
49
|
+
if (!activeAccounts.length) { spinner.fail('No active accounts'); await prompts.waitForEnter(); return; }
|
|
65
50
|
|
|
66
51
|
spinner.succeed(`Found ${activeAccounts.length} active account(s)`);
|
|
67
52
|
|
|
68
|
-
// Step
|
|
69
|
-
console.log();
|
|
70
|
-
console.log(chalk.cyan.bold(' STEP 1: SELECT ACCOUNT'));
|
|
53
|
+
// Step 1: Select account
|
|
54
|
+
console.log(chalk.cyan.bold('\n STEP 1: SELECT ACCOUNT'));
|
|
71
55
|
const accountOptions = activeAccounts.map(acc => {
|
|
72
56
|
const name = acc.accountName || acc.rithmicAccountId || acc.accountId;
|
|
73
|
-
const balance = acc.balance !== null && acc.balance !== undefined
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`,
|
|
77
|
-
value: acc
|
|
78
|
-
};
|
|
57
|
+
const balance = acc.balance !== null && acc.balance !== undefined ? ` - $${acc.balance.toLocaleString()}` : '';
|
|
58
|
+
return { label: `${name} (${acc.propfirm || acc.platform || 'Unknown'})${balance}`, value: acc };
|
|
79
59
|
});
|
|
80
60
|
accountOptions.push({ label: '< Back', value: 'back' });
|
|
81
61
|
|
|
@@ -84,16 +64,13 @@ const customStrategyMenu = async (service) => {
|
|
|
84
64
|
|
|
85
65
|
const accountService = selectedAccount.service || connections.getServiceForAccount(selectedAccount.accountId) || service;
|
|
86
66
|
|
|
87
|
-
// Step
|
|
88
|
-
console.log();
|
|
89
|
-
console.log(chalk.cyan.bold(' STEP 2: SELECT SYMBOL'));
|
|
67
|
+
// Step 2: Select symbol
|
|
68
|
+
console.log(chalk.cyan.bold('\n STEP 2: SELECT SYMBOL'));
|
|
90
69
|
const contract = await selectSymbol(accountService);
|
|
91
70
|
if (!contract) return;
|
|
92
71
|
|
|
93
|
-
// Step
|
|
94
|
-
console.log();
|
|
95
|
-
console.log(chalk.cyan.bold(' STEP 3: CONFIGURE PARAMETERS'));
|
|
96
|
-
console.log();
|
|
72
|
+
// Step 3: Configure parameters
|
|
73
|
+
console.log(chalk.cyan.bold('\n STEP 3: CONFIGURE PARAMETERS\n'));
|
|
97
74
|
|
|
98
75
|
const contracts = await prompts.numberInput('Number of contracts:', 1, 1, 10);
|
|
99
76
|
if (contracts === null) return;
|
|
@@ -107,50 +84,30 @@ const customStrategyMenu = async (service) => {
|
|
|
107
84
|
const showName = await prompts.confirmPrompt('Show account name?', false);
|
|
108
85
|
if (showName === null) return;
|
|
109
86
|
|
|
110
|
-
// Step
|
|
111
|
-
console.log();
|
|
112
|
-
console.log(chalk.cyan.bold(' STEP 4: AI SUPERVISION'));
|
|
87
|
+
// Step 4: AI Supervision
|
|
88
|
+
console.log(chalk.cyan.bold('\n STEP 4: AI SUPERVISION'));
|
|
113
89
|
const aiSupervision = await prompts.confirmPrompt('Enable AI supervision during execution?', true);
|
|
114
90
|
if (aiSupervision === null) return;
|
|
115
91
|
|
|
116
|
-
const config = {
|
|
117
|
-
account: selectedAccount,
|
|
118
|
-
contract,
|
|
119
|
-
contracts,
|
|
120
|
-
dailyTarget,
|
|
121
|
-
maxRisk,
|
|
122
|
-
showName,
|
|
123
|
-
aiSupervision,
|
|
124
|
-
aiProvider
|
|
125
|
-
};
|
|
92
|
+
const config = { account: selectedAccount, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider };
|
|
126
93
|
|
|
127
|
-
// Step
|
|
128
|
-
console.log();
|
|
129
|
-
console.log(chalk.
|
|
130
|
-
console.log(chalk.gray(' Describe your trading strategy in plain English.'));
|
|
131
|
-
console.log(chalk.gray(' The AI will help you build and validate it.'));
|
|
132
|
-
console.log();
|
|
94
|
+
// Step 5: AI Chat
|
|
95
|
+
console.log(chalk.cyan.bold('\n STEP 5: CREATE YOUR STRATEGY WITH AI'));
|
|
96
|
+
console.log(chalk.gray(' Describe your trading strategy. AI will help build it.\n'));
|
|
133
97
|
|
|
134
98
|
await strategyChat(config, accountService);
|
|
135
99
|
};
|
|
136
100
|
|
|
137
|
-
/** Select symbol
|
|
101
|
+
/** Select symbol */
|
|
138
102
|
const selectSymbol = async (service) => {
|
|
139
103
|
const spinner = ora({ text: 'Loading symbols...', color: 'yellow' }).start();
|
|
140
104
|
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
143
|
-
spinner.fail('Failed to load contracts');
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let contracts = contractsResult.contracts;
|
|
105
|
+
const result = await service.getContracts();
|
|
106
|
+
if (!result.success || !result.contracts?.length) { spinner.fail('Failed to load contracts'); return null; }
|
|
148
107
|
|
|
149
|
-
// Sort popular indices first
|
|
150
108
|
const popular = ['ES', 'NQ', 'MES', 'MNQ', 'M2K', 'RTY', 'YM', 'MYM', 'NKD', 'GC', 'SI', 'CL'];
|
|
151
|
-
contracts.sort((a, b) => {
|
|
152
|
-
const baseA = a.baseSymbol || a.symbol || '';
|
|
153
|
-
const baseB = b.baseSymbol || b.symbol || '';
|
|
109
|
+
result.contracts.sort((a, b) => {
|
|
110
|
+
const baseA = a.baseSymbol || a.symbol || '', baseB = b.baseSymbol || b.symbol || '';
|
|
154
111
|
const idxA = popular.findIndex(p => baseA === p || baseA.startsWith(p));
|
|
155
112
|
const idxB = popular.findIndex(p => baseB === p || baseB.startsWith(p));
|
|
156
113
|
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
@@ -159,12 +116,9 @@ const selectSymbol = async (service) => {
|
|
|
159
116
|
return baseA.localeCompare(baseB);
|
|
160
117
|
});
|
|
161
118
|
|
|
162
|
-
spinner.succeed(`Found ${contracts.length} contracts`);
|
|
119
|
+
spinner.succeed(`Found ${result.contracts.length} contracts`);
|
|
163
120
|
|
|
164
|
-
const options = contracts.map(c => ({
|
|
165
|
-
label: `${c.symbol} - ${c.name} (${c.exchange})`,
|
|
166
|
-
value: c
|
|
167
|
-
}));
|
|
121
|
+
const options = result.contracts.map(c => ({ label: `${c.symbol} - ${c.name} (${c.exchange})`, value: c }));
|
|
168
122
|
options.push({ label: chalk.gray('< Back'), value: 'back' });
|
|
169
123
|
|
|
170
124
|
const selected = await prompts.selectOption(chalk.yellow('Select Symbol:'), options);
|
|
@@ -174,75 +128,45 @@ const selectSymbol = async (service) => {
|
|
|
174
128
|
/** AI Chat for strategy creation */
|
|
175
129
|
const strategyChat = async (config, service) => {
|
|
176
130
|
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
177
|
-
|
|
178
|
-
const accountName = showName
|
|
179
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
180
|
-
: 'HQX *****';
|
|
131
|
+
const accountName = showName ? (account.accountName || account.rithmicAccountId || account.accountId) : 'HQX *****';
|
|
181
132
|
|
|
182
133
|
console.clear();
|
|
183
134
|
displayBanner();
|
|
184
135
|
|
|
185
|
-
const
|
|
186
|
-
const W = boxWidth - 2;
|
|
187
|
-
|
|
136
|
+
const W = getLogoWidth() - 2;
|
|
188
137
|
console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
|
|
189
138
|
console.log(chalk.cyan('║') + chalk.green.bold(centerText('CUSTOM STRATEGY - AI CHAT', W)) + chalk.cyan('║'));
|
|
190
139
|
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
191
140
|
console.log(chalk.cyan('║') + centerText(`Account: ${accountName} | Symbol: ${contract.name} | Qty: ${contracts}`, W) + chalk.cyan('║'));
|
|
192
|
-
console.log(chalk.cyan('║') + centerText(`Target: $${dailyTarget} | Risk: $${maxRisk} | AI
|
|
193
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText(`
|
|
141
|
+
console.log(chalk.cyan('║') + centerText(`Target: $${dailyTarget} | Risk: $${maxRisk} | AI: ${aiSupervision ? 'ON' : 'OFF'}`, W) + chalk.cyan('║'));
|
|
142
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText(`Provider: ${aiProvider.name}`, W)) + chalk.cyan('║'));
|
|
194
143
|
console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
|
|
195
|
-
console.log(chalk.cyan('║') + chalk.gray(centerText('
|
|
196
|
-
console.log(chalk.cyan('╚' + '═'.repeat(W) + '
|
|
197
|
-
console.log();
|
|
144
|
+
console.log(chalk.cyan('║') + chalk.gray(centerText('"run" to execute, "save" to save, "cancel" to abort', W)) + chalk.cyan('║'));
|
|
145
|
+
console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝\n'));
|
|
198
146
|
|
|
199
|
-
const systemPrompt = `You are an expert algorithmic trading assistant
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
- Account: ${accountName}
|
|
203
|
-
- Symbol: ${contract.name} (${contract.symbol})
|
|
204
|
-
- Contracts: ${contracts}
|
|
205
|
-
- Daily Target: $${dailyTarget}
|
|
206
|
-
- Max Risk: $${maxRisk}
|
|
207
|
-
- AI Supervision: ${aiSupervision ? 'Enabled' : 'Disabled'}
|
|
208
|
-
|
|
209
|
-
Help the user create a trading strategy. When they describe what they want:
|
|
210
|
-
1. Understand their entry/exit logic
|
|
211
|
-
2. Validate the strategy makes sense
|
|
212
|
-
3. Suggest improvements if needed
|
|
213
|
-
4. When ready, confirm the strategy is valid
|
|
214
|
-
|
|
215
|
-
Keep responses concise (2-3 sentences max unless explaining strategy details).
|
|
216
|
-
When the user says "run", output: [STRATEGY_READY]
|
|
217
|
-
Include the strategy parameters in JSON format when ready.`;
|
|
147
|
+
const systemPrompt = `You are an expert algorithmic trading assistant.
|
|
148
|
+
Setup: ${accountName} | ${contract.name} | ${contracts} contracts | Target: $${dailyTarget} | Risk: $${maxRisk}
|
|
149
|
+
Help create a trading strategy. Be concise (2-3 sentences). When ready, say [STRATEGY_READY] with JSON config.`;
|
|
218
150
|
|
|
219
151
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
220
|
-
let strategyReady = false;
|
|
221
|
-
let strategyConfig = null;
|
|
152
|
+
let strategyReady = false, strategyConfig = null;
|
|
222
153
|
|
|
223
|
-
console.log(chalk.green(' AI: ') + `
|
|
224
|
-
console.log(chalk.green(' ') + 'What kind of trading strategy do you want to build?');
|
|
154
|
+
console.log(chalk.green(' AI: ') + `I'll help you create a custom strategy for ${contract.name}. What kind of strategy?`);
|
|
225
155
|
console.log();
|
|
226
156
|
|
|
227
157
|
while (true) {
|
|
228
158
|
const userInput = await prompts.textInput(chalk.yellow(' You: '));
|
|
229
|
-
|
|
230
159
|
if (!userInput) continue;
|
|
231
160
|
|
|
232
161
|
const cmd = userInput.toLowerCase().trim();
|
|
233
162
|
|
|
234
163
|
if (cmd === 'cancel' || cmd === 'exit' || cmd === 'quit') {
|
|
235
|
-
console.log(chalk.gray('\n
|
|
236
|
-
await prompts.waitForEnter();
|
|
237
|
-
return;
|
|
164
|
+
console.log(chalk.gray('\n Cancelled.')); await prompts.waitForEnter(); return;
|
|
238
165
|
}
|
|
239
166
|
|
|
240
167
|
if (cmd === 'save') {
|
|
241
|
-
if (strategyConfig)
|
|
242
|
-
|
|
243
|
-
} else {
|
|
244
|
-
console.log(chalk.yellow('\n No strategy to save yet. Keep describing your strategy.'));
|
|
245
|
-
}
|
|
168
|
+
if (strategyConfig) await saveStrategy(strategyConfig, config);
|
|
169
|
+
else console.log(chalk.yellow('\n No strategy to save yet.'));
|
|
246
170
|
continue;
|
|
247
171
|
}
|
|
248
172
|
|
|
@@ -251,114 +175,101 @@ Include the strategy parameters in JSON format when ready.`;
|
|
|
251
175
|
console.log(chalk.green('\n Launching strategy...'));
|
|
252
176
|
await launchCustomStrategy(config, strategyConfig, service);
|
|
253
177
|
return;
|
|
254
|
-
} else {
|
|
255
|
-
console.log(chalk.yellow('\n Strategy not ready yet. Describe your entry/exit conditions first.'));
|
|
256
|
-
continue;
|
|
257
178
|
}
|
|
179
|
+
console.log(chalk.yellow('\n Strategy not ready. Describe your entry/exit conditions first.'));
|
|
180
|
+
continue;
|
|
258
181
|
}
|
|
259
182
|
|
|
260
183
|
messages.push({ role: 'user', content: userInput });
|
|
261
|
-
|
|
262
184
|
const spinner = ora({ text: 'AI thinking...', color: 'yellow' }).start();
|
|
263
185
|
|
|
264
186
|
try {
|
|
265
187
|
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
266
188
|
const result = await cliproxy.chatCompletion(modelId, messages);
|
|
267
189
|
|
|
268
|
-
if (!result.success) {
|
|
269
|
-
spinner.fail(`AI Error: ${result.error}`);
|
|
270
|
-
messages.pop();
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
190
|
+
if (!result.success) { spinner.fail(`AI Error: ${result.error}`); messages.pop(); continue; }
|
|
273
191
|
|
|
274
192
|
const response = result.response?.choices?.[0]?.message?.content || '';
|
|
275
193
|
messages.push({ role: 'assistant', content: response });
|
|
276
|
-
|
|
277
194
|
spinner.stop();
|
|
278
|
-
console.log();
|
|
279
|
-
console.log(chalk.green(' AI: ') + formatResponse(response));
|
|
280
|
-
console.log();
|
|
281
195
|
|
|
282
|
-
|
|
196
|
+
console.log('\n' + chalk.green(' AI: ') + formatResponse(response) + '\n');
|
|
197
|
+
|
|
283
198
|
if (response.includes('[STRATEGY_READY]') || response.toLowerCase().includes('strategy is ready')) {
|
|
284
199
|
strategyReady = true;
|
|
285
|
-
// Try to extract JSON config
|
|
286
200
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
287
|
-
if (jsonMatch) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!strategyConfig) {
|
|
291
|
-
strategyConfig = { description: userInput, messages: messages.slice(1) };
|
|
292
|
-
}
|
|
293
|
-
console.log(chalk.cyan(' [Strategy ready! Type "run" to execute or "save" to save for later]'));
|
|
294
|
-
console.log();
|
|
201
|
+
if (jsonMatch) try { strategyConfig = JSON.parse(jsonMatch[0]); } catch (e) {}
|
|
202
|
+
if (!strategyConfig) strategyConfig = { description: userInput, messages: messages.slice(1) };
|
|
203
|
+
console.log(chalk.cyan(' [Strategy ready! "run" to execute or "save" to save]\n'));
|
|
295
204
|
}
|
|
296
|
-
|
|
297
|
-
} catch (e) {
|
|
298
|
-
spinner.fail(`Error: ${e.message}`);
|
|
299
|
-
messages.pop();
|
|
300
|
-
}
|
|
205
|
+
} catch (e) { spinner.fail(`Error: ${e.message}`); messages.pop(); }
|
|
301
206
|
}
|
|
302
207
|
};
|
|
303
208
|
|
|
304
|
-
|
|
305
|
-
const getDefaultModel = (providerId) => {
|
|
306
|
-
const defaults = { anthropic: 'claude-sonnet-4-20250514', google: 'gemini-2.5-pro', openai: 'gpt-4o' };
|
|
307
|
-
return defaults[providerId] || 'claude-sonnet-4-20250514';
|
|
308
|
-
};
|
|
209
|
+
const getDefaultModel = (id) => ({ anthropic: 'claude-sonnet-4-20250514', google: 'gemini-2.5-pro', openai: 'gpt-4o' }[id] || 'claude-sonnet-4-20250514');
|
|
309
210
|
|
|
310
|
-
/** Format AI response */
|
|
311
211
|
const formatResponse = (text) => {
|
|
312
|
-
const
|
|
313
|
-
const lines = clean.split('\n');
|
|
212
|
+
const lines = text.replace(/\[STRATEGY_READY\]/g, '').trim().split('\n');
|
|
314
213
|
return lines.map((l, i) => i === 0 ? l : ' ' + l).join('\n');
|
|
315
214
|
};
|
|
316
215
|
|
|
317
|
-
/** Save strategy
|
|
216
|
+
/** Save strategy */
|
|
318
217
|
const saveStrategy = async (strategyConfig, config) => {
|
|
319
218
|
ensureStrategiesDir();
|
|
320
|
-
|
|
321
219
|
const name = await prompts.textInput(chalk.cyan(' Strategy name: '));
|
|
322
|
-
if (!name
|
|
323
|
-
console.log(chalk.gray(' Save cancelled.'));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
220
|
+
if (!name?.trim()) { console.log(chalk.gray(' Save cancelled.')); return; }
|
|
326
221
|
|
|
327
222
|
const folderName = name.trim().toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
328
223
|
const strategyPath = path.join(STRATEGIES_DIR, folderName);
|
|
329
|
-
|
|
330
224
|
if (!fs.existsSync(strategyPath)) fs.mkdirSync(strategyPath, { recursive: true });
|
|
331
225
|
|
|
332
226
|
const configFile = {
|
|
333
|
-
name: name.trim(),
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
dailyTarget: config.dailyTarget,
|
|
337
|
-
maxRisk: config.maxRisk,
|
|
338
|
-
aiSupervision: config.aiSupervision,
|
|
339
|
-
strategy: strategyConfig,
|
|
340
|
-
createdAt: new Date().toISOString()
|
|
227
|
+
name: name.trim(), symbol: config.contract.name, contracts: config.contracts,
|
|
228
|
+
dailyTarget: config.dailyTarget, maxRisk: config.maxRisk, aiSupervision: config.aiSupervision,
|
|
229
|
+
strategy: strategyConfig, createdAt: new Date().toISOString()
|
|
341
230
|
};
|
|
342
231
|
|
|
343
232
|
fs.writeFileSync(path.join(strategyPath, 'config.json'), JSON.stringify(configFile, null, 2));
|
|
344
|
-
console.log(chalk.green(`\n ✓
|
|
233
|
+
console.log(chalk.green(`\n ✓ Saved: ${strategyPath}`));
|
|
345
234
|
};
|
|
346
235
|
|
|
347
|
-
/** Launch custom strategy
|
|
236
|
+
/** Launch custom strategy with AI supervision */
|
|
348
237
|
const launchCustomStrategy = async (config, strategyConfig, service) => {
|
|
349
238
|
const { account, contract, contracts, dailyTarget, maxRisk, showName, aiSupervision, aiProvider } = config;
|
|
350
239
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
240
|
+
// AI supervision function
|
|
241
|
+
const askAI = async (aiContext, signal, ctx) => {
|
|
242
|
+
if (!aiSupervision) return { approve: true };
|
|
243
|
+
|
|
244
|
+
const prompt = `Trading supervisor check:
|
|
245
|
+
Symbol: ${ctx.symbolName} | Position: ${ctx.currentPosition === 0 ? 'FLAT' : (ctx.currentPosition > 0 ? 'LONG' : 'SHORT')}
|
|
246
|
+
P&L: $${ctx.stats.pnl.toFixed(2)} | Trades: ${ctx.stats.trades} (W:${ctx.stats.wins} L:${ctx.stats.losses})
|
|
247
|
+
Strategy: ${JSON.stringify(strategyConfig.description || strategyConfig).substring(0, 200)}
|
|
248
|
+
Signal: ${signal.direction.toUpperCase()} @ ${signal.entry.toFixed(2)} (${(signal.confidence * 100).toFixed(0)}%)
|
|
249
|
+
Recent prices: ${aiContext.recentTicks.slice(-5).map(t => t.price?.toFixed(2)).join(', ') || 'N/A'}
|
|
250
|
+
Reply JSON: {"approve": true/false, "reason": "brief"}`;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const modelId = aiProvider.modelId || getDefaultModel(aiProvider.id);
|
|
254
|
+
const result = await cliproxy.chatCompletion(modelId, [
|
|
255
|
+
{ role: 'system', content: 'Trading supervisor. JSON only.' },
|
|
256
|
+
{ role: 'user', content: prompt }
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
if (result.success) {
|
|
260
|
+
const content = result.response?.choices?.[0]?.message?.content || '';
|
|
261
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
262
|
+
if (match) return JSON.parse(match[0]);
|
|
263
|
+
}
|
|
264
|
+
} catch (e) { /* fallback */ }
|
|
265
|
+
return { approve: true, reason: 'AI unavailable' };
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await executeAlgo({
|
|
269
|
+
service, account, contract,
|
|
270
|
+
config: { contracts, dailyTarget, maxRisk, showName },
|
|
271
|
+
options: { aiSupervision, aiProvider, askAI, subtitle: 'CUSTOM STRATEGY + AI' }
|
|
272
|
+
});
|
|
362
273
|
};
|
|
363
274
|
|
|
364
275
|
/** Load saved strategies */
|
|
@@ -368,9 +279,7 @@ const loadStrategies = () => {
|
|
|
368
279
|
const items = fs.readdirSync(STRATEGIES_DIR, { withFileTypes: true });
|
|
369
280
|
return items.filter(i => i.isDirectory()).map(dir => {
|
|
370
281
|
const configPath = path.join(STRATEGIES_DIR, dir.name, 'config.json');
|
|
371
|
-
if (fs.existsSync(configPath)) {
|
|
372
|
-
return { folder: dir.name, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
373
|
-
}
|
|
282
|
+
if (fs.existsSync(configPath)) return { folder: dir.name, ...JSON.parse(fs.readFileSync(configPath, 'utf8')) };
|
|
374
283
|
return { folder: dir.name, name: dir.name };
|
|
375
284
|
});
|
|
376
285
|
} catch (e) { return []; }
|
|
@@ -4,16 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const ora = require('ora');
|
|
7
|
-
const readline = require('readline');
|
|
8
7
|
|
|
9
8
|
const { connections } = require('../../services');
|
|
10
|
-
const { AlgoUI, renderSessionSummary } = require('./ui');
|
|
11
9
|
const { prompts } = require('../../utils');
|
|
12
10
|
const { checkMarketHours } = require('../../services/rithmic/market');
|
|
13
|
-
|
|
14
|
-
// Strategy & Market Data
|
|
15
|
-
const { M1 } = require('../../lib/m/s1');
|
|
16
|
-
const { MarketDataFeed } = require('../../lib/data');
|
|
11
|
+
const { executeAlgo } = require('./algo-executor');
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
|
|
@@ -80,7 +75,12 @@ const oneAccountMenu = async (service) => {
|
|
|
80
75
|
const config = await configureAlgo(selectedAccount, contract);
|
|
81
76
|
if (!config) return;
|
|
82
77
|
|
|
83
|
-
await
|
|
78
|
+
await executeAlgo({
|
|
79
|
+
service: accountService,
|
|
80
|
+
account: selectedAccount,
|
|
81
|
+
contract,
|
|
82
|
+
config
|
|
83
|
+
});
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -158,286 +158,4 @@ const configureAlgo = async (account, contract) => {
|
|
|
158
158
|
return { contracts, dailyTarget, maxRisk, showName };
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
/**
|
|
162
|
-
* Launch algo trading - HQX Ultra Scalping Strategy
|
|
163
|
-
* Real-time market data + Strategy signals + Auto order execution
|
|
164
|
-
*/
|
|
165
|
-
const launchAlgo = async (service, account, contract, config) => {
|
|
166
|
-
const { contracts, dailyTarget, maxRisk, showName } = config;
|
|
167
|
-
|
|
168
|
-
// Use RAW API fields
|
|
169
|
-
const accountName = showName
|
|
170
|
-
? (account.accountName || account.rithmicAccountId || account.accountId)
|
|
171
|
-
: 'HQX *****';
|
|
172
|
-
const symbolName = contract.name;
|
|
173
|
-
const contractId = contract.id;
|
|
174
|
-
const connectionType = account.platform || 'Rithmic';
|
|
175
|
-
const tickSize = contract.tickSize || 0.25;
|
|
176
|
-
|
|
177
|
-
const ui = new AlgoUI({ subtitle: 'HQX Ultra Scalping', mode: 'one-account' });
|
|
178
|
-
|
|
179
|
-
const stats = {
|
|
180
|
-
accountName,
|
|
181
|
-
symbol: symbolName,
|
|
182
|
-
qty: contracts,
|
|
183
|
-
target: dailyTarget,
|
|
184
|
-
risk: maxRisk,
|
|
185
|
-
propfirm: account.propfirm || 'Unknown',
|
|
186
|
-
platform: connectionType,
|
|
187
|
-
pnl: 0,
|
|
188
|
-
trades: 0,
|
|
189
|
-
wins: 0,
|
|
190
|
-
losses: 0,
|
|
191
|
-
latency: 0,
|
|
192
|
-
connected: false,
|
|
193
|
-
startTime: Date.now()
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
let running = true;
|
|
197
|
-
let stopReason = null;
|
|
198
|
-
let startingPnL = null;
|
|
199
|
-
let currentPosition = 0; // Current position qty (+ long, - short)
|
|
200
|
-
let pendingOrder = false; // Prevent duplicate orders
|
|
201
|
-
let tickCount = 0;
|
|
202
|
-
|
|
203
|
-
// Initialize Strategy
|
|
204
|
-
const strategy = new M1({ tickSize });
|
|
205
|
-
strategy.initialize(contractId, tickSize);
|
|
206
|
-
|
|
207
|
-
// Initialize Market Data Feed
|
|
208
|
-
const marketFeed = new MarketDataFeed({ propfirm: account.propfirm });
|
|
209
|
-
|
|
210
|
-
// Log startup
|
|
211
|
-
ui.addLog('info', `Connection: ${connectionType}`);
|
|
212
|
-
ui.addLog('info', `Account: ${accountName}`);
|
|
213
|
-
ui.addLog('info', `Symbol: ${symbolName} | Qty: ${contracts}`);
|
|
214
|
-
ui.addLog('info', `Target: $${dailyTarget} | Max Risk: $${maxRisk}`);
|
|
215
|
-
ui.addLog('info', 'Connecting to market data...');
|
|
216
|
-
|
|
217
|
-
// Handle strategy signals
|
|
218
|
-
strategy.on('signal', async (signal) => {
|
|
219
|
-
if (!running || pendingOrder || currentPosition !== 0) return;
|
|
220
|
-
|
|
221
|
-
const { side, direction, entry, stopLoss, takeProfit, confidence } = signal;
|
|
222
|
-
|
|
223
|
-
ui.addLog('signal', `${direction.toUpperCase()} signal @ ${entry.toFixed(2)} (${(confidence * 100).toFixed(0)}%)`);
|
|
224
|
-
|
|
225
|
-
// Place order via API
|
|
226
|
-
pendingOrder = true;
|
|
227
|
-
try {
|
|
228
|
-
const orderSide = direction === 'long' ? 0 : 1; // 0=Buy, 1=Sell
|
|
229
|
-
const orderResult = await service.placeOrder({
|
|
230
|
-
accountId: account.accountId,
|
|
231
|
-
contractId: contractId,
|
|
232
|
-
type: 2, // Market order
|
|
233
|
-
side: orderSide,
|
|
234
|
-
size: contracts
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
if (orderResult.success) {
|
|
238
|
-
currentPosition = direction === 'long' ? contracts : -contracts;
|
|
239
|
-
stats.trades++;
|
|
240
|
-
ui.addLog('trade', `OPENED ${direction.toUpperCase()} ${contracts}x @ market`);
|
|
241
|
-
|
|
242
|
-
// Place bracket orders (SL/TP)
|
|
243
|
-
if (stopLoss && takeProfit) {
|
|
244
|
-
// Stop Loss
|
|
245
|
-
await service.placeOrder({
|
|
246
|
-
accountId: account.accountId,
|
|
247
|
-
contractId: contractId,
|
|
248
|
-
type: 4, // Stop order
|
|
249
|
-
side: direction === 'long' ? 1 : 0, // Opposite side
|
|
250
|
-
size: contracts,
|
|
251
|
-
stopPrice: stopLoss
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Take Profit
|
|
255
|
-
await service.placeOrder({
|
|
256
|
-
accountId: account.accountId,
|
|
257
|
-
contractId: contractId,
|
|
258
|
-
type: 1, // Limit order
|
|
259
|
-
side: direction === 'long' ? 1 : 0,
|
|
260
|
-
size: contracts,
|
|
261
|
-
limitPrice: takeProfit
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
ui.addLog('info', `SL: ${stopLoss.toFixed(2)} | TP: ${takeProfit.toFixed(2)}`);
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
ui.addLog('error', `Order failed: ${orderResult.error}`);
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
ui.addLog('error', `Order error: ${e.message}`);
|
|
271
|
-
}
|
|
272
|
-
pendingOrder = false;
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Handle market data ticks
|
|
276
|
-
marketFeed.on('tick', (tick) => {
|
|
277
|
-
tickCount++;
|
|
278
|
-
const latencyStart = Date.now();
|
|
279
|
-
|
|
280
|
-
// Feed tick to strategy
|
|
281
|
-
strategy.processTick({
|
|
282
|
-
contractId: tick.contractId || contractId,
|
|
283
|
-
price: tick.price,
|
|
284
|
-
bid: tick.bid,
|
|
285
|
-
ask: tick.ask,
|
|
286
|
-
volume: tick.volume || 1,
|
|
287
|
-
side: tick.lastTradeSide || 'unknown',
|
|
288
|
-
timestamp: tick.timestamp || Date.now()
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
stats.latency = Date.now() - latencyStart;
|
|
292
|
-
|
|
293
|
-
// Log every 100th tick to show activity
|
|
294
|
-
if (tickCount % 100 === 0) {
|
|
295
|
-
ui.addLog('info', `Tick #${tickCount} @ ${tick.price?.toFixed(2) || 'N/A'}`);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
marketFeed.on('connected', () => {
|
|
300
|
-
stats.connected = true;
|
|
301
|
-
ui.addLog('success', 'Market data connected!');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
marketFeed.on('error', (err) => {
|
|
305
|
-
ui.addLog('error', `Market: ${err.message}`);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
marketFeed.on('disconnected', () => {
|
|
309
|
-
stats.connected = false;
|
|
310
|
-
ui.addLog('error', 'Market data disconnected');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Connect to market data
|
|
314
|
-
try {
|
|
315
|
-
const token = service.token || service.getToken?.();
|
|
316
|
-
const propfirmKey = (account.propfirm || 'topstep').toLowerCase().replace(/\s+/g, '_');
|
|
317
|
-
await marketFeed.connect(token, propfirmKey, contractId);
|
|
318
|
-
await marketFeed.subscribe(symbolName, contractId);
|
|
319
|
-
} catch (e) {
|
|
320
|
-
ui.addLog('error', `Failed to connect: ${e.message}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Poll account P&L from API
|
|
324
|
-
const pollPnL = async () => {
|
|
325
|
-
try {
|
|
326
|
-
const accountResult = await service.getTradingAccounts();
|
|
327
|
-
if (accountResult.success && accountResult.accounts) {
|
|
328
|
-
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
329
|
-
if (acc && acc.profitAndLoss !== undefined) {
|
|
330
|
-
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
331
|
-
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
332
|
-
|
|
333
|
-
// Record trade result in strategy
|
|
334
|
-
if (stats.pnl !== 0) {
|
|
335
|
-
strategy.recordTradeResult(stats.pnl);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Check positions
|
|
341
|
-
const posResult = await service.getPositions(account.accountId);
|
|
342
|
-
if (posResult.success && posResult.positions) {
|
|
343
|
-
const pos = posResult.positions.find(p => {
|
|
344
|
-
const sym = p.contractId || p.symbol || '';
|
|
345
|
-
return sym.includes(contract.name) || sym.includes(contractId);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (pos && pos.quantity !== 0) {
|
|
349
|
-
currentPosition = pos.quantity;
|
|
350
|
-
const side = pos.quantity > 0 ? 'LONG' : 'SHORT';
|
|
351
|
-
const pnl = pos.profitAndLoss || 0;
|
|
352
|
-
|
|
353
|
-
// Check if position closed (win/loss)
|
|
354
|
-
if (pnl > 0) stats.wins = Math.max(stats.wins, 1);
|
|
355
|
-
else if (pnl < 0) stats.losses = Math.max(stats.losses, 1);
|
|
356
|
-
} else {
|
|
357
|
-
currentPosition = 0;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Check target/risk
|
|
362
|
-
if (stats.pnl >= dailyTarget) {
|
|
363
|
-
stopReason = 'target';
|
|
364
|
-
running = false;
|
|
365
|
-
ui.addLog('success', `TARGET REACHED! +$${stats.pnl.toFixed(2)}`);
|
|
366
|
-
} else if (stats.pnl <= -maxRisk) {
|
|
367
|
-
stopReason = 'risk';
|
|
368
|
-
running = false;
|
|
369
|
-
ui.addLog('error', `MAX RISK! -$${Math.abs(stats.pnl).toFixed(2)}`);
|
|
370
|
-
}
|
|
371
|
-
} catch (e) {
|
|
372
|
-
// Silently handle polling errors
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Start polling and UI refresh
|
|
377
|
-
const refreshInterval = setInterval(() => { if (running) ui.render(stats); }, 250);
|
|
378
|
-
const pnlInterval = setInterval(() => { if (running) pollPnL(); }, 2000);
|
|
379
|
-
pollPnL(); // Initial poll
|
|
380
|
-
|
|
381
|
-
// Keyboard handler
|
|
382
|
-
const setupKeyHandler = () => {
|
|
383
|
-
if (!process.stdin.isTTY) return;
|
|
384
|
-
readline.emitKeypressEvents(process.stdin);
|
|
385
|
-
process.stdin.setRawMode(true);
|
|
386
|
-
process.stdin.resume();
|
|
387
|
-
|
|
388
|
-
const onKey = (str, key) => {
|
|
389
|
-
if (key && (key.name === 'x' || key.name === 'X' || (key.ctrl && key.name === 'c'))) {
|
|
390
|
-
running = false;
|
|
391
|
-
stopReason = 'manual';
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
process.stdin.on('keypress', onKey);
|
|
395
|
-
return () => {
|
|
396
|
-
process.stdin.removeListener('keypress', onKey);
|
|
397
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
398
|
-
};
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const cleanupKeys = setupKeyHandler();
|
|
402
|
-
|
|
403
|
-
// Wait for stop
|
|
404
|
-
await new Promise(resolve => {
|
|
405
|
-
const check = setInterval(() => {
|
|
406
|
-
if (!running) {
|
|
407
|
-
clearInterval(check);
|
|
408
|
-
resolve();
|
|
409
|
-
}
|
|
410
|
-
}, 100);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
// Cleanup
|
|
414
|
-
clearInterval(refreshInterval);
|
|
415
|
-
clearInterval(pnlInterval);
|
|
416
|
-
await marketFeed.disconnect();
|
|
417
|
-
if (cleanupKeys) cleanupKeys();
|
|
418
|
-
ui.cleanup();
|
|
419
|
-
|
|
420
|
-
if (process.stdin.isTTY) {
|
|
421
|
-
process.stdin.setRawMode(false);
|
|
422
|
-
}
|
|
423
|
-
process.stdin.resume();
|
|
424
|
-
|
|
425
|
-
// Duration
|
|
426
|
-
const durationMs = Date.now() - stats.startTime;
|
|
427
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
428
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
429
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
430
|
-
stats.duration = hours > 0
|
|
431
|
-
? `${hours}h ${minutes}m ${seconds}s`
|
|
432
|
-
: minutes > 0
|
|
433
|
-
? `${minutes}m ${seconds}s`
|
|
434
|
-
: `${seconds}s`;
|
|
435
|
-
|
|
436
|
-
// Summary
|
|
437
|
-
renderSessionSummary(stats, stopReason);
|
|
438
|
-
|
|
439
|
-
console.log('\n Returning to menu in 3 seconds...');
|
|
440
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
441
|
-
};
|
|
442
|
-
|
|
443
161
|
module.exports = { oneAccountMenu };
|
package/src/pages/algo/ui.js
CHANGED
|
@@ -127,7 +127,7 @@ class AlgoUI {
|
|
|
127
127
|
|
|
128
128
|
// Separator + title
|
|
129
129
|
this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
|
|
130
|
-
this._line(chalk.cyan(BOX.V) + chalk.
|
|
130
|
+
this._line(chalk.cyan(BOX.V) + chalk.yellow(center(`PROP FUTURES ALGO TRADING V${version}`, W)) + chalk.cyan(BOX.V));
|
|
131
131
|
this._line(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
|
|
132
132
|
this._line(chalk.cyan(BOX.V) + chalk.yellow(center((this.config.subtitle || 'HQX ALGO TRADING').toUpperCase(), W)) + chalk.cyan(BOX.V));
|
|
133
133
|
}
|
|
@@ -387,7 +387,7 @@ const renderSessionSummary = (stats, stopReason) => {
|
|
|
387
387
|
|
|
388
388
|
// Separator + title
|
|
389
389
|
console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
|
|
390
|
-
console.log(chalk.cyan(BOX.V) + chalk.
|
|
390
|
+
console.log(chalk.cyan(BOX.V) + chalk.yellow(center(`PROP FUTURES ALGO TRADING V${version}`, W)) + chalk.cyan(BOX.V));
|
|
391
391
|
console.log(chalk.cyan(BOX.ML + BOX.H.repeat(W) + BOX.MR));
|
|
392
392
|
console.log(chalk.cyan(BOX.V) + chalk.yellow(center('SESSION SUMMARY', W)) + chalk.cyan(BOX.V));
|
|
393
393
|
|
package/src/ui/menu.js
CHANGED
|
@@ -48,7 +48,7 @@ const createBoxMenu = async (title, items, options = {}) => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
51
|
-
console.log(chalk.cyan('║') + chalk.
|
|
51
|
+
console.log(chalk.cyan('║') + chalk.yellow(centerText(`PROP FUTURES ALGO TRADING V${version}`, innerWidth)) + chalk.cyan('║'));
|
|
52
52
|
|
|
53
53
|
// Stats bar if provided
|
|
54
54
|
if (options.statsLine) {
|