hedgequantx 1.8.49 → 2.3.1
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/README.md +13 -6
- package/bin/cli.js +13 -7
- package/dist/algo/copy-engine.js +3 -0
- package/dist/algo/copy-engine.jsc +0 -0
- package/dist/algo/engine.js +3 -0
- package/dist/algo/engine.jsc +0 -0
- package/dist/algo/market-data-rithmic.js +3 -0
- package/dist/algo/market-data-rithmic.jsc +0 -0
- package/dist/algo/market-data.js +3 -0
- package/dist/algo/market-data.jsc +0 -0
- package/dist/algo/rithmic/connection.js +3 -0
- package/dist/algo/rithmic/connection.jsc +0 -0
- package/dist/algo/rithmic/constants.js +3 -0
- package/dist/algo/rithmic/constants.jsc +0 -0
- package/dist/algo/rithmic/index.js +3 -0
- package/dist/algo/rithmic/index.jsc +0 -0
- package/dist/algo/rithmic/market-data.js +3 -0
- package/dist/algo/rithmic/market-data.jsc +0 -0
- package/dist/algo/rithmic/pnl.js +3 -0
- package/dist/algo/rithmic/pnl.jsc +0 -0
- package/dist/algo/rithmic/pool.js +3 -0
- package/dist/algo/rithmic/pool.jsc +0 -0
- package/dist/algo/rithmic/trading.js +3 -0
- package/dist/algo/rithmic/trading.jsc +0 -0
- package/dist/algo/rithmic-decoder.js +3 -0
- package/dist/algo/rithmic-decoder.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping-v2.js +3 -0
- package/dist/algo/strategies/ultra-scalping-v2.jsc +0 -0
- package/dist/algo/strategies/ultra-scalping.js +3 -0
- package/dist/algo/strategies/ultra-scalping.jsc +0 -0
- package/dist/algo/trading-api-rithmic.js +3 -0
- package/dist/algo/trading-api-rithmic.jsc +0 -0
- package/dist/algo/trading-api.js +3 -0
- package/dist/algo/trading-api.jsc +0 -0
- package/dist/algo/utils/smart-logger.js +3 -0
- package/dist/algo/utils/smart-logger.jsc +0 -0
- package/dist/algo/utils/smart-logs.js +3 -0
- package/dist/algo/utils/smart-logs.jsc +0 -0
- package/package.json +33 -10
- package/protos/rithmic/account_pnl_position_update.proto +59 -0
- package/protos/rithmic/base.proto +7 -0
- package/protos/rithmic/best_bid_offer.proto +39 -0
- package/protos/rithmic/exchange_order_notification.proto +140 -0
- package/protos/rithmic/instrument_pnl_position_update.proto +50 -0
- package/protos/rithmic/last_trade.proto +53 -0
- package/protos/rithmic/request_account_list.proto +20 -0
- package/protos/rithmic/request_cancel_all_orders.proto +15 -0
- package/protos/rithmic/request_front_month_contract.proto +10 -0
- package/protos/rithmic/request_heartbeat.proto +13 -0
- package/protos/rithmic/request_login.proto +28 -0
- package/protos/rithmic/request_login_info.proto +10 -0
- package/protos/rithmic/request_logout.proto +10 -0
- package/protos/rithmic/request_market_data_update.proto +42 -0
- package/protos/rithmic/request_new_order.proto +84 -0
- package/protos/rithmic/request_pnl_position_snapshot.proto +14 -0
- package/protos/rithmic/request_pnl_position_updates.proto +20 -0
- package/protos/rithmic/request_product_codes.proto +9 -0
- package/protos/rithmic/request_rithmic_system_info.proto +8 -0
- package/protos/rithmic/request_show_order_history.proto +16 -0
- package/protos/rithmic/request_show_order_history_dates.proto +10 -0
- package/protos/rithmic/request_show_order_history_summary.proto +14 -0
- package/protos/rithmic/request_show_orders.proto +14 -0
- package/protos/rithmic/request_subscribe_for_order_updates.proto +14 -0
- package/protos/rithmic/request_tick_bar_replay.proto +48 -0
- package/protos/rithmic/request_trade_routes.proto +11 -0
- package/protos/rithmic/response_account_list.proto +18 -0
- package/protos/rithmic/response_front_month_contract.proto +13 -0
- package/protos/rithmic/response_heartbeat.proto +14 -0
- package/protos/rithmic/response_login.proto +18 -0
- package/protos/rithmic/response_login_info.proto +24 -0
- package/protos/rithmic/response_logout.proto +11 -0
- package/protos/rithmic/response_market_data_update.proto +9 -0
- package/protos/rithmic/response_new_order.proto +18 -0
- package/protos/rithmic/response_pnl_position_snapshot.proto +11 -0
- package/protos/rithmic/response_pnl_position_updates.proto +11 -0
- package/protos/rithmic/response_product_codes.proto +12 -0
- package/protos/rithmic/response_rithmic_system_info.proto +12 -0
- package/protos/rithmic/response_show_order_history.proto +11 -0
- package/protos/rithmic/response_show_order_history_dates.proto +13 -0
- package/protos/rithmic/response_show_order_history_summary.proto +11 -0
- package/protos/rithmic/response_show_orders.proto +11 -0
- package/protos/rithmic/response_subscribe_for_order_updates.proto +11 -0
- package/protos/rithmic/response_tick_bar_replay.proto +40 -0
- package/protos/rithmic/response_trade_routes.proto +19 -0
- package/protos/rithmic/rithmic_order_notification.proto +124 -0
- package/src/app.js +136 -89
- package/src/config/index.js +27 -8
- package/src/config/settings.js +155 -0
- package/src/pages/algo/copy-trading.js +293 -200
- package/src/pages/algo/one-account.js +1 -1
- package/src/security/encryption.js +81 -46
- package/src/security/index.js +12 -8
- package/src/security/rateLimit.js +68 -65
- package/src/security/validation.js +93 -79
- package/src/services/hqx-server.js +538 -206
- package/src/services/projectx/index.js +327 -204
- package/src/services/rithmic/index.js +288 -285
- package/src/services/session.js +184 -114
- package/src/services/tradovate/index.js +286 -297
- package/src/utils/http.js +236 -0
- package/src/utils/index.js +11 -2
- package/src/utils/logger.js +64 -33
- package/src/utils/prompts.js +79 -71
|
@@ -1,52 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rithmic Service
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Rithmic Service - Main service for Rithmic prop firm connections
|
|
3
|
+
* @module services/rithmic
|
|
4
|
+
*
|
|
5
|
+
* NO FAKE DATA - Only real values from Rithmic API
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
const EventEmitter = require('events');
|
|
7
9
|
const { RithmicConnection } = require('./connection');
|
|
8
|
-
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, getSymbolInfo
|
|
10
|
+
const { RITHMIC_ENDPOINTS, RITHMIC_SYSTEMS, getSymbolInfo } = require('./constants');
|
|
9
11
|
const { createOrderHandler, createPnLHandler } = require('./handlers');
|
|
10
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
fetchAccounts,
|
|
14
|
+
getTradingAccounts,
|
|
15
|
+
requestPnLSnapshot,
|
|
16
|
+
subscribePnLUpdates,
|
|
17
|
+
getPositions,
|
|
18
|
+
hashAccountId,
|
|
19
|
+
} = require('./accounts');
|
|
11
20
|
const { placeOrder, cancelOrder, getOrders, getOrderHistory, closePosition } = require('./orders');
|
|
12
|
-
const { decodeFrontMonthContract
|
|
21
|
+
const { decodeFrontMonthContract } = require('./protobuf');
|
|
22
|
+
const { TIMEOUTS, CACHE } = require('../../config/settings');
|
|
23
|
+
const { logger } = require('../../utils/logger');
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
const DEBUG = process.env.HQX_DEBUG === '1';
|
|
16
|
-
const debug = (...args) => DEBUG && console.log('[Rithmic:Service]', ...args);
|
|
25
|
+
const log = logger.scope('Rithmic');
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
/** PropFirm configurations */
|
|
19
28
|
const PROPFIRM_CONFIGS = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
apex: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
30
|
+
apex_rithmic: { name: 'Apex Trader Funding', systemName: 'Apex', gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
31
|
+
topstep_r: { name: 'Topstep (Rithmic)', systemName: RITHMIC_SYSTEMS.TOPSTEP, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
32
|
+
bulenox_r: { name: 'Bulenox (Rithmic)', systemName: RITHMIC_SYSTEMS.BULENOX, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
33
|
+
earn2trade: { name: 'Earn2Trade', systemName: RITHMIC_SYSTEMS.EARN_2_TRADE, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
34
|
+
mescapital: { name: 'MES Capital', systemName: RITHMIC_SYSTEMS.MES_CAPITAL, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
35
|
+
tradefundrr: { name: 'TradeFundrr', systemName: RITHMIC_SYSTEMS.TRADEFUNDRR, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
36
|
+
thetradingpit: { name: 'The Trading Pit', systemName: RITHMIC_SYSTEMS.THE_TRADING_PIT, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
37
|
+
fundedfutures: { name: 'Funded Futures Network', systemName: RITHMIC_SYSTEMS.FUNDED_FUTURES_NETWORK, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
38
|
+
propshop: { name: 'PropShop Trader', systemName: RITHMIC_SYSTEMS.PROPSHOP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
30
39
|
'4proptrader': { name: '4PropTrader', systemName: RITHMIC_SYSTEMS.FOUR_PROP_TRADER, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
31
|
-
|
|
40
|
+
daytraders: { name: 'DayTraders.com', systemName: RITHMIC_SYSTEMS.DAY_TRADERS, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
32
41
|
'10xfutures': { name: '10X Futures', systemName: RITHMIC_SYSTEMS.TEN_X_FUTURES, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
lucidtrading: { name: 'Lucid Trading', systemName: RITHMIC_SYSTEMS.LUCID_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
43
|
+
thrivetrading: { name: 'Thrive Trading', systemName: RITHMIC_SYSTEMS.THRIVE_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
44
|
+
legendstrading: { name: 'Legends Trading', systemName: RITHMIC_SYSTEMS.LEGENDS_TRADING, gateway: RITHMIC_ENDPOINTS.CHICAGO },
|
|
36
45
|
};
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Rithmic Service for prop firm trading
|
|
49
|
+
*/
|
|
38
50
|
class RithmicService extends EventEmitter {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} propfirmKey - PropFirm identifier
|
|
53
|
+
*/
|
|
39
54
|
constructor(propfirmKey) {
|
|
40
55
|
super();
|
|
41
56
|
this.propfirmKey = propfirmKey;
|
|
42
|
-
this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
|
|
43
|
-
name: propfirmKey,
|
|
44
|
-
systemName: 'Rithmic Paper Trading',
|
|
45
|
-
gateway: RITHMIC_ENDPOINTS.PAPER
|
|
57
|
+
this.propfirm = PROPFIRM_CONFIGS[propfirmKey] || {
|
|
58
|
+
name: propfirmKey,
|
|
59
|
+
systemName: 'Rithmic Paper Trading',
|
|
60
|
+
gateway: RITHMIC_ENDPOINTS.PAPER,
|
|
46
61
|
};
|
|
62
|
+
|
|
63
|
+
// Connections
|
|
47
64
|
this.orderConn = null;
|
|
48
65
|
this.pnlConn = null;
|
|
49
|
-
this.tickerConn = null;
|
|
66
|
+
this.tickerConn = null;
|
|
67
|
+
|
|
68
|
+
// State
|
|
50
69
|
this.loginInfo = null;
|
|
51
70
|
this.accounts = [];
|
|
52
71
|
this.accountPnL = new Map();
|
|
@@ -54,24 +73,31 @@ class RithmicService extends EventEmitter {
|
|
|
54
73
|
this.orders = [];
|
|
55
74
|
this.user = null;
|
|
56
75
|
this.credentials = null;
|
|
57
|
-
|
|
76
|
+
|
|
77
|
+
// Cache
|
|
78
|
+
this._contractsCache = null;
|
|
79
|
+
this._contractsCacheTime = 0;
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
// ==================== AUTH ====================
|
|
83
|
+
|
|
60
84
|
/**
|
|
61
85
|
* Login to Rithmic
|
|
86
|
+
* @param {string} username - Username
|
|
87
|
+
* @param {string} password - Password
|
|
88
|
+
* @returns {Promise<{success: boolean, user?: Object, accounts?: Array, error?: string}>}
|
|
62
89
|
*/
|
|
63
90
|
async login(username, password) {
|
|
64
91
|
try {
|
|
65
92
|
this.orderConn = new RithmicConnection();
|
|
66
|
-
const gateway = this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO;
|
|
67
93
|
|
|
68
94
|
const config = {
|
|
69
|
-
uri: gateway,
|
|
95
|
+
uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
|
|
70
96
|
systemName: this.propfirm.systemName,
|
|
71
97
|
userId: username,
|
|
72
|
-
password
|
|
98
|
+
password,
|
|
73
99
|
appName: 'HQX-CLI',
|
|
74
|
-
appVersion: '
|
|
100
|
+
appVersion: '2.0.0',
|
|
75
101
|
};
|
|
76
102
|
|
|
77
103
|
await this.orderConn.connect(config);
|
|
@@ -80,88 +106,83 @@ class RithmicService extends EventEmitter {
|
|
|
80
106
|
return new Promise((resolve) => {
|
|
81
107
|
const timeout = setTimeout(() => {
|
|
82
108
|
resolve({ success: false, error: 'Login timeout - server did not respond' });
|
|
83
|
-
},
|
|
109
|
+
}, TIMEOUTS.RITHMIC_LOGIN);
|
|
84
110
|
|
|
85
111
|
this.orderConn.once('loggedIn', async (data) => {
|
|
86
112
|
clearTimeout(timeout);
|
|
87
113
|
this.loginInfo = data;
|
|
88
114
|
this.user = { userName: username, fcmId: data.fcmId, ibId: data.ibId };
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
// Fetch accounts
|
|
117
|
+
try {
|
|
118
|
+
await fetchAccounts(this);
|
|
119
|
+
log.debug('Fetched accounts', { count: this.accounts.length });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
log.warn('Failed to fetch accounts', { error: err.message });
|
|
95
122
|
}
|
|
96
123
|
|
|
97
|
-
//
|
|
98
|
-
|
|
124
|
+
// Store credentials for reconnection
|
|
99
125
|
this.credentials = { username, password };
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
debug('Account IDs:', this.accounts.map(a => a.accountId));
|
|
103
|
-
|
|
104
|
-
// Connect to PNL_PLANT for balance/P&L data
|
|
127
|
+
// Connect to PNL_PLANT
|
|
105
128
|
try {
|
|
106
|
-
debug('Connecting to PNL_PLANT...');
|
|
107
129
|
const pnlConnected = await this.connectPnL(username, password);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (this.pnlConn) {
|
|
111
|
-
debug('Requesting P&L snapshot...');
|
|
130
|
+
if (pnlConnected && this.pnlConn) {
|
|
112
131
|
await requestPnLSnapshot(this);
|
|
113
|
-
debug('accountPnL map size after snapshot:', this.accountPnL.size);
|
|
114
132
|
subscribePnLUpdates(this);
|
|
115
133
|
}
|
|
116
|
-
} catch (
|
|
117
|
-
|
|
134
|
+
} catch (err) {
|
|
135
|
+
log.warn('PnL connection failed', { error: err.message });
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
// Get accounts with P&L data (if available)
|
|
121
138
|
const result = await getTradingAccounts(this);
|
|
122
|
-
|
|
139
|
+
log.info('Login successful', { accounts: result.accounts.length });
|
|
123
140
|
|
|
124
141
|
resolve({ success: true, user: this.user, accounts: result.accounts });
|
|
125
142
|
});
|
|
126
143
|
|
|
127
144
|
this.orderConn.once('loginFailed', (data) => {
|
|
128
145
|
clearTimeout(timeout);
|
|
146
|
+
log.warn('Login failed', { error: data.message });
|
|
129
147
|
resolve({ success: false, error: data.message || 'Login failed' });
|
|
130
148
|
});
|
|
131
149
|
|
|
132
150
|
this.orderConn.login('ORDER_PLANT');
|
|
133
151
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return { success: false, error:
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log.error('Login error', { error: err.message });
|
|
154
|
+
return { success: false, error: err.message };
|
|
137
155
|
}
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
/**
|
|
141
159
|
* Connect to PNL_PLANT for balance data
|
|
160
|
+
* @param {string} username - Username
|
|
161
|
+
* @param {string} password - Password
|
|
162
|
+
* @returns {Promise<boolean>}
|
|
142
163
|
*/
|
|
143
164
|
async connectPnL(username, password) {
|
|
144
165
|
try {
|
|
145
166
|
this.pnlConn = new RithmicConnection();
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
|
|
148
168
|
const config = {
|
|
149
|
-
uri: gateway,
|
|
169
|
+
uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
|
|
150
170
|
systemName: this.propfirm.systemName,
|
|
151
171
|
userId: username,
|
|
152
|
-
password
|
|
172
|
+
password,
|
|
153
173
|
appName: 'HQX-CLI',
|
|
154
|
-
appVersion: '
|
|
174
|
+
appVersion: '2.0.0',
|
|
155
175
|
};
|
|
156
176
|
|
|
157
177
|
await this.pnlConn.connect(config);
|
|
158
178
|
this.pnlConn.on('message', createPnLHandler(this));
|
|
159
179
|
|
|
160
180
|
return new Promise((resolve) => {
|
|
161
|
-
const timeout = setTimeout(() => resolve(false),
|
|
181
|
+
const timeout = setTimeout(() => resolve(false), TIMEOUTS.RITHMIC_PNL);
|
|
162
182
|
|
|
163
183
|
this.pnlConn.once('loggedIn', () => {
|
|
164
184
|
clearTimeout(timeout);
|
|
185
|
+
log.debug('PNL_PLANT connected');
|
|
165
186
|
resolve(true);
|
|
166
187
|
});
|
|
167
188
|
|
|
@@ -172,247 +193,219 @@ class RithmicService extends EventEmitter {
|
|
|
172
193
|
|
|
173
194
|
this.pnlConn.login('PNL_PLANT');
|
|
174
195
|
});
|
|
175
|
-
} catch (
|
|
196
|
+
} catch (err) {
|
|
197
|
+
log.warn('PNL connection error', { error: err.message });
|
|
176
198
|
return false;
|
|
177
199
|
}
|
|
178
200
|
}
|
|
179
201
|
|
|
180
|
-
// Delegate to modules
|
|
181
|
-
async getTradingAccounts() { return getTradingAccounts(this); }
|
|
182
|
-
async getPositions() { return getPositions(this); }
|
|
183
|
-
async getOrders() { return getOrders(this); }
|
|
184
|
-
async getOrderHistory(date) { return getOrderHistory(this, date); }
|
|
185
|
-
async placeOrder(orderData) { return placeOrder(this, orderData); }
|
|
186
|
-
async cancelOrder(orderId) { return cancelOrder(this, orderId); }
|
|
187
|
-
async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
|
|
188
|
-
|
|
189
|
-
// Stubs for API compatibility
|
|
190
|
-
async getUser() { return this.user; }
|
|
191
|
-
async getLifetimeStats() { return { success: true, stats: null }; }
|
|
192
|
-
async getDailyStats() { return { success: true, stats: [] }; }
|
|
193
|
-
async getTradeHistory() { return { success: true, trades: [] }; }
|
|
194
|
-
|
|
195
|
-
async getMarketStatus() {
|
|
196
|
-
const status = this.checkMarketHours();
|
|
197
|
-
return { success: true, isOpen: status.isOpen, message: status.message };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
getToken() { return this.loginInfo ? 'connected' : null; }
|
|
201
|
-
getPropfirm() { return this.propfirmKey || 'apex'; }
|
|
202
|
-
|
|
203
|
-
getRithmicCredentials() {
|
|
204
|
-
if (!this.credentials) return null;
|
|
205
|
-
return {
|
|
206
|
-
userId: this.credentials.username,
|
|
207
|
-
password: this.credentials.password,
|
|
208
|
-
systemName: this.propfirm.systemName,
|
|
209
|
-
gateway: this.propfirm.gateway || 'wss://rprotocol.rithmic.com:443'
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
202
|
/**
|
|
214
203
|
* Connect to TICKER_PLANT for symbol lookup
|
|
204
|
+
* @param {string} username - Username
|
|
205
|
+
* @param {string} password - Password
|
|
206
|
+
* @returns {Promise<boolean>}
|
|
215
207
|
*/
|
|
216
208
|
async connectTicker(username, password) {
|
|
217
209
|
try {
|
|
218
210
|
this.tickerConn = new RithmicConnection();
|
|
219
|
-
|
|
220
|
-
|
|
211
|
+
|
|
221
212
|
const config = {
|
|
222
|
-
uri: gateway,
|
|
213
|
+
uri: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
|
|
223
214
|
systemName: this.propfirm.systemName,
|
|
224
215
|
userId: username,
|
|
225
|
-
password
|
|
216
|
+
password,
|
|
226
217
|
appName: 'HQX-CLI',
|
|
227
|
-
appVersion: '
|
|
218
|
+
appVersion: '2.0.0',
|
|
228
219
|
};
|
|
229
220
|
|
|
230
221
|
await this.tickerConn.connect(config);
|
|
231
222
|
|
|
232
223
|
return new Promise((resolve) => {
|
|
233
224
|
const timeout = setTimeout(() => {
|
|
234
|
-
debug('TICKER_PLANT
|
|
225
|
+
log.debug('TICKER_PLANT timeout');
|
|
235
226
|
resolve(false);
|
|
236
|
-
},
|
|
227
|
+
}, TIMEOUTS.RITHMIC_TICKER);
|
|
237
228
|
|
|
238
229
|
this.tickerConn.once('loggedIn', () => {
|
|
239
230
|
clearTimeout(timeout);
|
|
240
|
-
debug('TICKER_PLANT connected');
|
|
231
|
+
log.debug('TICKER_PLANT connected');
|
|
241
232
|
resolve(true);
|
|
242
233
|
});
|
|
243
234
|
|
|
244
235
|
this.tickerConn.once('loginFailed', () => {
|
|
245
236
|
clearTimeout(timeout);
|
|
246
|
-
debug('TICKER_PLANT login failed');
|
|
247
237
|
resolve(false);
|
|
248
238
|
});
|
|
249
239
|
|
|
250
240
|
this.tickerConn.login('TICKER_PLANT');
|
|
251
241
|
});
|
|
252
|
-
} catch (
|
|
253
|
-
|
|
242
|
+
} catch (err) {
|
|
243
|
+
log.warn('TICKER connection error', { error: err.message });
|
|
254
244
|
return false;
|
|
255
245
|
}
|
|
256
246
|
}
|
|
257
247
|
|
|
258
|
-
|
|
259
|
-
* Get front month contracts for multiple symbols at once - batch request
|
|
260
|
-
*/
|
|
261
|
-
async getFrontMonthsBatch(products) {
|
|
262
|
-
if (!this.tickerConn) {
|
|
263
|
-
throw new Error('TICKER_PLANT not connected');
|
|
264
|
-
}
|
|
248
|
+
// ==================== DELEGATED METHODS ====================
|
|
265
249
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
250
|
+
async getTradingAccounts() { return getTradingAccounts(this); }
|
|
251
|
+
async getPositions() { return getPositions(this); }
|
|
252
|
+
async getOrders() { return getOrders(this); }
|
|
253
|
+
async getOrderHistory(date) { return getOrderHistory(this, date); }
|
|
254
|
+
async placeOrder(orderData) { return placeOrder(this, orderData); }
|
|
255
|
+
async cancelOrder(orderId) { return cancelOrder(this, orderId); }
|
|
256
|
+
async closePosition(accountId, symbol) { return closePosition(this, accountId, symbol); }
|
|
269
257
|
|
|
270
|
-
|
|
271
|
-
if (msg.templateId === 114) { // ResponseFrontMonthContract
|
|
272
|
-
const decoded = decodeFrontMonthContract(msg.data);
|
|
273
|
-
const baseSymbol = decoded.userMsg;
|
|
274
|
-
|
|
275
|
-
if (pending.has(baseSymbol)) {
|
|
276
|
-
pending.delete(baseSymbol);
|
|
277
|
-
|
|
278
|
-
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
279
|
-
results.set(baseSymbol, {
|
|
280
|
-
symbol: decoded.tradingSymbol,
|
|
281
|
-
baseSymbol: baseSymbol,
|
|
282
|
-
exchange: decoded.exchange,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
};
|
|
258
|
+
// ==================== STUBS ====================
|
|
288
259
|
|
|
289
|
-
|
|
260
|
+
async getUser() { return this.user; }
|
|
261
|
+
async getLifetimeStats() { return { success: true, stats: null }; }
|
|
262
|
+
async getDailyStats() { return { success: true, stats: [] }; }
|
|
263
|
+
async getTradeHistory() { return { success: true, trades: [] }; }
|
|
290
264
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
userMsg: [product.productCode],
|
|
296
|
-
symbol: product.productCode,
|
|
297
|
-
exchange: product.exchange,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
265
|
+
async getMarketStatus() {
|
|
266
|
+
const status = this.checkMarketHours();
|
|
267
|
+
return { success: true, isOpen: status.isOpen, message: status.message };
|
|
268
|
+
}
|
|
300
269
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
270
|
+
getToken() { return this.loginInfo ? 'connected' : null; }
|
|
271
|
+
getPropfirm() { return this.propfirmKey || 'apex'; }
|
|
272
|
+
|
|
273
|
+
getRithmicCredentials() {
|
|
274
|
+
if (!this.credentials) return null;
|
|
275
|
+
return {
|
|
276
|
+
userId: this.credentials.username,
|
|
277
|
+
password: this.credentials.password,
|
|
278
|
+
systemName: this.propfirm.systemName,
|
|
279
|
+
gateway: this.propfirm.gateway || RITHMIC_ENDPOINTS.CHICAGO,
|
|
280
|
+
};
|
|
307
281
|
}
|
|
308
282
|
|
|
283
|
+
// ==================== CONTRACTS ====================
|
|
284
|
+
|
|
309
285
|
/**
|
|
310
|
-
*
|
|
286
|
+
* Get all available contracts from Rithmic API
|
|
287
|
+
* @returns {Promise<{success: boolean, contracts: Array, source?: string, error?: string}>}
|
|
311
288
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
289
|
+
async getContracts() {
|
|
290
|
+
// Check cache
|
|
291
|
+
if (this._contractsCache && Date.now() - this._contractsCacheTime < CACHE.CONTRACTS_TTL) {
|
|
292
|
+
return { success: true, contracts: this._contractsCache, source: 'cache' };
|
|
293
|
+
}
|
|
315
294
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const wireType = tag & 0x7;
|
|
320
|
-
const fieldNumber = tag >>> 3;
|
|
321
|
-
offset = tagOffset;
|
|
295
|
+
if (!this.credentials) {
|
|
296
|
+
return { success: false, error: 'Not logged in' };
|
|
297
|
+
}
|
|
322
298
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
offset = newOff;
|
|
330
|
-
if (fieldNumber === 110101) result.exchange = val; // exchange
|
|
331
|
-
if (fieldNumber === 100749) result.productCode = val; // product_code (base symbol)
|
|
332
|
-
if (fieldNumber === 100003) result.productName = val; // symbol_name
|
|
333
|
-
if (fieldNumber === 132760) result.userMsg = val; // user_msg
|
|
334
|
-
if (fieldNumber === 132764) result.rpCode = val; // rp_code
|
|
335
|
-
} else {
|
|
336
|
-
offset = skipField(buffer, offset, wireType);
|
|
299
|
+
try {
|
|
300
|
+
// Connect to TICKER_PLANT if needed
|
|
301
|
+
if (!this.tickerConn) {
|
|
302
|
+
const connected = await this.connectTicker(this.credentials.username, this.credentials.password);
|
|
303
|
+
if (!connected) {
|
|
304
|
+
return { success: false, error: 'Failed to connect to TICKER_PLANT' };
|
|
337
305
|
}
|
|
338
|
-
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.tickerConn.setMaxListeners(5000);
|
|
309
|
+
|
|
310
|
+
log.debug('Fetching contracts from Rithmic API');
|
|
311
|
+
const contracts = await this._fetchAllFrontMonths();
|
|
312
|
+
|
|
313
|
+
if (!contracts.length) {
|
|
314
|
+
return { success: false, error: 'No tradeable contracts found' };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Cache results
|
|
318
|
+
this._contractsCache = contracts;
|
|
319
|
+
this._contractsCacheTime = Date.now();
|
|
320
|
+
|
|
321
|
+
return { success: true, contracts, source: 'api' };
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log.error('getContracts error', { error: err.message });
|
|
324
|
+
return { success: false, error: err.message };
|
|
339
325
|
}
|
|
326
|
+
}
|
|
340
327
|
|
|
341
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Search contracts
|
|
330
|
+
* @param {string} searchText - Search text
|
|
331
|
+
* @returns {Promise<Array>}
|
|
332
|
+
*/
|
|
333
|
+
async searchContracts(searchText) {
|
|
334
|
+
const result = await this.getContracts();
|
|
335
|
+
if (!searchText || !result.success) return result.contracts || [];
|
|
336
|
+
|
|
337
|
+
const search = searchText.toUpperCase();
|
|
338
|
+
return result.contracts.filter(c =>
|
|
339
|
+
c.symbol.toUpperCase().includes(search) ||
|
|
340
|
+
c.name.toUpperCase().includes(search)
|
|
341
|
+
);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
345
|
+
* Fetch all front month contracts from API
|
|
346
|
+
* @private
|
|
347
347
|
*/
|
|
348
|
-
async
|
|
348
|
+
async _fetchAllFrontMonths() {
|
|
349
349
|
if (!this.tickerConn) {
|
|
350
350
|
throw new Error('TICKER_PLANT not connected');
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
// Request front months for ALL products that might be futures
|
|
354
|
-
// The API will return the current trading symbol for each
|
|
355
353
|
return new Promise((resolve) => {
|
|
356
354
|
const contracts = new Map();
|
|
357
|
-
const productsToCheck = new Map();
|
|
355
|
+
const productsToCheck = new Map();
|
|
358
356
|
|
|
359
357
|
// Handler for ProductCodes responses
|
|
360
358
|
const productHandler = (msg) => {
|
|
361
|
-
if (msg.templateId
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
productName: decoded.productName || decoded.productCode,
|
|
380
|
-
exchange: decoded.exchange,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
359
|
+
if (msg.templateId !== 112) return;
|
|
360
|
+
|
|
361
|
+
const decoded = this._decodeProductCodes(msg.data);
|
|
362
|
+
if (!decoded.productCode || !decoded.exchange) return;
|
|
363
|
+
|
|
364
|
+
const validExchanges = ['CME', 'CBOT', 'NYMEX', 'COMEX', 'NYBOT', 'CFE'];
|
|
365
|
+
if (!validExchanges.includes(decoded.exchange)) return;
|
|
366
|
+
|
|
367
|
+
const name = (decoded.productName || '').toLowerCase();
|
|
368
|
+
if (name.includes('option') || name.includes('swap') || name.includes('spread')) return;
|
|
369
|
+
|
|
370
|
+
const key = `${decoded.productCode}:${decoded.exchange}`;
|
|
371
|
+
if (!productsToCheck.has(key)) {
|
|
372
|
+
productsToCheck.set(key, {
|
|
373
|
+
productCode: decoded.productCode,
|
|
374
|
+
productName: decoded.productName || decoded.productCode,
|
|
375
|
+
exchange: decoded.exchange,
|
|
376
|
+
});
|
|
383
377
|
}
|
|
384
378
|
};
|
|
385
379
|
|
|
386
380
|
// Handler for FrontMonth responses
|
|
387
381
|
const frontMonthHandler = (msg) => {
|
|
388
|
-
if (msg.templateId
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
382
|
+
if (msg.templateId !== 114) return;
|
|
383
|
+
|
|
384
|
+
const decoded = decodeFrontMonthContract(msg.data);
|
|
385
|
+
if (decoded.rpCode[0] === '0' && decoded.tradingSymbol) {
|
|
386
|
+
contracts.set(decoded.userMsg, {
|
|
387
|
+
symbol: decoded.tradingSymbol,
|
|
388
|
+
baseSymbol: decoded.userMsg,
|
|
389
|
+
exchange: decoded.exchange,
|
|
390
|
+
});
|
|
397
391
|
}
|
|
398
392
|
};
|
|
399
393
|
|
|
400
394
|
this.tickerConn.on('message', productHandler);
|
|
401
395
|
this.tickerConn.on('message', frontMonthHandler);
|
|
402
396
|
|
|
403
|
-
//
|
|
397
|
+
// Request all product codes
|
|
404
398
|
this.tickerConn.send('RequestProductCodes', {
|
|
405
399
|
templateId: 111,
|
|
406
400
|
userMsg: ['get-products'],
|
|
407
401
|
});
|
|
408
402
|
|
|
409
|
-
// After
|
|
403
|
+
// After timeout, request front months
|
|
410
404
|
setTimeout(() => {
|
|
411
405
|
this.tickerConn.removeListener('message', productHandler);
|
|
412
|
-
debug(
|
|
406
|
+
log.debug('Collected products', { count: productsToCheck.size });
|
|
413
407
|
|
|
414
|
-
|
|
415
|
-
for (const [key, product] of productsToCheck) {
|
|
408
|
+
for (const product of productsToCheck.values()) {
|
|
416
409
|
this.tickerConn.send('RequestFrontMonthContract', {
|
|
417
410
|
templateId: 113,
|
|
418
411
|
userMsg: [product.productCode],
|
|
@@ -421,20 +414,19 @@ class RithmicService extends EventEmitter {
|
|
|
421
414
|
});
|
|
422
415
|
}
|
|
423
416
|
|
|
424
|
-
//
|
|
417
|
+
// Collect results after timeout
|
|
425
418
|
setTimeout(() => {
|
|
426
419
|
this.tickerConn.removeListener('message', frontMonthHandler);
|
|
427
|
-
|
|
428
|
-
// Merge with product names and add category info
|
|
420
|
+
|
|
429
421
|
const results = [];
|
|
430
422
|
for (const [baseSymbol, contract] of contracts) {
|
|
431
423
|
const productKey = `${baseSymbol}:${contract.exchange}`;
|
|
432
424
|
const product = productsToCheck.get(productKey);
|
|
433
425
|
const symbolInfo = getSymbolInfo(baseSymbol);
|
|
434
|
-
|
|
426
|
+
|
|
435
427
|
results.push({
|
|
436
428
|
symbol: contract.symbol,
|
|
437
|
-
baseSymbol
|
|
429
|
+
baseSymbol,
|
|
438
430
|
name: symbolInfo.name || product?.productName || baseSymbol,
|
|
439
431
|
exchange: contract.exchange,
|
|
440
432
|
category: symbolInfo.category,
|
|
@@ -445,7 +437,7 @@ class RithmicService extends EventEmitter {
|
|
|
445
437
|
});
|
|
446
438
|
}
|
|
447
439
|
|
|
448
|
-
// Sort by category order
|
|
440
|
+
// Sort by category order
|
|
449
441
|
results.sort((a, b) => {
|
|
450
442
|
if (a.categoryOrder !== b.categoryOrder) {
|
|
451
443
|
return a.categoryOrder - b.categoryOrder;
|
|
@@ -453,72 +445,72 @@ class RithmicService extends EventEmitter {
|
|
|
453
445
|
return a.baseSymbol.localeCompare(b.baseSymbol);
|
|
454
446
|
});
|
|
455
447
|
|
|
456
|
-
debug(
|
|
448
|
+
log.debug('Got contracts from API', { count: results.length });
|
|
457
449
|
resolve(results);
|
|
458
|
-
},
|
|
459
|
-
},
|
|
450
|
+
}, TIMEOUTS.RITHMIC_PRODUCTS);
|
|
451
|
+
}, TIMEOUTS.RITHMIC_CONTRACTS);
|
|
460
452
|
});
|
|
461
453
|
}
|
|
462
454
|
|
|
463
455
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
456
|
+
* Decode ProductCodes response
|
|
457
|
+
* @private
|
|
466
458
|
*/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return { success: true, contracts: this.cachedContracts.data, source: 'cache' };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Need credentials to fetch real data
|
|
474
|
-
if (!this.credentials) {
|
|
475
|
-
return { success: false, error: 'Not logged in - cannot fetch contracts from API' };
|
|
476
|
-
}
|
|
459
|
+
_decodeProductCodes(buffer) {
|
|
460
|
+
const result = {};
|
|
461
|
+
let offset = 0;
|
|
477
462
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
463
|
+
const readVarint = (buf, off) => {
|
|
464
|
+
let value = 0;
|
|
465
|
+
let shift = 0;
|
|
466
|
+
while (off < buf.length) {
|
|
467
|
+
const byte = buf[off++];
|
|
468
|
+
value |= (byte & 0x7F) << shift;
|
|
469
|
+
if (!(byte & 0x80)) break;
|
|
470
|
+
shift += 7;
|
|
485
471
|
}
|
|
472
|
+
return [value, off];
|
|
473
|
+
};
|
|
486
474
|
|
|
487
|
-
|
|
488
|
-
|
|
475
|
+
const readString = (buf, off) => {
|
|
476
|
+
const [len, newOff] = readVarint(buf, off);
|
|
477
|
+
return [buf.slice(newOff, newOff + len).toString('utf8'), newOff + len];
|
|
478
|
+
};
|
|
489
479
|
|
|
490
|
-
|
|
491
|
-
|
|
480
|
+
while (offset < buffer.length) {
|
|
481
|
+
try {
|
|
482
|
+
const [tag, tagOff] = readVarint(buffer, offset);
|
|
483
|
+
const wireType = tag & 0x7;
|
|
484
|
+
const fieldNumber = tag >>> 3;
|
|
485
|
+
offset = tagOff;
|
|
492
486
|
|
|
493
|
-
|
|
494
|
-
|
|
487
|
+
if (wireType === 0) {
|
|
488
|
+
const [, newOff] = readVarint(buffer, offset);
|
|
489
|
+
offset = newOff;
|
|
490
|
+
} else if (wireType === 2) {
|
|
491
|
+
const [val, newOff] = readString(buffer, offset);
|
|
492
|
+
offset = newOff;
|
|
493
|
+
if (fieldNumber === 110101) result.exchange = val;
|
|
494
|
+
if (fieldNumber === 100749) result.productCode = val;
|
|
495
|
+
if (fieldNumber === 100003) result.productName = val;
|
|
496
|
+
} else {
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
break;
|
|
495
501
|
}
|
|
496
|
-
|
|
497
|
-
// Cache the results
|
|
498
|
-
this.cachedContracts = { data: contracts, timestamp: Date.now() };
|
|
499
|
-
return { success: true, contracts, source: 'api' };
|
|
500
|
-
|
|
501
|
-
} catch (e) {
|
|
502
|
-
debug('getContracts error:', e.message);
|
|
503
|
-
return { success: false, error: e.message };
|
|
504
502
|
}
|
|
505
|
-
}
|
|
506
503
|
|
|
507
|
-
|
|
508
|
-
const result = await this.getContracts();
|
|
509
|
-
if (!searchText || !result.success) return result.contracts || [];
|
|
510
|
-
const search = searchText.toUpperCase();
|
|
511
|
-
return result.contracts.filter(c =>
|
|
512
|
-
c.symbol.toUpperCase().includes(search) ||
|
|
513
|
-
c.name.toUpperCase().includes(search)
|
|
514
|
-
);
|
|
504
|
+
return result;
|
|
515
505
|
}
|
|
516
506
|
|
|
507
|
+
// ==================== MARKET HOURS ====================
|
|
508
|
+
|
|
517
509
|
checkMarketHours() {
|
|
518
510
|
const now = new Date();
|
|
519
511
|
const utcDay = now.getUTCDay();
|
|
520
512
|
const utcHour = now.getUTCHours();
|
|
521
|
-
|
|
513
|
+
|
|
522
514
|
const isDST = now.getTimezoneOffset() < Math.max(
|
|
523
515
|
new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
|
|
524
516
|
new Date(now.getFullYear(), 6, 1).getTimezoneOffset()
|
|
@@ -531,22 +523,31 @@ class RithmicService extends EventEmitter {
|
|
|
531
523
|
if (ctDay === 0 && ctHour < 17) return { isOpen: false, message: 'Market opens Sunday 5:00 PM CT' };
|
|
532
524
|
if (ctDay === 5 && ctHour >= 16) return { isOpen: false, message: 'Market closed (Friday after 4PM CT)' };
|
|
533
525
|
if (ctHour === 16 && ctDay >= 1 && ctDay <= 4) return { isOpen: false, message: 'Daily maintenance (4:00-5:00 PM CT)' };
|
|
526
|
+
|
|
534
527
|
return { isOpen: true, message: 'Market is open' };
|
|
535
528
|
}
|
|
536
529
|
|
|
530
|
+
// ==================== CLEANUP ====================
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Disconnect all connections
|
|
534
|
+
*/
|
|
537
535
|
async disconnect() {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
this.tickerConn = null;
|
|
536
|
+
const connections = [this.orderConn, this.pnlConn, this.tickerConn];
|
|
537
|
+
|
|
538
|
+
for (const conn of connections) {
|
|
539
|
+
if (conn) {
|
|
540
|
+
try {
|
|
541
|
+
await conn.disconnect();
|
|
542
|
+
} catch (err) {
|
|
543
|
+
log.warn('Disconnect error', { error: err.message });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
549
546
|
}
|
|
547
|
+
|
|
548
|
+
this.orderConn = null;
|
|
549
|
+
this.pnlConn = null;
|
|
550
|
+
this.tickerConn = null;
|
|
550
551
|
this.accounts = [];
|
|
551
552
|
this.accountPnL.clear();
|
|
552
553
|
this.positions.clear();
|
|
@@ -554,7 +555,9 @@ class RithmicService extends EventEmitter {
|
|
|
554
555
|
this.loginInfo = null;
|
|
555
556
|
this.user = null;
|
|
556
557
|
this.credentials = null;
|
|
557
|
-
this.
|
|
558
|
+
this._contractsCache = null;
|
|
559
|
+
|
|
560
|
+
log.info('Disconnected');
|
|
558
561
|
}
|
|
559
562
|
}
|
|
560
563
|
|