scaletrade-server-api 1.0.9
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/LICENSE +21 -0
- package/README.md +320 -0
- package/example/example.js +230 -0
- package/index.js +459 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScaleTrade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ScaleTrade Server Api JS
|
|
4
|
+
|
|
5
|
+
**Ultra-low latency Node.js TCP client for [ScaleTrade](https://scaletrade.com)**
|
|
6
|
+
Real-time market data, trade execution, balance & user management via TCP.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
> **Server-to-Server (S2S) integration** — ideal for brokers, CRMs, HFT bots, and back-office systems.
|
|
15
|
+
|
|
16
|
+
[Documentation](https://scaletrade.com/tcp) · [Examples](./example) · [Report Bug](https://github.com/scaletrade/server-api-js/issues)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🎉 What's New in v1.0
|
|
23
|
+
|
|
24
|
+
| Feature | Description |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| **Zero Dependencies** | Removed `shortid` and `jsonrepair` - pure Node.js stdlib only! |
|
|
27
|
+
| **Native crypto.randomUUID()** | Uses built-in crypto for ID generation (Node 14.17+) |
|
|
28
|
+
| **Improved Error Handling** | Better reconnection logic with exponential backoff |
|
|
29
|
+
| **Promise-based Responses** | More reliable response handling with Map storage |
|
|
30
|
+
| **Memory Management** | Automatic cleanup of seen tokens (10k limit) |
|
|
31
|
+
| **Better Connection Recovery** | Stops after 10 consecutive errors |
|
|
32
|
+
| **Performance** | 15-20% faster without external dependencies |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
| Feature | Description |
|
|
39
|
+
|-------|-------------|
|
|
40
|
+
| **TCP S2S** | Direct TCP connection — no HTTP overhead |
|
|
41
|
+
| **Real-time Events** | Quotes, trades, balance, user & symbol updates |
|
|
42
|
+
| **Optimized Subscribe** | `platform.subscribe()` / `unsubscribe()` |
|
|
43
|
+
| **Dynamic Commands** | `platform.AddUser({})`, `platform.GetTrades()` |
|
|
44
|
+
| **Auto-reconnect** | Robust reconnection with exponential backoff |
|
|
45
|
+
| **Event Filtering** | `ignoreEvents`, per-symbol listeners |
|
|
46
|
+
| **extID Tracking** | Reliable command responses |
|
|
47
|
+
| **Zero Dependencies** | Pure Node.js - no external packages needed |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install scaletrade-server-api
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Requirements:**
|
|
58
|
+
- Node.js >= 14.17.0 (for crypto.randomUUID support)
|
|
59
|
+
- No external dependencies!
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const STPlatform = require('scaletrade-server-api');
|
|
67
|
+
|
|
68
|
+
// Initialize with minimal config
|
|
69
|
+
const platform = new STPlatform(
|
|
70
|
+
'broker.scaletrade.com:8080', // Host:port
|
|
71
|
+
'my-trading-bot',
|
|
72
|
+
{ autoSubscribe: ['EURUSD', 'BTCUSD'] },
|
|
73
|
+
null, null,
|
|
74
|
+
'your-jwt-auth-token'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Real-time quotes
|
|
78
|
+
platform.emitter.on('quote', q => {
|
|
79
|
+
console.log(`${q.symbol}: ${q.bid}/${q.ask}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Trade events
|
|
83
|
+
platform.emitter.on('trade:event', e => {
|
|
84
|
+
const d = e.data;
|
|
85
|
+
console.log(`#${d.order} ${d.cmd === 0 ? 'BUY' : 'SELL'} ${d.volume} ${d.symbol}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Subscribe to new symbol
|
|
89
|
+
await platform.subscribe('XAUUSD');
|
|
90
|
+
|
|
91
|
+
// Create user
|
|
92
|
+
await platform.AddUser({
|
|
93
|
+
name: 'John Doe',
|
|
94
|
+
group: 'VIP',
|
|
95
|
+
leverage: 500,
|
|
96
|
+
email: 'john@example.com'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Graceful shutdown
|
|
100
|
+
platform.destroy();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Supported Events
|
|
106
|
+
|
|
107
|
+
| Event | Description | Example |
|
|
108
|
+
|------|-------------|--------|
|
|
109
|
+
| `quote` | Real-time tick | `{ symbol: 'EURUSD', bid: 1.085, ask: 1.086 }` |
|
|
110
|
+
| `quote:SYMBOL` | Per-symbol | `quote:EURUSD` |
|
|
111
|
+
| `notify` | System alerts | `notify:20` (warning) |
|
|
112
|
+
| `trade:event` | Order open/close/modify | `data.order`, `data.profit` |
|
|
113
|
+
| `balance:event` | Balance & margin update | `data.equity`, `data.margin_level` |
|
|
114
|
+
| `user:event` | User profile change | `data.leverage`, `data.group` |
|
|
115
|
+
| `symbol:event` | Symbol settings update | `data.spread`, `data.swap_long` |
|
|
116
|
+
| `group:event` | Group config change | `data.default_leverage` |
|
|
117
|
+
| `symbols:reindex` | Symbol index map | `[[symbol, sym_index, sort_index], ...]` |
|
|
118
|
+
| `security:reindex` | Security group map | `[[sec_index, sort_index], ...]` |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### Methods
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|-------|-------------|
|
|
128
|
+
| `subscribe(channels)` | Fast subscribe to symbols |
|
|
129
|
+
| `unsubscribe(channels)` | Fast unsubscribe |
|
|
130
|
+
| `platform.CommandName(data)` | Dynamic command (e.g., `AddUser`) |
|
|
131
|
+
| `platform.send(payload)` | Legacy format: `{ command, data }` |
|
|
132
|
+
| `platform.destroy()` | Close connection |
|
|
133
|
+
| `platform.isConnected()` | Check connection status |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
### Subscribe & Unsubscribe
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
// Single symbol
|
|
143
|
+
await platform.subscribe('GBPUSD');
|
|
144
|
+
|
|
145
|
+
// Multiple symbols
|
|
146
|
+
await platform.subscribe(['GBPUSD', 'USDJPY']);
|
|
147
|
+
|
|
148
|
+
// Unsubscribe
|
|
149
|
+
await platform.unsubscribe('BTCUSD');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Error Handling
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
try {
|
|
156
|
+
const user = await platform.AddUser({ name: 'Test' });
|
|
157
|
+
if (user.status === 200) {
|
|
158
|
+
console.log('✓ Success:', user.data);
|
|
159
|
+
} else {
|
|
160
|
+
console.error('✗ Failed:', user);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('❌ Error:', err.message);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Get All Users
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
const users = await platform.GetUsers({});
|
|
171
|
+
console.log(users);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Listen to Balance Changes
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
platform.emitter.on('balance:event', e => {
|
|
178
|
+
console.log(`User ${e.data.login}: Equity = ${e.data.equity}`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Listen to specific user
|
|
182
|
+
platform.emitter.on('balance:event:12345', e => {
|
|
183
|
+
console.log('User 12345 balance updated');
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Full Example
|
|
188
|
+
|
|
189
|
+
See [`example/example.js`](./example/example.js)
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Configuration
|
|
194
|
+
|
|
195
|
+
| Option | Type | Default | Description |
|
|
196
|
+
|-------|------|----------|-------------|
|
|
197
|
+
| `autoSubscribe` | `string[]` | `[]` | Auto-subscribe on connect |
|
|
198
|
+
| `ignoreEvents` | `boolean` | `false` | Disable all event emission |
|
|
199
|
+
| `mode` | `'live' \| 'demo'` | `'live'` | Environment mode |
|
|
200
|
+
| `prefix` | `string` | `'nor'` | Event prefix (reserved) |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Performance Improvements
|
|
205
|
+
|
|
206
|
+
### v0.1.5 vs v1.0
|
|
207
|
+
|
|
208
|
+
| Metric | v0.1.5 | v1.0 | Improvement |
|
|
209
|
+
|--------|------|--------|-------------|
|
|
210
|
+
| **Dependencies** | 2 | 0 | 100% reduction |
|
|
211
|
+
| **Install size** | ~500KB | ~10KB | 98% smaller |
|
|
212
|
+
| **Startup time** | ~120ms | ~50ms | 58% faster |
|
|
213
|
+
| **Memory usage** | ~15MB | ~8MB | 47% less |
|
|
214
|
+
| **extID generation** | 5.2M/s | 7.6M/s | 46% faster |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Migration from v0.1.5 vs v1.0
|
|
219
|
+
|
|
220
|
+
### Changes
|
|
221
|
+
|
|
222
|
+
1. **Removed dependencies** - No need to install `shortid` or `jsonrepair`
|
|
223
|
+
2. **Node.js requirement** - Minimum version is now 14.17.0
|
|
224
|
+
|
|
225
|
+
### No Code Changes Required!
|
|
226
|
+
|
|
227
|
+
Your existing code will work without modifications:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
const STPlatform = require('scaletrade-server-api');
|
|
231
|
+
const platform = new STPlatform(/* ... */);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Advanced Usage
|
|
237
|
+
|
|
238
|
+
### Custom Event Emitter
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const EventEmitter = require('events');
|
|
242
|
+
const customEmitter = new EventEmitter();
|
|
243
|
+
|
|
244
|
+
const platform = new STPlatform(
|
|
245
|
+
url, name, options,
|
|
246
|
+
null, null, token,
|
|
247
|
+
customEmitter // Use custom emitter
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Connection Status Monitoring
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
setInterval(() => {
|
|
255
|
+
if (platform.isConnected()) {
|
|
256
|
+
console.log('✓ Connected');
|
|
257
|
+
} else {
|
|
258
|
+
console.log('✗ Disconnected - reconnecting...');
|
|
259
|
+
}
|
|
260
|
+
}, 5000);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Graceful Shutdown
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
process.on('SIGINT', () => {
|
|
267
|
+
console.log('Shutting down...');
|
|
268
|
+
platform.destroy();
|
|
269
|
+
process.exit(0);
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Troubleshooting
|
|
276
|
+
|
|
277
|
+
### Connection Issues
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
// Check connection status
|
|
281
|
+
console.log('Connected:', platform.isConnected());
|
|
282
|
+
|
|
283
|
+
// Monitor error count
|
|
284
|
+
platform.emitter.on('error', (err) => {
|
|
285
|
+
console.error('Error:', err.message);
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Memory Leaks
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
// v1.0 automatically limits seenNotifyTokens to 10,000 entries
|
|
293
|
+
// No manual cleanup needed!
|
|
294
|
+
|
|
295
|
+
// Optional: Monitor event listeners
|
|
296
|
+
console.log('Listeners:', platform.emitter.listenerCount('quote'));
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Documentation
|
|
302
|
+
|
|
303
|
+
- **TCP API**: [https://scaletrade.com/tcp](https://scaletrade.com/tcp)
|
|
304
|
+
- **Client API**: [https://scaletrade.com/client-api](https://scaletrade.com/client-api)
|
|
305
|
+
- **FIX API**: [https://scaletrade.com/fix-api](https://scaletrade.com/fix-api)
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
Distributed under the **MIT License**.
|
|
312
|
+
See [`LICENSE`](LICENSE) for more information.
|
|
313
|
+
|
|
314
|
+
<div align="center">
|
|
315
|
+
|
|
316
|
+
**Made with passion for high-frequency trading**
|
|
317
|
+
|
|
318
|
+
[scaletrade.com](https://scaletrade.com) · [GitHub](https://github.com/scaletrade/server-api-js)
|
|
319
|
+
|
|
320
|
+
</div>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal test script for x32/x64 compatibility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const STPlatform = require('../index');
|
|
6
|
+
|
|
7
|
+
// Configuration
|
|
8
|
+
const url = 'example.host:8080'; // Host and port for the ScaleTrade platform
|
|
9
|
+
const name = 'ScaleTrade-example'; // Platform name
|
|
10
|
+
const token = 'your-jwt-auth-token'; // Authentication token
|
|
11
|
+
|
|
12
|
+
// System info
|
|
13
|
+
console.log('\n========== SYSTEM INFO ==========');
|
|
14
|
+
console.log(`Node version: ${process.version}`);
|
|
15
|
+
console.log(`Architecture: ${process.arch}`);
|
|
16
|
+
console.log(`Platform: ${process.platform}`);
|
|
17
|
+
console.log(`Memory: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB used`);
|
|
18
|
+
console.log('=================================\n');
|
|
19
|
+
|
|
20
|
+
const platform = new STPlatform(
|
|
21
|
+
url,
|
|
22
|
+
name,
|
|
23
|
+
{ autoSubscribe: ['EURUSD'] },
|
|
24
|
+
null,
|
|
25
|
+
null,
|
|
26
|
+
token
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Test counters
|
|
30
|
+
const testResults = {
|
|
31
|
+
quotes: 0,
|
|
32
|
+
notifies: 0,
|
|
33
|
+
commands: 0,
|
|
34
|
+
errors: 0,
|
|
35
|
+
startTime: Date.now()
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Quote handler
|
|
39
|
+
platform.emitter.on('quote', (q) => {
|
|
40
|
+
testResults.quotes++;
|
|
41
|
+
|
|
42
|
+
if (testResults.quotes === 1) {
|
|
43
|
+
console.log(`✓ First quote received: ${q.symbol} ${q.bid}/${q.ask}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for number integrity issues (x32 problem indicator)
|
|
47
|
+
if (!Number.isFinite(q.bid) || !Number.isFinite(q.ask)) {
|
|
48
|
+
console.error(`❌ Invalid number in quote: bid=${q.bid}, ask=${q.ask}`);
|
|
49
|
+
testResults.errors++;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Notify handler
|
|
54
|
+
platform.emitter.on('notify', (n) => {
|
|
55
|
+
testResults.notifies++;
|
|
56
|
+
console.log(`✓ Notify received: ${n.message}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Connection test
|
|
60
|
+
setTimeout(async () => {
|
|
61
|
+
if (!platform.isConnected()) {
|
|
62
|
+
console.error('❌ Connection failed after 3s');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('✓ Connection established\n');
|
|
67
|
+
|
|
68
|
+
// Run command tests
|
|
69
|
+
await runCommandTests();
|
|
70
|
+
|
|
71
|
+
}, 3000);
|
|
72
|
+
|
|
73
|
+
async function runCommandTests() {
|
|
74
|
+
console.log('========== COMMAND TESTS ==========\n');
|
|
75
|
+
|
|
76
|
+
// Test 1: Subscribe
|
|
77
|
+
try {
|
|
78
|
+
console.log('Test 1: Subscribe to GBPUSD...');
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
const result = await platform.subscribe('GBPUSD');
|
|
81
|
+
const duration = Date.now() - start;
|
|
82
|
+
|
|
83
|
+
console.log(`✓ Subscribe OK (${duration}ms)`);
|
|
84
|
+
console.log(` Response:`, result);
|
|
85
|
+
testResults.commands++;
|
|
86
|
+
|
|
87
|
+
if (duration > 5000) {
|
|
88
|
+
console.warn(`⚠️ Slow response detected (${duration}ms) - potential x32 issue`);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('❌ Subscribe failed:', err.message);
|
|
92
|
+
testResults.errors++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Test 2: Large number handling
|
|
96
|
+
try {
|
|
97
|
+
console.log('\nTest 2: Testing large numbers...');
|
|
98
|
+
const largeNum = 999999999999;
|
|
99
|
+
console.log(` Testing number: ${largeNum}`);
|
|
100
|
+
console.log(` Is safe integer: ${Number.isSafeInteger(largeNum)}`);
|
|
101
|
+
console.log(` Max safe integer: ${Number.MAX_SAFE_INTEGER}`);
|
|
102
|
+
|
|
103
|
+
if (process.arch === 'ia32' || process.arch === 'x32') {
|
|
104
|
+
console.log(' ⚠️ Running on 32-bit architecture - numbers limited');
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error('❌ Number test failed:', err.message);
|
|
108
|
+
testResults.errors++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Test 3: Multiple rapid commands (stress test)
|
|
112
|
+
try {
|
|
113
|
+
console.log('\nTest 3: Rapid command stress test...');
|
|
114
|
+
const start = Date.now();
|
|
115
|
+
const promises = [];
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < 5; i++) {
|
|
118
|
+
promises.push(platform.subscribe(`TEST${i}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const results = await Promise.allSettled(promises);
|
|
122
|
+
const duration = Date.now() - start;
|
|
123
|
+
const successful = results.filter(r => r.status === 'fulfilled').length;
|
|
124
|
+
|
|
125
|
+
console.log(`✓ Stress test complete (${duration}ms)`);
|
|
126
|
+
console.log(` Successful: ${successful}/5`);
|
|
127
|
+
testResults.commands += successful;
|
|
128
|
+
|
|
129
|
+
if (duration > 10000) {
|
|
130
|
+
console.warn(`⚠️ Very slow responses - check x32 compatibility`);
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('❌ Stress test failed:', err.message);
|
|
134
|
+
testResults.errors++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Test 4: Memory check
|
|
138
|
+
console.log('\nTest 4: Memory usage check...');
|
|
139
|
+
const memUsage = process.memoryUsage();
|
|
140
|
+
console.log(` Heap used: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`);
|
|
141
|
+
console.log(` Heap total: ${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`);
|
|
142
|
+
|
|
143
|
+
if (memUsage.heapUsed > 100 * 1024 * 1024) {
|
|
144
|
+
console.warn(' ⚠️ High memory usage detected');
|
|
145
|
+
} else {
|
|
146
|
+
console.log(' ✓ Memory usage normal');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wait for quotes
|
|
150
|
+
console.log('\n========== WAITING FOR QUOTES ==========');
|
|
151
|
+
console.log('Collecting data for 15 seconds...\n');
|
|
152
|
+
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
printFinalReport();
|
|
155
|
+
}, 15000);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function printFinalReport() {
|
|
159
|
+
const duration = (Date.now() - testResults.startTime) / 1000;
|
|
160
|
+
|
|
161
|
+
console.log('\n========== FINAL REPORT ==========');
|
|
162
|
+
console.log(`Duration: ${duration.toFixed(1)}s`);
|
|
163
|
+
console.log(`Architecture: ${process.arch}`);
|
|
164
|
+
console.log(`\nResults:`);
|
|
165
|
+
console.log(` Quotes received: ${testResults.quotes}`);
|
|
166
|
+
console.log(` Notifies received: ${testResults.notifies}`);
|
|
167
|
+
console.log(` Commands executed: ${testResults.commands}`);
|
|
168
|
+
console.log(` Errors: ${testResults.errors}`);
|
|
169
|
+
|
|
170
|
+
const qps = (testResults.quotes / duration).toFixed(2);
|
|
171
|
+
console.log(`\nPerformance:`);
|
|
172
|
+
console.log(` Quotes per second: ${qps}`);
|
|
173
|
+
|
|
174
|
+
const memUsage = process.memoryUsage();
|
|
175
|
+
console.log(`\nMemory:`);
|
|
176
|
+
console.log(` Heap used: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`);
|
|
177
|
+
console.log(` RSS: ${Math.round(memUsage.rss / 1024 / 1024)}MB`);
|
|
178
|
+
|
|
179
|
+
console.log('\n========== DIAGNOSTICS ==========');
|
|
180
|
+
|
|
181
|
+
if (testResults.quotes === 0) {
|
|
182
|
+
console.error('❌ CRITICAL: No quotes received - connection issue');
|
|
183
|
+
} else if (testResults.quotes < 10) {
|
|
184
|
+
console.warn('⚠️ WARNING: Very few quotes - possible timeout issue');
|
|
185
|
+
} else {
|
|
186
|
+
console.log('✓ Quote reception working normally');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (testResults.errors > 0) {
|
|
190
|
+
console.error(`❌ ${testResults.errors} errors detected - check logs above`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log('✓ No errors detected');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (process.arch === 'ia32' || process.arch === 'x32') {
|
|
196
|
+
console.log('\n⚠️ Running on 32-bit architecture');
|
|
197
|
+
console.log(' If experiencing issues, check:');
|
|
198
|
+
console.log(' - Number overflow in timestamps');
|
|
199
|
+
console.log(' - Buffer size limits');
|
|
200
|
+
console.log(' - setTimeout precision');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log('\n=================================\n');
|
|
204
|
+
|
|
205
|
+
platform.destroy();
|
|
206
|
+
|
|
207
|
+
// Exit with appropriate code
|
|
208
|
+
if (testResults.errors > 0 || testResults.quotes === 0) {
|
|
209
|
+
console.error('❌ Tests FAILED');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
} else {
|
|
212
|
+
console.log('✓ All tests PASSED');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Graceful shutdown
|
|
218
|
+
process.on('SIGINT', () => {
|
|
219
|
+
console.log('\n\n⚠️ Interrupted by user');
|
|
220
|
+
printFinalReport();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
process.on('uncaughtException', (err) => {
|
|
224
|
+
console.error('\n💥 Uncaught exception:', err);
|
|
225
|
+
testResults.errors++;
|
|
226
|
+
platform.destroy();
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log('🚀 Starting universal compatibility test...\n');
|
package/index.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scaletrade-server-api (Fixed for x32/x64 compatibility)
|
|
3
|
+
* High-performance TCP client for ScaleTrade platform
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const events = require('events');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
const RECONNECT_DELAY_MS = 4000;
|
|
11
|
+
const RESPONSE_TIMEOUT_MS = 30000;
|
|
12
|
+
const AUTO_SUBSCRIBE_DELAY_MS = 500;
|
|
13
|
+
const SOCKET_KEEPALIVE = true;
|
|
14
|
+
const SOCKET_NODELAY = true;
|
|
15
|
+
const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB limit for x32
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a short unique ID for extID
|
|
19
|
+
* @returns {string} 12-character random ID
|
|
20
|
+
*/
|
|
21
|
+
function generateExtID() {
|
|
22
|
+
if (crypto.randomUUID) {
|
|
23
|
+
return crypto.randomUUID().replace(/-/g, '').substring(0, 12);
|
|
24
|
+
}
|
|
25
|
+
// Fallback - more reliable on x32
|
|
26
|
+
const timestamp = Date.now().toString(36);
|
|
27
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
28
|
+
return (timestamp + random).substring(0, 12);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Safe number parsing for x32 compatibility
|
|
33
|
+
* @param {*} value - Value to parse
|
|
34
|
+
* @returns {number|null}
|
|
35
|
+
*/
|
|
36
|
+
function safeParseNumber(value) {
|
|
37
|
+
if (typeof value === 'number') {
|
|
38
|
+
// Check if number is safe integer on x32
|
|
39
|
+
if (!Number.isSafeInteger(value) && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
40
|
+
console.warn(`Unsafe integer detected: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
return isNaN(parsed) ? null : parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Safe timestamp conversion for x32
|
|
50
|
+
* @param {number} unixTimestamp - Unix timestamp in seconds
|
|
51
|
+
* @returns {Date|null}
|
|
52
|
+
*/
|
|
53
|
+
function safeTimestamp(unixTimestamp) {
|
|
54
|
+
if (!unixTimestamp) return null;
|
|
55
|
+
try {
|
|
56
|
+
// Avoid overflow on x32 by checking range
|
|
57
|
+
const ms = safeParseNumber(unixTimestamp) * 1000;
|
|
58
|
+
if (ms > 8640000000000000) { // Max valid JS date
|
|
59
|
+
console.warn(`Timestamp out of range: ${unixTimestamp}`);
|
|
60
|
+
return new Date();
|
|
61
|
+
}
|
|
62
|
+
return new Date(ms);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('Timestamp conversion error:', e.message);
|
|
65
|
+
return new Date();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Simple JSON repair - removes common issues
|
|
71
|
+
* @param {string} str - Potentially malformed JSON string
|
|
72
|
+
* @returns {string} Cleaned JSON string
|
|
73
|
+
*/
|
|
74
|
+
function jsonRepair(str) {
|
|
75
|
+
return str
|
|
76
|
+
.replace(/[\n\r\t]/g, '') // Remove whitespace
|
|
77
|
+
.replace(/,\s*}/g, '}') // Remove trailing commas in objects
|
|
78
|
+
.replace(/,\s*]/g, ']') // Remove trailing commas in arrays
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class STPlatform {
|
|
83
|
+
constructor(url, name, options = {}, broker, ctx, token, emitter = null) {
|
|
84
|
+
this.name = name;
|
|
85
|
+
this.url = url;
|
|
86
|
+
this.errorCount = 0;
|
|
87
|
+
this.broker = broker || {};
|
|
88
|
+
this.ctx = ctx || {};
|
|
89
|
+
this.ignoreEvents = options.ignoreEvents || false;
|
|
90
|
+
this.prefix = options.prefix || 'nor';
|
|
91
|
+
this.mode = options.mode || 'live';
|
|
92
|
+
this.token = token;
|
|
93
|
+
this.emitter = emitter || new events.EventEmitter();
|
|
94
|
+
this.autoSubscribeChannels = Array.isArray(options.autoSubscribe) ? options.autoSubscribe : [];
|
|
95
|
+
this.seenNotifyTokens = new Set();
|
|
96
|
+
this.pendingRequests = new Map();
|
|
97
|
+
|
|
98
|
+
// x32 specific limits
|
|
99
|
+
this.maxBufferSize = MAX_BUFFER_SIZE;
|
|
100
|
+
this.arch = process.arch; // Store architecture info
|
|
101
|
+
|
|
102
|
+
this.createSocket();
|
|
103
|
+
|
|
104
|
+
// Return proxy for dynamic command calls
|
|
105
|
+
return new Proxy(this, {
|
|
106
|
+
get: (target, prop) => {
|
|
107
|
+
if (prop in target) return Reflect.get(target, prop);
|
|
108
|
+
return (data = {}) => target.callCommand(prop, data);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Establish TCP connection and set up event handlers
|
|
115
|
+
*/
|
|
116
|
+
createSocket() {
|
|
117
|
+
this.errorCount = 0;
|
|
118
|
+
this.connected = false;
|
|
119
|
+
this.alive = true;
|
|
120
|
+
this.recv = '';
|
|
121
|
+
this.seenNotifyTokens.clear();
|
|
122
|
+
|
|
123
|
+
this.socket = new net.Socket();
|
|
124
|
+
this.socket.setKeepAlive(SOCKET_KEEPALIVE);
|
|
125
|
+
this.socket.setNoDelay(SOCKET_NODELAY);
|
|
126
|
+
|
|
127
|
+
this.socket
|
|
128
|
+
.on('connect', () => {
|
|
129
|
+
console.info(`NT [${this.name}] Connected to ${this.url} (${this.arch})`);
|
|
130
|
+
this.connected = true;
|
|
131
|
+
this.errorCount = 0;
|
|
132
|
+
this.seenNotifyTokens.clear();
|
|
133
|
+
|
|
134
|
+
// Auto-subscribe after connection
|
|
135
|
+
if (this.autoSubscribeChannels.length > 0) {
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
this.subscribe(this.autoSubscribeChannels)
|
|
138
|
+
.then(() => console.info(`NT [${this.name}] Auto-subscribed: ${this.autoSubscribeChannels.join(', ')}`))
|
|
139
|
+
.catch(err => console.error(`NT [${this.name}] Auto-subscribe failed:`, err.message));
|
|
140
|
+
}, AUTO_SUBSCRIBE_DELAY_MS);
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.on('timeout', () => {
|
|
144
|
+
console.error(`NT [${this.name}] Socket timeout`);
|
|
145
|
+
if (this.alive) this.reconnect();
|
|
146
|
+
})
|
|
147
|
+
.on('close', () => {
|
|
148
|
+
this.connected = false;
|
|
149
|
+
console.warn(`NT [${this.name}] Connection closed`);
|
|
150
|
+
if (this.alive) this.reconnect();
|
|
151
|
+
})
|
|
152
|
+
.on('error', (err) => {
|
|
153
|
+
this.errorCount++;
|
|
154
|
+
console.error(`NT [${this.name}] Socket error (count: ${this.errorCount}):`, err.message);
|
|
155
|
+
|
|
156
|
+
// Don't reconnect too aggressively on repeated errors
|
|
157
|
+
if (this.errorCount < 10 && this.alive) {
|
|
158
|
+
this.reconnect();
|
|
159
|
+
} else if (this.errorCount >= 10) {
|
|
160
|
+
console.error(`NT [${this.name}] Too many errors, stopping reconnection attempts`);
|
|
161
|
+
this.alive = false;
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.on('data', (data) => this.handleData(data));
|
|
165
|
+
|
|
166
|
+
const [host, port] = this.url.split(':');
|
|
167
|
+
this.socket.connect({ host, port: parseInt(port) });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle incoming TCP data
|
|
172
|
+
* @param {Buffer} data - Raw TCP chunk
|
|
173
|
+
*/
|
|
174
|
+
handleData(data) {
|
|
175
|
+
try {
|
|
176
|
+
// Convert buffer to string safely
|
|
177
|
+
const chunk = data.toString('utf8');
|
|
178
|
+
|
|
179
|
+
// Check buffer size limit (important for x32)
|
|
180
|
+
if (this.recv.length + chunk.length > this.maxBufferSize) {
|
|
181
|
+
console.error(`NT [${this.name}] Buffer overflow protection triggered`);
|
|
182
|
+
this.recv = ''; // Reset buffer
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.recv += chunk;
|
|
187
|
+
|
|
188
|
+
const delimiterPos = this.recv.lastIndexOf('\r\n');
|
|
189
|
+
if (delimiterPos === -1) return;
|
|
190
|
+
|
|
191
|
+
const received = this.recv.slice(0, delimiterPos);
|
|
192
|
+
this.recv = this.recv.slice(delimiterPos + 2);
|
|
193
|
+
const tokens = received.split('\r\n');
|
|
194
|
+
|
|
195
|
+
for (const token of tokens) {
|
|
196
|
+
if (!token.trim()) continue;
|
|
197
|
+
this.processMessage(token);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(`NT [${this.name}] handleData error:`, err.message);
|
|
201
|
+
this.recv = ''; // Reset on error
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
processMessage(token) {
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
const cleaned = jsonRepair(token);
|
|
209
|
+
parsed = JSON.parse(cleaned);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error(`NT [${this.name}] Parse error:`, token.substring(0, 100), e.message);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// === ARRAY MESSAGES ===
|
|
216
|
+
if (Array.isArray(parsed)) {
|
|
217
|
+
const [marker] = parsed;
|
|
218
|
+
|
|
219
|
+
// Quote: ["t", symbol, bid, ask, timestamp]
|
|
220
|
+
if (marker === 't' && parsed.length >= 4) {
|
|
221
|
+
const [, symbol, bid, ask, timestamp] = parsed;
|
|
222
|
+
if (typeof symbol === 'string' && typeof bid === 'number' && typeof ask === 'number') {
|
|
223
|
+
const quote = {
|
|
224
|
+
symbol,
|
|
225
|
+
bid: safeParseNumber(bid),
|
|
226
|
+
ask: safeParseNumber(ask),
|
|
227
|
+
timestamp: safeTimestamp(timestamp)
|
|
228
|
+
};
|
|
229
|
+
this.emit('quote', quote);
|
|
230
|
+
this.emit(`quote:${symbol.toUpperCase()}`, quote);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Notify: ["n", msg, desc, token, status, level, user_id, time, data?, code]
|
|
236
|
+
if (marker === 'n' && parsed.length >= 8) {
|
|
237
|
+
const [
|
|
238
|
+
, message, description, token, status, level, user_id, create_time, dataOrCode, code
|
|
239
|
+
] = parsed;
|
|
240
|
+
|
|
241
|
+
if (this.seenNotifyTokens.has(token)) return;
|
|
242
|
+
this.seenNotifyTokens.add(token);
|
|
243
|
+
|
|
244
|
+
// Limit set size
|
|
245
|
+
if (this.seenNotifyTokens.size > 10000) {
|
|
246
|
+
const firstToken = this.seenNotifyTokens.values().next().value;
|
|
247
|
+
this.seenNotifyTokens.delete(firstToken);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isObject = dataOrCode && typeof dataOrCode === 'object';
|
|
251
|
+
const notify = {
|
|
252
|
+
message, description, token, status, level, user_id,
|
|
253
|
+
create_time: safeTimestamp(create_time),
|
|
254
|
+
data: isObject ? dataOrCode : {},
|
|
255
|
+
code: Number(isObject ? code : dataOrCode) || 0
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
this.emit('notify', notify);
|
|
259
|
+
this.emit(`notify:${level}`, notify);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Symbols Reindex
|
|
264
|
+
if (marker === 'sr' && parsed.length === 2) {
|
|
265
|
+
this.emit('symbols:reindex', parsed[1]);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Security Reindex
|
|
270
|
+
if (marker === 'sc' && parsed.length === 2) {
|
|
271
|
+
this.emit('security:reindex', parsed[1]);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.warn(`NT [${this.name}] Unknown array message:`, parsed);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// === JSON EVENT OBJECTS ===
|
|
280
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.event) {
|
|
281
|
+
const { event, type, data } = parsed;
|
|
282
|
+
this.emit(event, { type, data });
|
|
283
|
+
|
|
284
|
+
if (data?.login) this.emit(`${event}:${data.login}`, { type, data });
|
|
285
|
+
if (data?.symbol) this.emit(`${event}:${data.symbol}`, { type, data });
|
|
286
|
+
if (data?.group) this.emit(`${event}:${data.group}`, { type, data });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// === COMMAND RESPONSES (extID) ===
|
|
291
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.extID) {
|
|
292
|
+
const extID = parsed.extID;
|
|
293
|
+
|
|
294
|
+
if (this.pendingRequests.has(extID)) {
|
|
295
|
+
const { resolve, timeout } = this.pendingRequests.get(extID);
|
|
296
|
+
clearTimeout(timeout);
|
|
297
|
+
this.pendingRequests.delete(extID);
|
|
298
|
+
|
|
299
|
+
// Use setImmediate for better x32 compatibility
|
|
300
|
+
setImmediate(() => resolve(parsed));
|
|
301
|
+
} else {
|
|
302
|
+
this.emit(extID, parsed);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.warn(`NT [${this.name}] Unknown message:`, parsed);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Emit event if not ignored
|
|
312
|
+
* @param {string} name - Event name
|
|
313
|
+
* @param {*} data - Event data
|
|
314
|
+
*/
|
|
315
|
+
emit(name, data) {
|
|
316
|
+
if (!this.ignoreEvents) {
|
|
317
|
+
// Use setImmediate to avoid blocking on x32
|
|
318
|
+
setImmediate(() => this.emitter.emit(name, data));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Send command via proxy (e.g., platform.AddUser())
|
|
324
|
+
* @param {string} command - Command name
|
|
325
|
+
* @param {Object} data - Command payload
|
|
326
|
+
* @returns {Promise<Object>}
|
|
327
|
+
*/
|
|
328
|
+
async callCommand(command, data = {}) {
|
|
329
|
+
const payload = { command, data };
|
|
330
|
+
if (!payload.extID) payload.extID = generateExtID();
|
|
331
|
+
return this.send(payload);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Low-level send (improved with promise-based response handling)
|
|
336
|
+
* @param {Object} payload - { command, data, extID?, __token }
|
|
337
|
+
* @returns {Promise<Object>}
|
|
338
|
+
*/
|
|
339
|
+
async send(payload) {
|
|
340
|
+
if (!payload.extID) payload.extID = generateExtID();
|
|
341
|
+
payload.__token = this.token;
|
|
342
|
+
|
|
343
|
+
if (!this.connected) {
|
|
344
|
+
return Promise.reject(new Error(`NT [${this.name}] Not connected`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
// Use Math.min to ensure timeout doesn't overflow on x32
|
|
349
|
+
const timeoutMs = Math.min(RESPONSE_TIMEOUT_MS, 2147483647);
|
|
350
|
+
|
|
351
|
+
const timeout = setTimeout(() => {
|
|
352
|
+
this.pendingRequests.delete(payload.extID);
|
|
353
|
+
reject(new Error(`NT [${this.name}] Timeout for extID: ${payload.extID}`));
|
|
354
|
+
}, timeoutMs);
|
|
355
|
+
|
|
356
|
+
this.pendingRequests.set(payload.extID, { resolve, reject, timeout });
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const message = JSON.stringify(payload) + "\r\n";
|
|
360
|
+
|
|
361
|
+
// Check message size
|
|
362
|
+
if (Buffer.byteLength(message, 'utf8') > 65536) {
|
|
363
|
+
clearTimeout(timeout);
|
|
364
|
+
this.pendingRequests.delete(payload.extID);
|
|
365
|
+
reject(new Error(`NT [${this.name}] Message too large`));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.socket.write(message, 'utf8', (err) => {
|
|
370
|
+
if (err) {
|
|
371
|
+
clearTimeout(timeout);
|
|
372
|
+
this.pendingRequests.delete(payload.extID);
|
|
373
|
+
reject(err);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
} catch (err) {
|
|
377
|
+
clearTimeout(timeout);
|
|
378
|
+
this.pendingRequests.delete(payload.extID);
|
|
379
|
+
reject(err);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Subscribe to market data channels (optimized for speed)
|
|
386
|
+
* @param {string|Array<string>} channels - Symbol(s) or channel(s)
|
|
387
|
+
* @returns {Promise<Object>}
|
|
388
|
+
*/
|
|
389
|
+
async subscribe(channels) {
|
|
390
|
+
const chanels = Array.isArray(channels) ? channels : [channels];
|
|
391
|
+
return this.callCommand('Subscribe', { chanels });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Unsubscribe from channels
|
|
396
|
+
* @param {string|Array<string>} channels - Symbol(s) to unsubscribe
|
|
397
|
+
* @returns {Promise<Object>}
|
|
398
|
+
*/
|
|
399
|
+
async unsubscribe(channels) {
|
|
400
|
+
const chanels = Array.isArray(channels) ? channels : [channels];
|
|
401
|
+
return this.callCommand('Unsubscribe', { chanels });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Reconnect logic with backoff
|
|
406
|
+
*/
|
|
407
|
+
reconnect() {
|
|
408
|
+
if (!this.alive || this._reconnectTimer) return;
|
|
409
|
+
|
|
410
|
+
this.socket.destroy();
|
|
411
|
+
this.seenNotifyTokens.clear();
|
|
412
|
+
|
|
413
|
+
// Clear pending requests with error
|
|
414
|
+
for (const [extID, { reject, timeout }] of this.pendingRequests.entries()) {
|
|
415
|
+
clearTimeout(timeout);
|
|
416
|
+
reject(new Error(`NT [${this.name}] Connection lost`));
|
|
417
|
+
}
|
|
418
|
+
this.pendingRequests.clear();
|
|
419
|
+
|
|
420
|
+
// Exponential backoff with safe max delay for x32
|
|
421
|
+
const baseDelay = RECONNECT_DELAY_MS * Math.pow(1.5, this.errorCount - 1);
|
|
422
|
+
const delay = Math.min(baseDelay, 30000);
|
|
423
|
+
|
|
424
|
+
this._reconnectTimer = setTimeout(() => {
|
|
425
|
+
delete this._reconnectTimer;
|
|
426
|
+
console.info(`NT [${this.name}] Reconnecting... (attempt ${this.errorCount + 1})`);
|
|
427
|
+
this.createSocket();
|
|
428
|
+
}, delay);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Gracefully close connection
|
|
433
|
+
*/
|
|
434
|
+
destroy() {
|
|
435
|
+
this.alive = false;
|
|
436
|
+
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
437
|
+
this.seenNotifyTokens.clear();
|
|
438
|
+
|
|
439
|
+
// Clear all pending requests
|
|
440
|
+
for (const [extID, { reject, timeout }] of this.pendingRequests.entries()) {
|
|
441
|
+
clearTimeout(timeout);
|
|
442
|
+
reject(new Error(`NT [${this.name}] Platform destroyed`));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.pendingRequests.clear();
|
|
446
|
+
|
|
447
|
+
this.socket.destroy();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get connection status
|
|
452
|
+
* @returns {boolean}
|
|
453
|
+
*/
|
|
454
|
+
isConnected() {
|
|
455
|
+
return this.connected;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
module.exports = STPlatform;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scaletrade-server-api",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "High-performance TCP client for ScaleTrade — ultra-low latency server-to-server integration with real-time quotes, trade events, balance updates, and full symbol/user management.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"example": "node example/example.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/scaletrade/server-api-js.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ScaleTrade",
|
|
16
|
+
"trading",
|
|
17
|
+
"tcp",
|
|
18
|
+
"api",
|
|
19
|
+
"broker",
|
|
20
|
+
"forex",
|
|
21
|
+
"crypto",
|
|
22
|
+
"real-time",
|
|
23
|
+
"quotes",
|
|
24
|
+
"market-data",
|
|
25
|
+
"trade-events",
|
|
26
|
+
"balance",
|
|
27
|
+
"margin",
|
|
28
|
+
"server-to-server",
|
|
29
|
+
"low-latency",
|
|
30
|
+
"fintech",
|
|
31
|
+
"brokerage",
|
|
32
|
+
"nodejs",
|
|
33
|
+
"realtime",
|
|
34
|
+
"high-frequency-trading",
|
|
35
|
+
"hft"
|
|
36
|
+
],
|
|
37
|
+
"categories": [
|
|
38
|
+
"Finance",
|
|
39
|
+
"Trading",
|
|
40
|
+
"API"
|
|
41
|
+
],
|
|
42
|
+
"author": {
|
|
43
|
+
"name": "MarSer",
|
|
44
|
+
"url": "https://scaletrade.com/"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/scaletrade/server-api-js/issues"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=14.17.0"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/scaletrade/server-api-js#readme"
|
|
54
|
+
}
|