hedgequantx 1.8.49 → 2.3.0
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 +7 -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
|
@@ -5,76 +5,87 @@
|
|
|
5
5
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const { SECURITY } = require('../config/settings');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const {
|
|
11
|
+
ALGORITHM,
|
|
12
|
+
IV_LENGTH,
|
|
13
|
+
SALT_LENGTH,
|
|
14
|
+
KEY_LENGTH,
|
|
15
|
+
PBKDF2_ITERATIONS,
|
|
16
|
+
TOKEN_VISIBLE_CHARS,
|
|
17
|
+
} = SECURITY;
|
|
18
|
+
|
|
19
|
+
/** @type {Buffer|null} Cached machine key */
|
|
20
|
+
let cachedMachineKey = null;
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Derives a unique machine key from hardware identifiers
|
|
19
|
-
* @returns {
|
|
24
|
+
* @returns {Buffer} Machine-specific key (cached)
|
|
20
25
|
* @private
|
|
21
26
|
*/
|
|
22
27
|
const getMachineKey = () => {
|
|
28
|
+
if (cachedMachineKey) return cachedMachineKey;
|
|
29
|
+
|
|
23
30
|
const components = [
|
|
24
31
|
os.hostname(),
|
|
25
32
|
os.platform(),
|
|
26
33
|
os.arch(),
|
|
27
|
-
os.cpus()[0]?.model || '
|
|
34
|
+
os.cpus()[0]?.model || 'cpu',
|
|
28
35
|
os.homedir(),
|
|
29
|
-
process.env.USER || process.env.USERNAME || 'user'
|
|
30
|
-
];
|
|
31
|
-
|
|
36
|
+
process.env.USER || process.env.USERNAME || 'user',
|
|
37
|
+
].join('|');
|
|
38
|
+
|
|
39
|
+
cachedMachineKey = crypto.createHash('sha256').update(components).digest();
|
|
40
|
+
return cachedMachineKey;
|
|
32
41
|
};
|
|
33
42
|
|
|
34
43
|
/**
|
|
35
44
|
* Derives encryption key from password and salt using PBKDF2
|
|
36
|
-
* @param {string} password - Password to derive key from
|
|
45
|
+
* @param {Buffer|string} password - Password to derive key from
|
|
37
46
|
* @param {Buffer} salt - Salt for key derivation
|
|
38
47
|
* @returns {Buffer} Derived key
|
|
39
48
|
* @private
|
|
40
49
|
*/
|
|
41
50
|
const deriveKey = (password, salt) => {
|
|
42
|
-
|
|
51
|
+
const pwd = Buffer.isBuffer(password) ? password : Buffer.from(password);
|
|
52
|
+
return crypto.pbkdf2Sync(pwd, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
|
|
43
53
|
};
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
56
|
* Encrypts data using AES-256-GCM
|
|
47
57
|
* @param {string} plaintext - Data to encrypt
|
|
48
|
-
* @param {string} [password] - Optional password (uses machine key if not provided)
|
|
58
|
+
* @param {Buffer|string} [password] - Optional password (uses machine key if not provided)
|
|
49
59
|
* @returns {string} Encrypted data as hex string (salt:iv:authTag:ciphertext)
|
|
50
60
|
*/
|
|
51
61
|
const encrypt = (plaintext, password = null) => {
|
|
52
62
|
if (!plaintext) return '';
|
|
53
63
|
|
|
54
|
-
const secret = password
|
|
64
|
+
const secret = password ? Buffer.from(password) : getMachineKey();
|
|
55
65
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
56
|
-
const key = deriveKey(secret, salt);
|
|
57
66
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
67
|
+
const key = deriveKey(secret, salt);
|
|
58
68
|
|
|
59
69
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
const encrypted = Buffer.concat([
|
|
71
|
+
cipher.update(plaintext, 'utf8'),
|
|
72
|
+
cipher.final(),
|
|
73
|
+
]);
|
|
62
74
|
|
|
63
75
|
const authTag = cipher.getAuthTag();
|
|
64
76
|
|
|
65
|
-
// Format: salt:iv:authTag:ciphertext (all in hex)
|
|
66
77
|
return [
|
|
67
78
|
salt.toString('hex'),
|
|
68
79
|
iv.toString('hex'),
|
|
69
80
|
authTag.toString('hex'),
|
|
70
|
-
encrypted
|
|
81
|
+
encrypted.toString('hex'),
|
|
71
82
|
].join(':');
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
/**
|
|
75
86
|
* Decrypts data encrypted with AES-256-GCM
|
|
76
87
|
* @param {string} encryptedData - Encrypted data as hex string
|
|
77
|
-
* @param {string} [password] - Optional password (uses machine key if not provided)
|
|
88
|
+
* @param {Buffer|string} [password] - Optional password (uses machine key if not provided)
|
|
78
89
|
* @returns {string|null} Decrypted plaintext or null if decryption fails
|
|
79
90
|
*/
|
|
80
91
|
const decrypt = (encryptedData, password = null) => {
|
|
@@ -85,40 +96,41 @@ const decrypt = (encryptedData, password = null) => {
|
|
|
85
96
|
if (parts.length !== 4) return null;
|
|
86
97
|
|
|
87
98
|
const [saltHex, ivHex, authTagHex, ciphertext] = parts;
|
|
88
|
-
|
|
89
99
|
const salt = Buffer.from(saltHex, 'hex');
|
|
90
100
|
const iv = Buffer.from(ivHex, 'hex');
|
|
91
101
|
const authTag = Buffer.from(authTagHex, 'hex');
|
|
102
|
+
const encrypted = Buffer.from(ciphertext, 'hex');
|
|
92
103
|
|
|
93
|
-
const secret = password
|
|
104
|
+
const secret = password ? Buffer.from(password) : getMachineKey();
|
|
94
105
|
const key = deriveKey(secret, salt);
|
|
95
106
|
|
|
96
107
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
97
108
|
decipher.setAuthTag(authTag);
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
const decrypted = Buffer.concat([
|
|
111
|
+
decipher.update(encrypted),
|
|
112
|
+
decipher.final(),
|
|
113
|
+
]);
|
|
101
114
|
|
|
102
|
-
return decrypted;
|
|
103
|
-
} catch
|
|
104
|
-
// Decryption failed (wrong key, tampered data, etc.)
|
|
115
|
+
return decrypted.toString('utf8');
|
|
116
|
+
} catch {
|
|
105
117
|
return null;
|
|
106
118
|
}
|
|
107
119
|
};
|
|
108
120
|
|
|
109
121
|
/**
|
|
110
|
-
* Hashes a password using
|
|
122
|
+
* Hashes a password using PBKDF2-SHA512
|
|
111
123
|
* @param {string} password - Password to hash
|
|
112
124
|
* @returns {string} Hashed password (salt:hash in hex)
|
|
113
125
|
*/
|
|
114
126
|
const hashPassword = (password) => {
|
|
115
127
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
116
|
-
const hash = crypto.pbkdf2Sync(password, salt,
|
|
117
|
-
return salt.toString('hex')
|
|
128
|
+
const hash = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 64, 'sha512');
|
|
129
|
+
return `${salt.toString('hex')}:${hash.toString('hex')}`;
|
|
118
130
|
};
|
|
119
131
|
|
|
120
132
|
/**
|
|
121
|
-
* Verifies a password against a hash
|
|
133
|
+
* Verifies a password against a hash using constant-time comparison
|
|
122
134
|
* @param {string} password - Password to verify
|
|
123
135
|
* @param {string} storedHash - Stored hash (salt:hash)
|
|
124
136
|
* @returns {boolean} True if password matches
|
|
@@ -126,9 +138,13 @@ const hashPassword = (password) => {
|
|
|
126
138
|
const verifyPassword = (password, storedHash) => {
|
|
127
139
|
try {
|
|
128
140
|
const [saltHex, hashHex] = storedHash.split(':');
|
|
141
|
+
if (!saltHex || !hashHex) return false;
|
|
142
|
+
|
|
129
143
|
const salt = Buffer.from(saltHex, 'hex');
|
|
130
|
-
const
|
|
131
|
-
|
|
144
|
+
const storedHashBuffer = Buffer.from(hashHex, 'hex');
|
|
145
|
+
const hash = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 64, 'sha512');
|
|
146
|
+
|
|
147
|
+
return crypto.timingSafeEqual(hash, storedHashBuffer);
|
|
132
148
|
} catch {
|
|
133
149
|
return false;
|
|
134
150
|
}
|
|
@@ -139,23 +155,40 @@ const verifyPassword = (password, storedHash) => {
|
|
|
139
155
|
* @param {number} [length=32] - Token length in bytes
|
|
140
156
|
* @returns {string} Random token as hex string
|
|
141
157
|
*/
|
|
142
|
-
const generateToken = (length = 32) =>
|
|
143
|
-
return crypto.randomBytes(length).toString('hex');
|
|
144
|
-
};
|
|
158
|
+
const generateToken = (length = 32) => crypto.randomBytes(length).toString('hex');
|
|
145
159
|
|
|
146
160
|
/**
|
|
147
161
|
* Masks sensitive data for logging
|
|
148
162
|
* @param {string} data - Data to mask
|
|
149
|
-
* @param {number} [visibleChars
|
|
163
|
+
* @param {number} [visibleChars] - Number of visible characters at start/end
|
|
150
164
|
* @returns {string} Masked data
|
|
151
165
|
*/
|
|
152
|
-
const maskSensitive = (data, visibleChars =
|
|
153
|
-
if (!data || data
|
|
154
|
-
|
|
166
|
+
const maskSensitive = (data, visibleChars = TOKEN_VISIBLE_CHARS) => {
|
|
167
|
+
if (!data || typeof data !== 'string') return '****';
|
|
168
|
+
if (data.length <= visibleChars * 2) return '****';
|
|
169
|
+
|
|
170
|
+
return `${data.slice(0, visibleChars)}****${data.slice(-visibleChars)}`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Securely clears sensitive data from a buffer
|
|
175
|
+
* @param {Buffer} buffer - Buffer to clear
|
|
176
|
+
*/
|
|
177
|
+
const secureWipe = (buffer) => {
|
|
178
|
+
if (Buffer.isBuffer(buffer)) {
|
|
179
|
+
crypto.randomFillSync(buffer);
|
|
180
|
+
buffer.fill(0);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Clears cached machine key (call on logout for extra security)
|
|
186
|
+
*/
|
|
187
|
+
const clearCache = () => {
|
|
188
|
+
if (cachedMachineKey) {
|
|
189
|
+
secureWipe(cachedMachineKey);
|
|
190
|
+
cachedMachineKey = null;
|
|
155
191
|
}
|
|
156
|
-
const start = data.substring(0, visibleChars);
|
|
157
|
-
const end = data.substring(data.length - visibleChars);
|
|
158
|
-
return `${start}****${end}`;
|
|
159
192
|
};
|
|
160
193
|
|
|
161
194
|
module.exports = {
|
|
@@ -164,5 +197,7 @@ module.exports = {
|
|
|
164
197
|
hashPassword,
|
|
165
198
|
verifyPassword,
|
|
166
199
|
generateToken,
|
|
167
|
-
maskSensitive
|
|
200
|
+
maskSensitive,
|
|
201
|
+
secureWipe,
|
|
202
|
+
clearCache,
|
|
168
203
|
};
|
package/src/security/index.js
CHANGED
|
@@ -9,7 +9,9 @@ const {
|
|
|
9
9
|
hashPassword,
|
|
10
10
|
verifyPassword,
|
|
11
11
|
generateToken,
|
|
12
|
-
maskSensitive
|
|
12
|
+
maskSensitive,
|
|
13
|
+
secureWipe,
|
|
14
|
+
clearCache,
|
|
13
15
|
} = require('./encryption');
|
|
14
16
|
|
|
15
17
|
const {
|
|
@@ -22,14 +24,14 @@ const {
|
|
|
22
24
|
validatePrice,
|
|
23
25
|
validateSymbol,
|
|
24
26
|
sanitizeString,
|
|
25
|
-
validateObject
|
|
27
|
+
validateObject,
|
|
26
28
|
} = require('./validation');
|
|
27
29
|
|
|
28
30
|
const {
|
|
29
31
|
RateLimiter,
|
|
30
|
-
rateLimiters,
|
|
31
32
|
getLimiter,
|
|
32
|
-
withRateLimit
|
|
33
|
+
withRateLimit,
|
|
34
|
+
resetAll,
|
|
33
35
|
} = require('./rateLimit');
|
|
34
36
|
|
|
35
37
|
module.exports = {
|
|
@@ -40,7 +42,9 @@ module.exports = {
|
|
|
40
42
|
verifyPassword,
|
|
41
43
|
generateToken,
|
|
42
44
|
maskSensitive,
|
|
43
|
-
|
|
45
|
+
secureWipe,
|
|
46
|
+
clearCache,
|
|
47
|
+
|
|
44
48
|
// Validation
|
|
45
49
|
ValidationError,
|
|
46
50
|
validateUsername,
|
|
@@ -52,10 +56,10 @@ module.exports = {
|
|
|
52
56
|
validateSymbol,
|
|
53
57
|
sanitizeString,
|
|
54
58
|
validateObject,
|
|
55
|
-
|
|
59
|
+
|
|
56
60
|
// Rate Limiting
|
|
57
61
|
RateLimiter,
|
|
58
|
-
rateLimiters,
|
|
59
62
|
getLimiter,
|
|
60
|
-
withRateLimit
|
|
63
|
+
withRateLimit,
|
|
64
|
+
resetAll,
|
|
61
65
|
};
|
|
@@ -3,95 +3,101 @@
|
|
|
3
3
|
* @module security/rateLimit
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { RATE_LIMITS } = require('../config/settings');
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* High-performance rate limiter using sliding window
|
|
8
10
|
*/
|
|
9
11
|
class RateLimiter {
|
|
10
12
|
/**
|
|
11
|
-
* Creates a new rate limiter
|
|
12
13
|
* @param {Object} options - Rate limiter options
|
|
13
14
|
* @param {number} [options.maxRequests=60] - Maximum requests per window
|
|
14
15
|
* @param {number} [options.windowMs=60000] - Time window in milliseconds
|
|
15
|
-
* @param {number} [options.minInterval=100] - Minimum interval between requests
|
|
16
|
+
* @param {number} [options.minInterval=100] - Minimum interval between requests
|
|
16
17
|
*/
|
|
17
|
-
constructor(
|
|
18
|
-
this.maxRequests =
|
|
19
|
-
this.windowMs =
|
|
20
|
-
this.minInterval =
|
|
21
|
-
this.
|
|
18
|
+
constructor({ maxRequests = 60, windowMs = 60000, minInterval = 100 } = {}) {
|
|
19
|
+
this.maxRequests = maxRequests;
|
|
20
|
+
this.windowMs = windowMs;
|
|
21
|
+
this.minInterval = minInterval;
|
|
22
|
+
this.timestamps = [];
|
|
22
23
|
this.lastRequest = 0;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* Removes expired timestamps from the window
|
|
27
28
|
* @private
|
|
28
29
|
*/
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
this.
|
|
30
|
+
_prune() {
|
|
31
|
+
const cutoff = Date.now() - this.windowMs;
|
|
32
|
+
// Binary search would be overkill for typical request counts
|
|
33
|
+
while (this.timestamps.length && this.timestamps[0] <= cutoff) {
|
|
34
|
+
this.timestamps.shift();
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
* Checks if a request is allowed
|
|
37
|
-
* @returns {boolean}
|
|
39
|
+
* Checks if a request is allowed without recording it
|
|
40
|
+
* @returns {boolean}
|
|
38
41
|
*/
|
|
39
42
|
canRequest() {
|
|
40
|
-
this.
|
|
43
|
+
this._prune();
|
|
41
44
|
const now = Date.now();
|
|
42
45
|
|
|
43
|
-
// Check minimum interval
|
|
44
46
|
if (now - this.lastRequest < this.minInterval) {
|
|
45
47
|
return false;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
return this.requests.length < this.maxRequests;
|
|
50
|
+
return this.timestamps.length < this.maxRequests;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
* Records a request
|
|
54
|
+
* Records a request timestamp
|
|
54
55
|
*/
|
|
55
56
|
recordRequest() {
|
|
56
57
|
const now = Date.now();
|
|
57
|
-
this.
|
|
58
|
+
this.timestamps.push(now);
|
|
58
59
|
this.lastRequest = now;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Gets remaining requests in current window
|
|
63
|
-
* @returns {number}
|
|
64
|
+
* @returns {number}
|
|
64
65
|
*/
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
return Math.max(0, this.maxRequests - this.
|
|
66
|
+
get remaining() {
|
|
67
|
+
this._prune();
|
|
68
|
+
return Math.max(0, this.maxRequests - this.timestamps.length);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
* Gets
|
|
72
|
-
* @returns {number}
|
|
72
|
+
* Gets milliseconds until next slot is available
|
|
73
|
+
* @returns {number}
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
-
if (this.
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
get resetIn() {
|
|
76
|
+
if (!this.timestamps.length) return 0;
|
|
77
|
+
this._prune();
|
|
78
|
+
if (this.timestamps.length < this.maxRequests) return 0;
|
|
79
|
+
return Math.max(0, this.timestamps[0] + this.windowMs - Date.now());
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
* Waits until a request is
|
|
82
|
-
* @returns {Promise<void>}
|
|
83
|
+
* Waits until a request slot is available
|
|
84
|
+
* @returns {Promise<void>}
|
|
83
85
|
*/
|
|
84
86
|
async waitForSlot() {
|
|
85
87
|
while (!this.canRequest()) {
|
|
86
|
-
const waitTime = Math.max(
|
|
87
|
-
|
|
88
|
+
const waitTime = Math.max(
|
|
89
|
+
this.minInterval - (Date.now() - this.lastRequest),
|
|
90
|
+
Math.min(this.resetIn, 1000)
|
|
91
|
+
);
|
|
92
|
+
await new Promise(r => setTimeout(r, Math.max(1, waitTime)));
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
97
|
* Executes a function with rate limiting
|
|
93
|
-
* @
|
|
94
|
-
* @
|
|
98
|
+
* @template T
|
|
99
|
+
* @param {() => Promise<T>} fn - Function to execute
|
|
100
|
+
* @returns {Promise<T>}
|
|
95
101
|
*/
|
|
96
102
|
async execute(fn) {
|
|
97
103
|
await this.waitForSlot();
|
|
@@ -103,53 +109,50 @@ class RateLimiter {
|
|
|
103
109
|
* Resets the rate limiter
|
|
104
110
|
*/
|
|
105
111
|
reset() {
|
|
106
|
-
this.
|
|
112
|
+
this.timestamps = [];
|
|
107
113
|
this.lastRequest = 0;
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Login attempts - 5 per minute (stricter for security)
|
|
119
|
-
login: new RateLimiter({ maxRequests: 5, windowMs: 60000, minInterval: 2000 }),
|
|
120
|
-
|
|
121
|
-
// Order placement - 30 per minute
|
|
122
|
-
orders: new RateLimiter({ maxRequests: 30, windowMs: 60000, minInterval: 200 }),
|
|
123
|
-
|
|
124
|
-
// Data fetching - 120 per minute
|
|
125
|
-
data: new RateLimiter({ maxRequests: 120, windowMs: 60000, minInterval: 50 })
|
|
126
|
-
};
|
|
117
|
+
/** @type {Map<string, RateLimiter>} */
|
|
118
|
+
const limiters = new Map([
|
|
119
|
+
['api', new RateLimiter(RATE_LIMITS.API)],
|
|
120
|
+
['login', new RateLimiter(RATE_LIMITS.LOGIN)],
|
|
121
|
+
['orders', new RateLimiter(RATE_LIMITS.ORDERS)],
|
|
122
|
+
['data', new RateLimiter(RATE_LIMITS.DATA)],
|
|
123
|
+
]);
|
|
127
124
|
|
|
128
125
|
/**
|
|
129
126
|
* Gets a rate limiter by name
|
|
130
127
|
* @param {string} name - Rate limiter name
|
|
131
|
-
* @returns {RateLimiter}
|
|
128
|
+
* @returns {RateLimiter}
|
|
132
129
|
*/
|
|
133
|
-
const getLimiter = (name) =>
|
|
134
|
-
return rateLimiters[name] || rateLimiters.api;
|
|
135
|
-
};
|
|
130
|
+
const getLimiter = (name) => limiters.get(name) || limiters.get('api');
|
|
136
131
|
|
|
137
132
|
/**
|
|
138
133
|
* Wraps an async function with rate limiting
|
|
139
|
-
* @
|
|
134
|
+
* @template T
|
|
135
|
+
* @param {(...args: any[]) => Promise<T>} fn - Function to wrap
|
|
140
136
|
* @param {string} [limiterName='api'] - Rate limiter to use
|
|
141
|
-
* @returns {
|
|
137
|
+
* @returns {(...args: any[]) => Promise<T>}
|
|
142
138
|
*/
|
|
143
139
|
const withRateLimit = (fn, limiterName = 'api') => {
|
|
144
140
|
const limiter = getLimiter(limiterName);
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
return (...args) => limiter.execute(() => fn(...args));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resets all rate limiters
|
|
146
|
+
*/
|
|
147
|
+
const resetAll = () => {
|
|
148
|
+
for (const limiter of limiters.values()) {
|
|
149
|
+
limiter.reset();
|
|
150
|
+
}
|
|
148
151
|
};
|
|
149
152
|
|
|
150
153
|
module.exports = {
|
|
151
154
|
RateLimiter,
|
|
152
|
-
rateLimiters,
|
|
153
155
|
getLimiter,
|
|
154
|
-
withRateLimit
|
|
156
|
+
withRateLimit,
|
|
157
|
+
resetAll,
|
|
155
158
|
};
|