hedgequantx 2.4.31 → 2.4.33
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/pages/algo/one-account.js +70 -22
- package/src/pages/stats.js +12 -31
- package/src/services/projectx/index.js +36 -0
package/package.json
CHANGED
|
@@ -206,6 +206,8 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
206
206
|
let currentPosition = 0; // Current position qty (+ long, - short)
|
|
207
207
|
let pendingOrder = false; // Prevent duplicate orders
|
|
208
208
|
let tickCount = 0;
|
|
209
|
+
let lastTradeCount = 0; // Track number of trades from API
|
|
210
|
+
let lastPositionQty = 0; // Track position changes
|
|
209
211
|
|
|
210
212
|
// Initialize Strategy (M1 is singleton instance)
|
|
211
213
|
const strategy = M1;
|
|
@@ -301,16 +303,23 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
301
303
|
tps++;
|
|
302
304
|
const latencyStart = Date.now();
|
|
303
305
|
|
|
306
|
+
// Debug: log first tick to see structure
|
|
307
|
+
if (tickCount === 1) {
|
|
308
|
+
algoLogger.info(ui, 'FIRST TICK', `price=${tick.price} bid=${tick.bid} ask=${tick.ask} vol=${tick.volume}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
304
311
|
// Feed tick to strategy
|
|
305
|
-
|
|
312
|
+
const tickData = {
|
|
306
313
|
contractId: tick.contractId || contractId,
|
|
307
|
-
price: tick.price,
|
|
314
|
+
price: tick.price || tick.lastPrice || tick.bid,
|
|
308
315
|
bid: tick.bid,
|
|
309
316
|
ask: tick.ask,
|
|
310
|
-
volume: tick.volume || 1,
|
|
311
|
-
side: tick.lastTradeSide || 'unknown',
|
|
317
|
+
volume: tick.volume || tick.size || 1,
|
|
318
|
+
side: tick.lastTradeSide || tick.side || 'unknown',
|
|
312
319
|
timestamp: tick.timestamp || Date.now()
|
|
313
|
-
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
strategy.processTick(tickData);
|
|
314
323
|
|
|
315
324
|
stats.latency = Date.now() - latencyStart;
|
|
316
325
|
|
|
@@ -368,24 +377,20 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
368
377
|
algoLogger.error(ui, 'CONNECTION ERROR', e.message.substring(0, 50));
|
|
369
378
|
}
|
|
370
379
|
|
|
371
|
-
// Poll account P&L from API
|
|
380
|
+
// Poll account P&L and sync with real trades from API
|
|
372
381
|
const pollPnL = async () => {
|
|
373
382
|
try {
|
|
383
|
+
// Get account P&L
|
|
374
384
|
const accountResult = await service.getTradingAccounts();
|
|
375
385
|
if (accountResult.success && accountResult.accounts) {
|
|
376
386
|
const acc = accountResult.accounts.find(a => a.accountId === account.accountId);
|
|
377
387
|
if (acc && acc.profitAndLoss !== undefined) {
|
|
378
388
|
if (startingPnL === null) startingPnL = acc.profitAndLoss;
|
|
379
389
|
stats.pnl = acc.profitAndLoss - startingPnL;
|
|
380
|
-
|
|
381
|
-
// Record trade result in strategy
|
|
382
|
-
if (stats.pnl !== 0) {
|
|
383
|
-
strategy.recordTradeResult(stats.pnl);
|
|
384
|
-
}
|
|
385
390
|
}
|
|
386
391
|
}
|
|
387
392
|
|
|
388
|
-
// Check positions
|
|
393
|
+
// Check positions - detect when position closes
|
|
389
394
|
const posResult = await service.getPositions(account.accountId);
|
|
390
395
|
if (posResult.success && posResult.positions) {
|
|
391
396
|
const pos = posResult.positions.find(p => {
|
|
@@ -393,24 +398,67 @@ const launchAlgo = async (service, account, contract, config) => {
|
|
|
393
398
|
return sym.includes(contract.name) || sym.includes(contractId);
|
|
394
399
|
});
|
|
395
400
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
const newPositionQty = pos?.quantity || 0;
|
|
402
|
+
|
|
403
|
+
// Position just closed - cancel remaining orders and log result
|
|
404
|
+
if (lastPositionQty !== 0 && newPositionQty === 0) {
|
|
405
|
+
// Cancel all open orders to prevent new positions
|
|
406
|
+
try {
|
|
407
|
+
await service.cancelAllOrders(account.accountId);
|
|
408
|
+
algoLogger.info(ui, 'ORDERS CANCELLED', 'Position closed - brackets removed');
|
|
409
|
+
} catch (e) {
|
|
410
|
+
// Silent fail
|
|
411
|
+
}
|
|
400
412
|
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
413
|
+
// Get real trade data from API
|
|
414
|
+
try {
|
|
415
|
+
const tradesResult = await service.getTrades(account.accountId);
|
|
416
|
+
if (tradesResult.success && tradesResult.trades?.length > 0) {
|
|
417
|
+
// Count completed trades (those with profitAndLoss not null)
|
|
418
|
+
const completedTrades = tradesResult.trades.filter(t => t.profitAndLoss !== null);
|
|
419
|
+
|
|
420
|
+
// Update stats from real trades
|
|
421
|
+
let wins = 0, losses = 0;
|
|
422
|
+
for (const trade of completedTrades) {
|
|
423
|
+
if (trade.profitAndLoss > 0) wins++;
|
|
424
|
+
else if (trade.profitAndLoss < 0) losses++;
|
|
425
|
+
}
|
|
426
|
+
stats.trades = completedTrades.length;
|
|
427
|
+
stats.wins = wins;
|
|
428
|
+
stats.losses = losses;
|
|
429
|
+
|
|
430
|
+
// Log the trade that just closed
|
|
431
|
+
const lastTrade = completedTrades[completedTrades.length - 1];
|
|
432
|
+
if (lastTrade) {
|
|
433
|
+
const pnl = lastTrade.profitAndLoss || 0;
|
|
434
|
+
const side = lastTrade.side === 0 ? 'LONG' : 'SHORT';
|
|
435
|
+
const exitPrice = lastTrade.price || 0;
|
|
436
|
+
|
|
437
|
+
if (pnl >= 0) {
|
|
438
|
+
algoLogger.targetHit(ui, symbolName, exitPrice, pnl);
|
|
439
|
+
} else {
|
|
440
|
+
algoLogger.stopHit(ui, symbolName, exitPrice, Math.abs(pnl));
|
|
441
|
+
}
|
|
442
|
+
algoLogger.positionClosed(ui, symbolName, side, contracts, exitPrice, pnl);
|
|
443
|
+
|
|
444
|
+
// Record in strategy for adaptation
|
|
445
|
+
strategy.recordTradeResult(pnl);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {
|
|
449
|
+
// Silent fail - trades API might not be available
|
|
450
|
+
}
|
|
406
451
|
}
|
|
452
|
+
|
|
453
|
+
lastPositionQty = newPositionQty;
|
|
454
|
+
currentPosition = newPositionQty;
|
|
407
455
|
}
|
|
408
456
|
|
|
409
457
|
// Check target/risk
|
|
410
458
|
if (stats.pnl >= dailyTarget) {
|
|
411
459
|
stopReason = 'target';
|
|
412
460
|
running = false;
|
|
413
|
-
algoLogger.
|
|
461
|
+
algoLogger.info(ui, 'DAILY TARGET REACHED', `+$${stats.pnl.toFixed(2)}`);
|
|
414
462
|
} else if (stats.pnl <= -maxRisk) {
|
|
415
463
|
stopReason = 'risk';
|
|
416
464
|
running = false;
|
package/src/pages/stats.js
CHANGED
|
@@ -186,8 +186,8 @@ const showStats = async (service) => {
|
|
|
186
186
|
} catch (e) {}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// ========== AGGREGATE STATS FROM API DATA ==========
|
|
190
|
-
//
|
|
189
|
+
// ========== AGGREGATE STATS FROM TRADE HISTORY (API DATA) ==========
|
|
190
|
+
// Calculate stats from actual trades to ensure consistency with displayed trades
|
|
191
191
|
|
|
192
192
|
let stats = {
|
|
193
193
|
totalTrades: 0, winningTrades: 0, losingTrades: 0,
|
|
@@ -197,27 +197,8 @@ const showStats = async (service) => {
|
|
|
197
197
|
longTrades: 0, shortTrades: 0, longWins: 0, shortWins: 0
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
if (account.lifetimeStats) {
|
|
203
|
-
const s = account.lifetimeStats;
|
|
204
|
-
stats.totalTrades += s.totalTrades || 0;
|
|
205
|
-
stats.winningTrades += s.winningTrades || 0;
|
|
206
|
-
stats.losingTrades += s.losingTrades || 0;
|
|
207
|
-
stats.totalWinAmount += s.totalWinAmount || 0;
|
|
208
|
-
stats.totalLossAmount += s.totalLossAmount || 0;
|
|
209
|
-
stats.bestTrade = Math.max(stats.bestTrade, s.bestTrade || 0);
|
|
210
|
-
stats.worstTrade = Math.min(stats.worstTrade, s.worstTrade || 0);
|
|
211
|
-
stats.totalVolume += s.totalVolume || 0;
|
|
212
|
-
stats.maxConsecutiveWins = Math.max(stats.maxConsecutiveWins, s.maxConsecutiveWins || 0);
|
|
213
|
-
stats.maxConsecutiveLosses = Math.max(stats.maxConsecutiveLosses, s.maxConsecutiveLosses || 0);
|
|
214
|
-
stats.longTrades += s.longTrades || 0;
|
|
215
|
-
stats.shortTrades += s.shortTrades || 0;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// If no lifetimeStats, calculate from trade history (still 100% API data)
|
|
220
|
-
if (stats.totalTrades === 0 && allTrades.length > 0) {
|
|
200
|
+
// Calculate stats from trade history (100% API data, consistent with displayed trades)
|
|
201
|
+
if (allTrades.length > 0) {
|
|
221
202
|
stats.totalTrades = allTrades.length;
|
|
222
203
|
let consecutiveWins = 0, consecutiveLosses = 0;
|
|
223
204
|
|
|
@@ -464,11 +445,16 @@ const showStats = async (service) => {
|
|
|
464
445
|
// Header
|
|
465
446
|
const header = ` ${'Time'.padEnd(colTime)}| ${'Symbol'.padEnd(colSymbol)}| ${'Price'.padEnd(colPrice)}| ${'P&L'.padEnd(colPnl)}| ${'Side'.padEnd(colSide)}| ${'Account'.padEnd(colAccount - 2)}`;
|
|
466
447
|
console.log(chalk.cyan('\u2551') + chalk.white(header) + chalk.cyan('\u2551'));
|
|
467
|
-
console.log(chalk.cyan('\
|
|
448
|
+
console.log(chalk.cyan('\u255F') + chalk.cyan('\u2500'.repeat(innerWidth)) + chalk.cyan('\u2562'));
|
|
468
449
|
|
|
469
|
-
|
|
450
|
+
// Show ALL trades, sorted by time (most recent first)
|
|
451
|
+
const sortedTrades = [...allTrades].sort((a, b) => {
|
|
452
|
+
const timeA = new Date(a.creationTimestamp || a.timestamp || 0).getTime();
|
|
453
|
+
const timeB = new Date(b.creationTimestamp || b.timestamp || 0).getTime();
|
|
454
|
+
return timeB - timeA;
|
|
455
|
+
});
|
|
470
456
|
|
|
471
|
-
for (const trade of
|
|
457
|
+
for (const trade of sortedTrades) {
|
|
472
458
|
const timestamp = trade.creationTimestamp || trade.timestamp;
|
|
473
459
|
const time = timestamp ? new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }) : '--:--';
|
|
474
460
|
const symbol = extractSymbol(trade.contractId || trade.symbol);
|
|
@@ -493,11 +479,6 @@ const showStats = async (service) => {
|
|
|
493
479
|
const row = ` ${timeStr}| ${symbolStr}| ${priceStr}| ${pnlColored}| ${sideColored}| ${accountStr}`;
|
|
494
480
|
console.log(chalk.cyan('\u2551') + row + chalk.cyan('\u2551'));
|
|
495
481
|
}
|
|
496
|
-
|
|
497
|
-
if (allTrades.length > 10) {
|
|
498
|
-
const moreMsg = ` ... and ${allTrades.length - 10} more trades`;
|
|
499
|
-
console.log(chalk.cyan('\u2551') + moreMsg.padEnd(innerWidth) + chalk.cyan('\u2551'));
|
|
500
|
-
}
|
|
501
482
|
} else {
|
|
502
483
|
const msg = connectionTypes.rithmic > 0
|
|
503
484
|
? ' No trade history (Rithmic API limitation)'
|
|
@@ -384,6 +384,42 @@ class ProjectXService {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Get trades history for today
|
|
389
|
+
* @param {number|string} accountId - Account ID
|
|
390
|
+
* @returns {Promise<{success: boolean, trades: Array, error?: string}>}
|
|
391
|
+
*/
|
|
392
|
+
async getTrades(accountId) {
|
|
393
|
+
try {
|
|
394
|
+
const id = validateAccountId(accountId);
|
|
395
|
+
|
|
396
|
+
// Get today's trades (from midnight UTC)
|
|
397
|
+
const now = new Date();
|
|
398
|
+
const startOfDay = new Date(now);
|
|
399
|
+
startOfDay.setUTCHours(0, 0, 0, 0);
|
|
400
|
+
|
|
401
|
+
const response = await this._request(
|
|
402
|
+
this.propfirm.gatewayApi,
|
|
403
|
+
'/api/Trade/search',
|
|
404
|
+
'POST',
|
|
405
|
+
{
|
|
406
|
+
accountId: id,
|
|
407
|
+
startTimestamp: startOfDay.toISOString(),
|
|
408
|
+
endTimestamp: now.toISOString()
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (response.statusCode === 200) {
|
|
413
|
+
const trades = response.data.trades || response.data || [];
|
|
414
|
+
return { success: true, trades: Array.isArray(trades) ? trades : [] };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { success: true, trades: [] };
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return { success: true, trades: [], error: err.message };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
387
423
|
/**
|
|
388
424
|
* Place an order
|
|
389
425
|
* @param {Object} orderData - Order details
|