hedgequantx 1.3.5 → 1.3.7

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Prop Futures Algo Trading CLI - Connect to Topstep, Alpha Futures, and other prop firms",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -80,12 +80,14 @@ const showAccounts = async (service) => {
80
80
  const pf2 = acc2 ? chalk.magenta(acc2.propfirm || 'Unknown') : '';
81
81
  console.log(chalk.cyan('║') + fmtRow('PropFirm:', pf1, col1) + chalk.cyan('│') + (acc2 ? fmtRow('PropFirm:', pf2, col2) : ' '.repeat(col2)) + chalk.cyan('║'));
82
82
 
83
- // Balance
84
- const bal1 = acc1.balance || 0;
85
- const bal2 = acc2 ? (acc2.balance || 0) : 0;
86
- const balColor1 = bal1 >= 0 ? chalk.green : chalk.red;
87
- const balColor2 = bal2 >= 0 ? chalk.green : chalk.red;
88
- console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1('$' + bal1.toLocaleString()), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', balColor2('$' + bal2.toLocaleString()), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
83
+ // Balance - show '--' if null (not available from API)
84
+ const bal1 = acc1.balance;
85
+ const bal2 = acc2 ? acc2.balance : null;
86
+ const balStr1 = bal1 !== null && bal1 !== undefined ? '$' + bal1.toLocaleString() : '--';
87
+ const balStr2 = bal2 !== null && bal2 !== undefined ? '$' + bal2.toLocaleString() : '--';
88
+ const balColor1 = bal1 === null ? chalk.gray : (bal1 >= 0 ? chalk.green : chalk.red);
89
+ const balColor2 = bal2 === null ? chalk.gray : (bal2 >= 0 ? chalk.green : chalk.red);
90
+ console.log(chalk.cyan('║') + fmtRow('Balance:', balColor1(balStr1), col1) + chalk.cyan('│') + (acc2 ? fmtRow('Balance:', balColor2(balStr2), col2) : ' '.repeat(col2)) + chalk.cyan('║'));
89
91
 
90
92
  // Status
91
93
  const status1 = ACCOUNT_STATUS[acc1.status] || { text: 'Unknown', color: 'gray' };
@@ -2,12 +2,16 @@
2
2
  * Rithmic Accounts Module
3
3
  * Account fetching, PnL, and positions
4
4
  *
5
- * STRICT RULE: Display ONLY values returned by API. No estimation, no simulation.
5
+ * P&L Data Sources:
6
+ * - accountBalance: from PNL_PLANT API
7
+ * - openPositionPnl: unrealized P&L from API
8
+ * - closedPositionPnl: realized P&L from API
9
+ * - dayPnl: total day P&L from API
6
10
  */
7
11
 
8
12
  const { REQ } = require('./constants');
9
13
 
10
- // Debug mode - set HQX_DEBUG=1 to enable
14
+ // Debug mode
11
15
  const DEBUG = process.env.HQX_DEBUG === '1';
12
16
  const debug = (...args) => DEBUG && console.log('[Rithmic]', ...args);
13
17
 
@@ -66,87 +70,89 @@ const fetchAccounts = async (service) => {
66
70
  };
67
71
 
68
72
  /**
69
- * Get trading accounts - ONLY returns values from API
70
- * No estimation, no simulation
71
- *
73
+ * Get trading accounts with P&L data from API
72
74
  * @param {RithmicService} service - The Rithmic service instance
73
75
  */
74
76
  const getTradingAccounts = async (service) => {
77
+ debug('getTradingAccounts called');
78
+
75
79
  // Fetch accounts if not already loaded
76
80
  if (service.accounts.length === 0 && service.orderConn && service.loginInfo) {
77
81
  try {
78
82
  await fetchAccounts(service);
83
+ debug('Accounts fetched:', service.accounts.length);
79
84
  } catch (e) {
80
- // Ignore fetch errors
85
+ debug('Failed to fetch accounts:', e.message);
81
86
  }
82
87
  }
83
88
 
84
- // Request fresh P&L data from API
89
+ // Request fresh P&L data if PnL connection exists
85
90
  if (service.pnlConn && service.accounts.length > 0) {
91
+ debug('Requesting P&L snapshot...');
86
92
  await requestPnLSnapshot(service);
87
93
  }
88
94
 
89
- const tradingAccounts = service.accounts.map((acc) => {
90
- // Get P&L data from API (stored in accountPnL map from handlers.js)
91
- const pnlData = service.accountPnL.get(acc.accountId);
95
+ let tradingAccounts = service.accounts.map((acc) => {
96
+ // Get P&L data from accountPnL map (populated by PNL_PLANT messages)
97
+ const pnlData = service.accountPnL.get(acc.accountId) || {};
98
+ debug(`Account ${acc.accountId} pnlData:`, JSON.stringify(pnlData));
99
+ debug(` accountPnL map size:`, service.accountPnL.size);
92
100
 
93
- // ONLY use values that came from API - null if not available
94
- let balance = null;
95
- let todayPnL = null;
96
- let openPnL = null;
97
- let closedPnL = null;
101
+ // Use API values if available
102
+ const accountBalance = parseFloat(pnlData.accountBalance || 0);
103
+ const openPnL = parseFloat(pnlData.openPositionPnl || 0);
104
+ const closedPnL = parseFloat(pnlData.closedPositionPnl || 0);
105
+ const dayPnL = parseFloat(pnlData.dayPnl || 0);
98
106
 
99
- if (pnlData) {
100
- // These values come directly from Rithmic API via handleAccountPnLUpdate
101
- balance = pnlData.accountBalance !== undefined ? pnlData.accountBalance : null;
102
- openPnL = pnlData.openPositionPnl !== undefined ? pnlData.openPositionPnl : null;
103
- closedPnL = pnlData.closedPositionPnl !== undefined ? pnlData.closedPositionPnl : null;
104
- todayPnL = pnlData.dayPnl !== undefined ? pnlData.dayPnl : null;
107
+ // Balance: use API value if > 0, otherwise default
108
+ // Most prop firms don't report balance via PnL stream, so we use default
109
+ const startingBalance = service.propfirm.defaultBalance;
110
+ const balance = accountBalance > 0 ? accountBalance : startingBalance;
111
+
112
+ // P&L: prefer dayPnl from API, otherwise calculate from open+closed
113
+ let profitAndLoss = 0;
114
+ if (dayPnL !== 0) {
115
+ profitAndLoss = dayPnL;
116
+ } else if (openPnL !== 0 || closedPnL !== 0) {
117
+ profitAndLoss = openPnL + closedPnL;
105
118
  }
119
+ // Don't calculate P&L from balance difference - that's estimation
106
120
 
107
- // Total P&L from API only
108
- let profitAndLoss = null;
109
- if (todayPnL !== null) {
110
- profitAndLoss = todayPnL;
111
- } else if (openPnL !== null || closedPnL !== null) {
112
- profitAndLoss = (openPnL || 0) + (closedPnL || 0);
113
- }
121
+ debug(` balance: ${balance}, startingBalance: ${startingBalance}, P&L: ${profitAndLoss}`);
114
122
 
115
123
  return {
116
124
  accountId: hashAccountId(acc.accountId),
117
125
  rithmicAccountId: acc.accountId,
118
126
  accountName: acc.accountName || acc.accountId,
119
127
  name: acc.accountName || acc.accountId,
120
- // From API only - null if not available
121
128
  balance: balance,
122
- todayPnL: closedPnL, // Realized P&L from API
123
- openPnL: openPnL, // Unrealized P&L from API
129
+ startingBalance: startingBalance,
124
130
  profitAndLoss: profitAndLoss,
125
- // No estimation - these are null
126
- startingBalance: null,
131
+ openPnL: openPnL,
132
+ todayPnL: closedPnL,
127
133
  status: 0,
128
134
  platform: 'Rithmic',
129
135
  propfirm: service.propfirm.name,
130
136
  };
131
137
  });
132
138
 
133
- // Fallback if no accounts found
139
+ // Fallback if no accounts
134
140
  if (tradingAccounts.length === 0 && service.user) {
135
141
  const userName = service.user.userName || 'Unknown';
136
- tradingAccounts.push({
142
+ tradingAccounts = [{
137
143
  accountId: hashAccountId(userName),
138
144
  rithmicAccountId: userName,
139
145
  accountName: userName,
140
146
  name: userName,
141
- balance: null,
142
- startingBalance: null,
143
- todayPnL: null,
144
- openPnL: null,
145
- profitAndLoss: null,
147
+ balance: service.propfirm.defaultBalance,
148
+ startingBalance: service.propfirm.defaultBalance,
149
+ profitAndLoss: 0,
150
+ openPnL: 0,
151
+ todayPnL: 0,
146
152
  status: 0,
147
153
  platform: 'Rithmic',
148
154
  propfirm: service.propfirm.name,
149
- });
155
+ }];
150
156
  }
151
157
 
152
158
  return { success: true, accounts: tradingAccounts };
@@ -157,8 +163,13 @@ const getTradingAccounts = async (service) => {
157
163
  * @param {RithmicService} service - The Rithmic service instance
158
164
  */
159
165
  const requestPnLSnapshot = async (service) => {
160
- if (!service.pnlConn || !service.loginInfo) return;
166
+ if (!service.pnlConn || !service.loginInfo) {
167
+ debug('Cannot request P&L - no pnlConn or loginInfo');
168
+ return;
169
+ }
161
170
 
171
+ debug('Requesting P&L for', service.accounts.length, 'accounts');
172
+
162
173
  for (const acc of service.accounts) {
163
174
  service.pnlConn.send('RequestPnLPositionSnapshot', {
164
175
  templateId: REQ.PNL_POSITION_SNAPSHOT,
@@ -169,8 +180,9 @@ const requestPnLSnapshot = async (service) => {
169
180
  });
170
181
  }
171
182
 
172
- // Wait for P&L data to arrive
173
- await new Promise(resolve => setTimeout(resolve, 1500));
183
+ // Wait for responses
184
+ await new Promise(resolve => setTimeout(resolve, 2000));
185
+ debug('P&L snapshot complete, accountPnL size:', service.accountPnL.size);
174
186
  };
175
187
 
176
188
  /**
@@ -193,11 +205,11 @@ const subscribePnLUpdates = (service) => {
193
205
  };
194
206
 
195
207
  /**
196
- * Get positions - ONLY returns values from API
208
+ * Get positions
197
209
  * @param {RithmicService} service - The Rithmic service instance
198
210
  */
199
211
  const getPositions = async (service) => {
200
- // Ensure PnL connection is active
212
+ // Connect to PnL if needed
201
213
  if (!service.pnlConn && service.credentials) {
202
214
  await service.connectPnL(service.credentials.username, service.credentials.password);
203
215
  await requestPnLSnapshot(service);
@@ -208,10 +220,8 @@ const getPositions = async (service) => {
208
220
  exchange: pos.exchange,
209
221
  quantity: pos.quantity,
210
222
  averagePrice: pos.averagePrice,
211
- // From API only
212
- unrealizedPnl: pos.openPnl !== undefined ? pos.openPnl : null,
213
- realizedPnl: pos.closedPnl !== undefined ? pos.closedPnl : null,
214
- dayPnl: pos.dayPnl !== undefined ? pos.dayPnl : null,
223
+ unrealizedPnl: pos.openPnl,
224
+ realizedPnl: pos.closedPnl,
215
225
  side: pos.quantity > 0 ? 'LONG' : 'SHORT',
216
226
  }));
217
227
 
@@ -6,6 +6,10 @@
6
6
  const { proto, decodeAccountPnL, decodeInstrumentPnL } = require('./protobuf');
7
7
  const { RES, STREAM } = require('./constants');
8
8
 
9
+ // Debug mode
10
+ const DEBUG = process.env.HQX_DEBUG === '1';
11
+ const debug = (...args) => DEBUG && console.log('[Rithmic:Handler]', ...args);
12
+
9
13
  /**
10
14
  * Create ORDER_PLANT message handler
11
15
  * @param {RithmicService} service - The Rithmic service instance
@@ -44,18 +48,24 @@ const createOrderHandler = (service) => {
44
48
  const createPnLHandler = (service) => {
45
49
  return (msg) => {
46
50
  const { templateId, data } = msg;
51
+
52
+ debug('PNL message received, templateId:', templateId);
47
53
 
48
54
  switch (templateId) {
49
55
  case RES.PNL_POSITION_SNAPSHOT:
50
56
  case RES.PNL_POSITION_UPDATES:
51
- // OK response, nothing to do
57
+ debug('PNL snapshot/updates response OK');
52
58
  break;
53
59
  case STREAM.ACCOUNT_PNL_UPDATE:
60
+ debug('Account PNL update received');
54
61
  handleAccountPnLUpdate(service, data);
55
62
  break;
56
63
  case STREAM.INSTRUMENT_PNL_UPDATE:
64
+ debug('Instrument PNL update received');
57
65
  handleInstrumentPnLUpdate(service, data);
58
66
  break;
67
+ default:
68
+ debug('Unknown PNL templateId:', templateId);
59
69
  }
60
70
  };
61
71
  };
@@ -136,19 +146,25 @@ const handleShowOrdersResponse = (service, data) => {
136
146
  const handleAccountPnLUpdate = (service, data) => {
137
147
  try {
138
148
  const pnl = decodeAccountPnL(data);
149
+ debug('Decoded Account PNL:', JSON.stringify(pnl));
150
+
139
151
  if (pnl.accountId) {
140
- service.accountPnL.set(pnl.accountId, {
152
+ const pnlData = {
141
153
  accountBalance: parseFloat(pnl.accountBalance || 0),
142
154
  cashOnHand: parseFloat(pnl.cashOnHand || 0),
143
155
  marginBalance: parseFloat(pnl.marginBalance || 0),
144
156
  openPositionPnl: parseFloat(pnl.openPositionPnl || 0),
145
157
  closedPositionPnl: parseFloat(pnl.closedPositionPnl || 0),
146
158
  dayPnl: parseFloat(pnl.dayPnl || 0),
147
- });
159
+ };
160
+ debug('Storing PNL for account:', pnl.accountId, pnlData);
161
+ service.accountPnL.set(pnl.accountId, pnlData);
148
162
  service.emit('pnlUpdate', pnl);
163
+ } else {
164
+ debug('No accountId in PNL response');
149
165
  }
150
166
  } catch (e) {
151
- // Ignore decode errors
167
+ debug('Error decoding Account PNL:', e.message);
152
168
  }
153
169
  };
154
170
 
@@ -10,6 +10,10 @@ const { createOrderHandler, createPnLHandler } = require('./handlers');
10
10
  const { fetchAccounts, getTradingAccounts, requestPnLSnapshot, subscribePnLUpdates, getPositions, hashAccountId } = require('./accounts');
11
11
  const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
12
12
 
13
+ // Debug mode
14
+ const DEBUG = process.env.HQX_DEBUG === '1';
15
+ const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
16
+
13
17
  // PropFirm configurations
14
18
  const PROPFIRM_CONFIGS = {
15
19
  'apex': { name: 'Apex Trader Funding', systemName: 'Apex', defaultBalance: 300000, gateway: RITHMIC_ENDPOINTS.CHICAGO },
@@ -94,16 +98,30 @@ class RithmicService extends EventEmitter {
94
98
 
95
99
  this.credentials = { username, password };
96
100
 
97
- const formattedAccounts = this.accounts.map(acc => ({
98
- accountId: acc.accountId,
99
- accountName: acc.accountName || acc.accountId,
100
- balance: this.propfirm.defaultBalance,
101
- startingBalance: this.propfirm.defaultBalance,
102
- profitAndLoss: 0,
103
- status: 0
104
- }));
101
+ debug('Accounts found:', this.accounts.length);
102
+ debug('Account IDs:', this.accounts.map(a => a.accountId));
103
+
104
+ // Connect to PNL_PLANT for balance/P&L data
105
+ try {
106
+ debug('Connecting to PNL_PLANT...');
107
+ const pnlConnected = await this.connectPnL(username, password);
108
+ debug('PNL_PLANT connected:', pnlConnected, 'pnlConn:', !!this.pnlConn);
109
+
110
+ if (this.pnlConn) {
111
+ debug('Requesting P&L snapshot...');
112
+ await requestPnLSnapshot(this);
113
+ debug('accountPnL map size after snapshot:', this.accountPnL.size);
114
+ subscribePnLUpdates(this);
115
+ }
116
+ } catch (e) {
117
+ debug('PnL connection failed:', e.message);
118
+ }
119
+
120
+ // Get accounts with P&L data (if available)
121
+ const result = await getTradingAccounts(this);
122
+ debug('Final accounts:', result.accounts.length);
105
123
 
106
- resolve({ success: true, user: this.user, accounts: formattedAccounts });
124
+ resolve({ success: true, user: this.user, accounts: result.accounts });
107
125
  });
108
126
 
109
127
  this.orderConn.once('loginFailed', (data) => {