@icgio/icg-exchanges-wrapper 1.14.253 → 1.14.255
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 +129 -1
- package/index.js +5 -0
- package/lib/ws-data-client.js +248 -0
- package/middleware/account.js +3 -3
- package/middleware/orders.js +41 -17
- package/package.json +1 -1
- package/lib/order-status-store.js +0 -255
package/README.md
CHANGED
|
@@ -1 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ICG Exchanges Wrapper
|
|
2
|
+
|
|
3
|
+
High-level wrapper functions for the ICG exchanges package, providing simplified interfaces for common trading operations across multiple exchanges.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Exchange Operations**: Execute operations across multiple exchanges in parallel
|
|
8
|
+
- **Unified Data Format**: Consistent response format regardless of exchange
|
|
9
|
+
- **Order Management Middleware**: Advanced order placement with retries and validation
|
|
10
|
+
- **WebSocket Data Client**: Real-time log streaming support
|
|
11
|
+
- **Rate Data Aggregation**: Combine orderbook data from multiple sources
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @icgio/icg-exchanges-wrapper
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Functions Module
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const { functions } = require('@icgio/icg-exchanges-wrapper')
|
|
25
|
+
|
|
26
|
+
// Get rates (orderbook) from multiple exchanges
|
|
27
|
+
const rates = await functions.get_exchanges_rates(exchanges, pairs)
|
|
28
|
+
|
|
29
|
+
// Get balances from multiple exchanges
|
|
30
|
+
const balances = await functions.get_exchanges_balance(exchanges)
|
|
31
|
+
|
|
32
|
+
// Get trade history
|
|
33
|
+
const trades = await functions.get_exchanges_trades(exchanges, pairs)
|
|
34
|
+
|
|
35
|
+
// Get open orders
|
|
36
|
+
const open_orders = await functions.get_exchanges_open_orders(exchanges, pairs)
|
|
37
|
+
|
|
38
|
+
// Get deposit history
|
|
39
|
+
const deposits = await functions.get_exchanges_deposits(exchanges)
|
|
40
|
+
|
|
41
|
+
// Get withdrawal history
|
|
42
|
+
const withdrawals = await functions.get_exchanges_withdrawals(exchanges)
|
|
43
|
+
|
|
44
|
+
// Get OHLCV data
|
|
45
|
+
const ohlcv = await functions.get_exchanges_ohlcv(exchanges, pairs, interval)
|
|
46
|
+
|
|
47
|
+
// Get market history
|
|
48
|
+
const market_history = await functions.get_exchanges_market_history(exchanges, pairs)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Orders Middleware
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
const Orders = require('@icgio/icg-exchanges-wrapper').orders
|
|
55
|
+
|
|
56
|
+
// Create an order manager instance
|
|
57
|
+
const orders = new Orders(exchanges_config)
|
|
58
|
+
|
|
59
|
+
// Place orders with advanced features
|
|
60
|
+
await orders.place_order(exchange, pair, side, amount, price, options)
|
|
61
|
+
|
|
62
|
+
// Cancel orders
|
|
63
|
+
await orders.cancel_order(exchange, pair, order_id)
|
|
64
|
+
|
|
65
|
+
// Get order status
|
|
66
|
+
const status = await orders.get_order_status(exchange, pair, order_id)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### WebSocket Data Client
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const { ws_data_client, send_log } = require('@icgio/icg-exchanges-wrapper')
|
|
73
|
+
|
|
74
|
+
// Send log messages via WebSocket
|
|
75
|
+
send_log('info', 'Operation completed successfully')
|
|
76
|
+
send_log('error', 'An error occurred')
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
icg-exchanges-wrapper/
|
|
83
|
+
├── index.js # Main entry point
|
|
84
|
+
├── functions/
|
|
85
|
+
│ ├── get-exchanges-balance.js # Balance fetching
|
|
86
|
+
│ ├── get-exchanges-deposits.js # Deposit history
|
|
87
|
+
│ ├── get-exchanges-open-orders.js # Open orders
|
|
88
|
+
│ ├── get-exchanges-rates-market-history-ohlcv.js # Market data
|
|
89
|
+
│ ├── get-exchanges-trades.js # Trade history
|
|
90
|
+
│ └── get-exchanges-withdrawals.js # Withdrawal history
|
|
91
|
+
├── middleware/
|
|
92
|
+
│ ├── account.js # Account middleware
|
|
93
|
+
│ └── orders.js # Order management middleware
|
|
94
|
+
├── lib/
|
|
95
|
+
│ └── ws-data-client.js # WebSocket data client
|
|
96
|
+
└── package.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Available Functions
|
|
100
|
+
|
|
101
|
+
| Function | Description |
|
|
102
|
+
|----------|-------------|
|
|
103
|
+
| `get_exchanges_rates` | Fetch orderbook/rates from exchanges |
|
|
104
|
+
| `get_exchanges_market_history` | Fetch recent trade history |
|
|
105
|
+
| `get_exchanges_ohlcv` | Fetch OHLCV candlestick data |
|
|
106
|
+
| `get_exchanges_balance` | Fetch account balances |
|
|
107
|
+
| `get_exchanges_trades` | Fetch user trade history |
|
|
108
|
+
| `get_exchanges_open_orders` | Fetch open orders |
|
|
109
|
+
| `get_exchanges_deposits` | Fetch deposit history |
|
|
110
|
+
| `get_exchanges_withdrawals` | Fetch withdrawal history |
|
|
111
|
+
|
|
112
|
+
## Dependencies
|
|
113
|
+
|
|
114
|
+
| Package | Purpose |
|
|
115
|
+
|---------|---------|
|
|
116
|
+
| `@icgio/icg-exchanges` | Exchange implementations |
|
|
117
|
+
| `@icgio/icg-utils` | Utility functions |
|
|
118
|
+
| `lodash` | Utility library |
|
|
119
|
+
| `pg` | PostgreSQL client |
|
|
120
|
+
|
|
121
|
+
## Code Style
|
|
122
|
+
|
|
123
|
+
- CommonJS modules (`require()` / `module.exports`)
|
|
124
|
+
- Variable naming: `snake_case`
|
|
125
|
+
- Function naming: `snake_case`
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
ISC License
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const get_exchanges_trades = require('./functions/get-exchanges-trades.js')
|
|
|
4
4
|
const get_exchanges_open_orders = require('./functions/get-exchanges-open-orders.js')
|
|
5
5
|
const get_exchanges_deposits = require('./functions/get-exchanges-deposits.js')
|
|
6
6
|
const get_exchanges_withdrawals = require('./functions/get-exchanges-withdrawals.js')
|
|
7
|
+
const { ws_data_client } = require('./lib/ws-data-client.js')
|
|
7
8
|
|
|
8
9
|
module.exports.functions = {
|
|
9
10
|
get_exchanges_rates,
|
|
@@ -17,3 +18,7 @@ module.exports.functions = {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
module.exports.orders = require('./middleware/orders.js')
|
|
21
|
+
|
|
22
|
+
// WebSocket data client for log streaming
|
|
23
|
+
module.exports.ws_data_client = ws_data_client
|
|
24
|
+
module.exports.send_log = (level, message) => ws_data_client.send_log(level, message)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const WebSocket = require('ws')
|
|
2
|
+
const { randomUUID: uuid } = require('crypto')
|
|
3
|
+
|
|
4
|
+
// Hardcoded backend URL for production
|
|
5
|
+
const WS_BACKEND_URL = 'wss://api-prod.icg.io/ws/data/ingest'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WebSocket client for streaming logs and order status to dashboard backend.
|
|
9
|
+
* Singleton instance shared across the application.
|
|
10
|
+
*/
|
|
11
|
+
class WSDataClient {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.backend_url = WS_BACKEND_URL
|
|
14
|
+
this.secret = null
|
|
15
|
+
this.client_id = null
|
|
16
|
+
this.ws = null
|
|
17
|
+
this.queue = []
|
|
18
|
+
this.max_queue = 100
|
|
19
|
+
this.reconnect_interval = 5000
|
|
20
|
+
this.enabled = false
|
|
21
|
+
this.connected = false
|
|
22
|
+
this.reconnect_timer = null
|
|
23
|
+
this.sync_interval = null
|
|
24
|
+
this.sync_interval_ms = 30 * 1000 // 30 seconds
|
|
25
|
+
this.order_info_ref = null // Reference to Orders.order_info for periodic sync
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the WS client with connection details.
|
|
30
|
+
* @param {string} secret - Shared secret for authentication
|
|
31
|
+
* @param {string} client_id - Client identifier (e.g., "FREEZONE")
|
|
32
|
+
*/
|
|
33
|
+
init(secret, client_id) {
|
|
34
|
+
if (!secret || !client_id) {
|
|
35
|
+
console.log('[WSDataClient] Missing config (secret or client_id), disabled')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.secret = secret
|
|
40
|
+
this.client_id = client_id
|
|
41
|
+
this.enabled = true
|
|
42
|
+
|
|
43
|
+
console.log('[WSDataClient] Initializing for client:', client_id, 'URL:', this.backend_url)
|
|
44
|
+
this.connect()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set reference to order_info for periodic sync.
|
|
49
|
+
* @param {object} order_info - Reference to Orders.order_info object
|
|
50
|
+
*/
|
|
51
|
+
set_order_info_ref(order_info) {
|
|
52
|
+
this.order_info_ref = order_info
|
|
53
|
+
|
|
54
|
+
// Start periodic sync if not already started
|
|
55
|
+
if (!this.sync_interval && this.enabled) {
|
|
56
|
+
this.start_periodic_sync()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Start periodic full sync of order_info (every 30 seconds).
|
|
62
|
+
*/
|
|
63
|
+
start_periodic_sync() {
|
|
64
|
+
if (this.sync_interval) return
|
|
65
|
+
|
|
66
|
+
// Send initial reset on startup
|
|
67
|
+
this.send_reset('orders')
|
|
68
|
+
|
|
69
|
+
this.sync_interval = setInterval(() => {
|
|
70
|
+
if (this.order_info_ref && this.connected) {
|
|
71
|
+
this.send_order_sync(this.order_info_ref)
|
|
72
|
+
}
|
|
73
|
+
}, this.sync_interval_ms)
|
|
74
|
+
|
|
75
|
+
console.log('[WSDataClient] Periodic sync started (every %ds)', this.sync_interval_ms / 1000)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Connect to the dashboard backend WebSocket.
|
|
80
|
+
*/
|
|
81
|
+
connect() {
|
|
82
|
+
if (!this.enabled) return
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
this.ws = new WebSocket(this.backend_url, {
|
|
86
|
+
headers: {
|
|
87
|
+
'x-client-secret': this.secret,
|
|
88
|
+
'x-client-id': this.client_id,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
this.ws.on('open', () => {
|
|
93
|
+
console.log('[WSDataClient] Connected to backend')
|
|
94
|
+
this.connected = true
|
|
95
|
+
this.flush_queue()
|
|
96
|
+
|
|
97
|
+
// Send reset on reconnect to clear stale state
|
|
98
|
+
this.send_reset('orders')
|
|
99
|
+
|
|
100
|
+
// Send full sync immediately after connect
|
|
101
|
+
if (this.order_info_ref) {
|
|
102
|
+
this.send_order_sync(this.order_info_ref)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
this.ws.on('close', (code, reason) => {
|
|
107
|
+
console.log('[WSDataClient] Disconnected:', code, reason?.toString())
|
|
108
|
+
this.connected = false
|
|
109
|
+
this.schedule_reconnect()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
this.ws.on('error', (err) => {
|
|
113
|
+
console.error('[WSDataClient] Error:', err.message)
|
|
114
|
+
this.connected = false
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
this.ws.on('message', (data) => {
|
|
118
|
+
// Handle any messages from backend (e.g., acknowledgments)
|
|
119
|
+
try {
|
|
120
|
+
const msg = JSON.parse(data.toString())
|
|
121
|
+
if (msg.type === 'error') {
|
|
122
|
+
console.error('[WSDataClient] Backend error:', msg.message)
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Ignore parse errors
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[WSDataClient] Connection error:', err.message)
|
|
130
|
+
this.schedule_reconnect()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Schedule reconnection attempt.
|
|
136
|
+
*/
|
|
137
|
+
schedule_reconnect() {
|
|
138
|
+
if (this.reconnect_timer) return
|
|
139
|
+
|
|
140
|
+
this.reconnect_timer = setTimeout(() => {
|
|
141
|
+
this.reconnect_timer = null
|
|
142
|
+
console.log('[WSDataClient] Attempting reconnect...')
|
|
143
|
+
this.connect()
|
|
144
|
+
}, this.reconnect_interval)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Send a message to the backend.
|
|
149
|
+
* @param {string} type - Message type
|
|
150
|
+
* @param {object} data - Message data
|
|
151
|
+
*/
|
|
152
|
+
send(type, data) {
|
|
153
|
+
if (!this.enabled) return
|
|
154
|
+
|
|
155
|
+
const msg = JSON.stringify({
|
|
156
|
+
type,
|
|
157
|
+
client: this.client_id,
|
|
158
|
+
ts: Date.now(),
|
|
159
|
+
...data,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
163
|
+
this.ws.send(msg)
|
|
164
|
+
} else {
|
|
165
|
+
// Queue message for later
|
|
166
|
+
this.queue.push(msg)
|
|
167
|
+
if (this.queue.length > this.max_queue) {
|
|
168
|
+
this.queue.shift() // Drop oldest
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Flush queued messages after reconnection.
|
|
175
|
+
*/
|
|
176
|
+
flush_queue() {
|
|
177
|
+
while (this.queue.length > 0 && this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
178
|
+
const msg = this.queue.shift()
|
|
179
|
+
this.ws.send(msg)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Send reset event to clear state on backend.
|
|
185
|
+
* @param {string} scope - What to reset ('orders', 'logs', 'all')
|
|
186
|
+
*/
|
|
187
|
+
send_reset(scope = 'orders') {
|
|
188
|
+
this.send('reset', { scope })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Send full order_info sync to backend.
|
|
193
|
+
* @param {object} order_info - Complete order_info object
|
|
194
|
+
*/
|
|
195
|
+
send_order_sync(order_info) {
|
|
196
|
+
this.send('order_sync', { data: order_info })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Send order update event to backend.
|
|
201
|
+
* @param {object} event - Order event data
|
|
202
|
+
*/
|
|
203
|
+
send_order(event) {
|
|
204
|
+
this.send('order', event)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Send log entry to backend.
|
|
209
|
+
* @param {string} level - Log level ('info', 'warn', 'error')
|
|
210
|
+
* @param {string} message - Log message
|
|
211
|
+
*/
|
|
212
|
+
send_log(level, message) {
|
|
213
|
+
this.send('log', {
|
|
214
|
+
id: uuid(),
|
|
215
|
+
level,
|
|
216
|
+
msg: message,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Close the WebSocket connection.
|
|
222
|
+
*/
|
|
223
|
+
close() {
|
|
224
|
+
this.enabled = false
|
|
225
|
+
|
|
226
|
+
if (this.sync_interval) {
|
|
227
|
+
clearInterval(this.sync_interval)
|
|
228
|
+
this.sync_interval = null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.reconnect_timer) {
|
|
232
|
+
clearTimeout(this.reconnect_timer)
|
|
233
|
+
this.reconnect_timer = null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (this.ws) {
|
|
237
|
+
this.ws.close()
|
|
238
|
+
this.ws = null
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.connected = false
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Singleton instance
|
|
246
|
+
const ws_data_client = new WSDataClient()
|
|
247
|
+
|
|
248
|
+
module.exports = { WSDataClient, ws_data_client }
|
package/middleware/account.js
CHANGED
|
@@ -130,11 +130,11 @@ module.exports = class Account {
|
|
|
130
130
|
if (_.includes(constants.stablecoins, cur)) {
|
|
131
131
|
profit.USD += position
|
|
132
132
|
} else if (_.includes(['KRW'], cur)) {
|
|
133
|
-
profit.USD += position
|
|
133
|
+
profit.USD += position * this.forex_rates['KRWUSD']
|
|
134
134
|
} else if (_.includes(['HKD'], cur)) {
|
|
135
|
-
profit.USD += position
|
|
135
|
+
profit.USD += position * this.forex_rates['HKDUSD']
|
|
136
136
|
} else if (_.includes(['CNYT'], cur)) {
|
|
137
|
-
profit.USD += position
|
|
137
|
+
profit.USD += position * this.forex_rates['CNYUSD']
|
|
138
138
|
} else {
|
|
139
139
|
if (!cmc_rates[cur]) {
|
|
140
140
|
console.log('get_exchange_profit rates_cmc %s does not exist', cur)
|
package/middleware/orders.js
CHANGED
|
@@ -7,7 +7,7 @@ const utils = require('@icgio/icg-utils').utils
|
|
|
7
7
|
const { coinmarketcap: get_coinmarketcap_rates, observable: Observable } = require('@icgio/icg-utils')
|
|
8
8
|
|
|
9
9
|
const Account = require('./account.js')
|
|
10
|
-
const
|
|
10
|
+
const { ws_data_client } = require('../lib/ws-data-client.js')
|
|
11
11
|
|
|
12
12
|
// Default precision functions - will be overridden if provided in settings
|
|
13
13
|
let round_price = (exchange, pair, price) => price
|
|
@@ -199,7 +199,6 @@ module.exports = class Orders {
|
|
|
199
199
|
market_history_check: true,
|
|
200
200
|
ohlcv_check: false,
|
|
201
201
|
cmc_rates_check: true,
|
|
202
|
-
all_orders_store: false,
|
|
203
202
|
}
|
|
204
203
|
if (this.function_enabled.balances_check) {
|
|
205
204
|
this.check_balances()
|
|
@@ -244,9 +243,10 @@ module.exports = class Orders {
|
|
|
244
243
|
this.process_query_order(this.query_order)
|
|
245
244
|
}, PROCESS_OPEN_ORDERS_TRADERS_INTERVAL_MS)
|
|
246
245
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
// Initialize WS data client for streaming to dashboard backend
|
|
247
|
+
if (settings.ws_backend_secret && settings.client_id) {
|
|
248
|
+
ws_data_client.init(settings.ws_backend_secret, settings.client_id)
|
|
249
|
+
ws_data_client.set_order_info_ref(this.order_info)
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
init_account(balances, benchmarks) {
|
|
@@ -781,6 +781,17 @@ module.exports = class Orders {
|
|
|
781
781
|
}
|
|
782
782
|
_.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.${key}`, dict[key], Object)
|
|
783
783
|
}
|
|
784
|
+
|
|
785
|
+
// Add trace entry for order history
|
|
786
|
+
const trace_entry = {
|
|
787
|
+
ts: Date.now(),
|
|
788
|
+
status: dict.status || _.get(this.order_info, [exchange, pair, type, order_id, 'status']),
|
|
789
|
+
note,
|
|
790
|
+
f_amount: dict.f_amount,
|
|
791
|
+
}
|
|
792
|
+
const existing_trace = _.get(this.order_info, [exchange, pair, type, order_id, 'trace'], [])
|
|
793
|
+
_.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.trace`, [...existing_trace, trace_entry], Object)
|
|
794
|
+
|
|
784
795
|
console.log('UPDATE %s: %s %s %s %s %j', note, exchange, pair, type, order_id, dict)
|
|
785
796
|
// start: log every f-filled trade for future analysis
|
|
786
797
|
if (dict.status === 'F-FILLED') {
|
|
@@ -789,21 +800,38 @@ module.exports = class Orders {
|
|
|
789
800
|
console.log(`f_filled_orders_info:${exchange},${pair},${type},${order_id},${price},${amount},${f_amount},${t_amount},${fees},${close_time},${note}`)
|
|
790
801
|
}
|
|
791
802
|
// end: log every f-filled trade for future analysis
|
|
792
|
-
|
|
803
|
+
|
|
804
|
+
// Send order update via WebSocket to dashboard backend
|
|
793
805
|
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
794
|
-
|
|
795
|
-
|
|
806
|
+
// Only send N-FILLED and later statuses (skip INIT which has temporary order_id)
|
|
807
|
+
if (dict.status !== 'INIT' && order_data.status !== 'INIT') {
|
|
808
|
+
ws_data_client.send_order({
|
|
809
|
+
action: 'update',
|
|
810
|
+
exchange,
|
|
811
|
+
pair,
|
|
812
|
+
side: type,
|
|
813
|
+
order_id,
|
|
814
|
+
data: order_data,
|
|
815
|
+
note,
|
|
816
|
+
})
|
|
796
817
|
}
|
|
797
818
|
}
|
|
798
819
|
delete_order_info(exchange, pair, type, order_id, note) {
|
|
799
820
|
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
800
821
|
this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
|
|
801
822
|
console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
|
|
802
|
-
|
|
803
|
-
//
|
|
804
|
-
if (order_data.status
|
|
805
|
-
|
|
806
|
-
|
|
823
|
+
|
|
824
|
+
// Send delete event via WebSocket (only for non-INIT orders)
|
|
825
|
+
if (order_data.status !== 'INIT') {
|
|
826
|
+
ws_data_client.send_order({
|
|
827
|
+
action: 'delete',
|
|
828
|
+
exchange,
|
|
829
|
+
pair,
|
|
830
|
+
side: type,
|
|
831
|
+
order_id,
|
|
832
|
+
data: order_data,
|
|
833
|
+
note,
|
|
834
|
+
})
|
|
807
835
|
}
|
|
808
836
|
}
|
|
809
837
|
check_balances() {
|
|
@@ -925,10 +953,6 @@ module.exports = class Orders {
|
|
|
925
953
|
}
|
|
926
954
|
// cancel previous orders
|
|
927
955
|
else if (this.cancel_previous_orders && !this.previous_orders_canceled) {
|
|
928
|
-
// remove all previous orders for this exchange/pair from all_orders (fire-and-forget)
|
|
929
|
-
if (this.function_enabled.all_orders_store) {
|
|
930
|
-
this.all_orders_store.clear_orders(exchange, pair).catch((err) => console.error('clear_all_orders error', exchange, pair, err.message))
|
|
931
|
-
}
|
|
932
956
|
if (this.cancel_previous_orders_with_delay) {
|
|
933
957
|
setTimeout(() => {
|
|
934
958
|
orders.map((order) => {
|
package/package.json
CHANGED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
const { Pool } = require('pg')
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Async PostgreSQL store for all orders
|
|
5
|
-
* Non-blocking: all writes are fire-and-forget to avoid blocking the main process
|
|
6
|
-
*/
|
|
7
|
-
class AllOrdersStore {
|
|
8
|
-
constructor(connection_string) {
|
|
9
|
-
this.enabled = true
|
|
10
|
-
this.pool = null
|
|
11
|
-
this.table_ready = false
|
|
12
|
-
this.queue = []
|
|
13
|
-
this.max_queue_size = 2000
|
|
14
|
-
this.flush_interval = null
|
|
15
|
-
this.flush_interval_ms = 5000
|
|
16
|
-
|
|
17
|
-
if (connection_string) {
|
|
18
|
-
this.init(connection_string)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async init(connection_string) {
|
|
23
|
-
try {
|
|
24
|
-
this.pool = new Pool({
|
|
25
|
-
connectionString: connection_string,
|
|
26
|
-
max: 5,
|
|
27
|
-
idleTimeoutMillis: 30000,
|
|
28
|
-
connectionTimeoutMillis: 5000,
|
|
29
|
-
ssl: { rejectUnauthorized: false },
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
this.pool.on('error', (err) => {
|
|
33
|
-
console.error('AllOrdersStore pool error:', err.message)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// Set timezone to UTC for all connections
|
|
37
|
-
this.pool.on('connect', async (client) => {
|
|
38
|
-
await client.query("SET timezone = 'UTC'")
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
await this.ensure_table()
|
|
42
|
-
this.enabled = true
|
|
43
|
-
this.start_flush_interval()
|
|
44
|
-
console.log('AllOrdersStore initialized successfully')
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error('AllOrdersStore init failed:', err.message)
|
|
47
|
-
this.enabled = false
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async ensure_table() {
|
|
52
|
-
const create_table_query = `
|
|
53
|
-
-- All orders (persistent mapping of id/order_id to order_type & status)
|
|
54
|
-
CREATE TABLE IF NOT EXISTS all_orders (
|
|
55
|
-
id VARCHAR(128),
|
|
56
|
-
exchange VARCHAR(64) NOT NULL,
|
|
57
|
-
pair VARCHAR(32) NOT NULL,
|
|
58
|
-
order_type VARCHAR(64),
|
|
59
|
-
type VARCHAR(16) NOT NULL,
|
|
60
|
-
order_id VARCHAR(128) NOT NULL,
|
|
61
|
-
status VARCHAR(32) NOT NULL,
|
|
62
|
-
price DECIMAL(32, 16),
|
|
63
|
-
amount DECIMAL(32, 16),
|
|
64
|
-
f_amount DECIMAL(32, 16),
|
|
65
|
-
f_percentage DECIMAL(8, 4),
|
|
66
|
-
t_amount DECIMAL(32, 16),
|
|
67
|
-
fees JSONB,
|
|
68
|
-
open_time TIMESTAMPTZ,
|
|
69
|
-
close_time TIMESTAMPTZ,
|
|
70
|
-
note VARCHAR(256),
|
|
71
|
-
created_at TIMESTAMPTZ DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
|
72
|
-
updated_at TIMESTAMPTZ DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
|
73
|
-
PRIMARY KEY (exchange, pair, type, order_id)
|
|
74
|
-
);
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_all_orders_exchange_pair_order ON all_orders(exchange, pair, order_id);
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
const client = await this.pool.connect()
|
|
79
|
-
try {
|
|
80
|
-
await client.query(create_table_query)
|
|
81
|
-
this.table_ready = true
|
|
82
|
-
} finally {
|
|
83
|
-
client.release()
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Remove all rows for a specific exchange/pair (fresh start).
|
|
89
|
-
* @param {string} exchange
|
|
90
|
-
* @param {string} pair
|
|
91
|
-
*/
|
|
92
|
-
async clear_orders(exchange, pair) {
|
|
93
|
-
if (!this.enabled || !this.table_ready) return
|
|
94
|
-
const client = await this.pool.connect()
|
|
95
|
-
try {
|
|
96
|
-
await client.query('DELETE FROM all_orders WHERE exchange = $1 AND pair = $2', [exchange, pair])
|
|
97
|
-
} finally {
|
|
98
|
-
client.release()
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
start_flush_interval() {
|
|
103
|
-
if (this.flush_interval) return
|
|
104
|
-
|
|
105
|
-
this.flush_interval = setInterval(() => {
|
|
106
|
-
this.flush().catch((err) => {
|
|
107
|
-
console.error('AllOrdersStore flush error:', err.message)
|
|
108
|
-
})
|
|
109
|
-
}, this.flush_interval_ms)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
stop_flush_interval() {
|
|
113
|
-
if (this.flush_interval) {
|
|
114
|
-
clearInterval(this.flush_interval)
|
|
115
|
-
this.flush_interval = null
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Queue an order status update (non-blocking)
|
|
121
|
-
* @param {string} exchange
|
|
122
|
-
* @param {string} pair
|
|
123
|
-
* @param {string} type - 'buy' or 'sell'
|
|
124
|
-
* @param {string} order_id
|
|
125
|
-
* @param {object} data - order data
|
|
126
|
-
* @param {string} note - update note
|
|
127
|
-
*/
|
|
128
|
-
store(exchange, pair, type, order_id, data, note) {
|
|
129
|
-
if (!this.enabled) return
|
|
130
|
-
|
|
131
|
-
// Convert Date objects to UTC ISO strings for consistent timezone handling
|
|
132
|
-
const to_utc_iso = (date) => {
|
|
133
|
-
if (!date) return null
|
|
134
|
-
if (date instanceof Date) {
|
|
135
|
-
return date.toISOString()
|
|
136
|
-
}
|
|
137
|
-
return date
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const record = {
|
|
141
|
-
id: data.id,
|
|
142
|
-
exchange,
|
|
143
|
-
pair,
|
|
144
|
-
type,
|
|
145
|
-
order_id: order_id,
|
|
146
|
-
order_type: data.order_type || null,
|
|
147
|
-
status: data.status || null,
|
|
148
|
-
price: data.price || null,
|
|
149
|
-
amount: data.amount || null,
|
|
150
|
-
f_amount: data.f_amount || null,
|
|
151
|
-
f_percentage: data.f_percentage || null,
|
|
152
|
-
t_amount: data.t_amount || null,
|
|
153
|
-
fees: data.fees ? JSON.stringify(data.fees) : null,
|
|
154
|
-
open_time: to_utc_iso(data.open_time),
|
|
155
|
-
close_time: to_utc_iso(data.close_time),
|
|
156
|
-
note: note || null,
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this.queue.push(record)
|
|
160
|
-
|
|
161
|
-
// Prevent memory overflow
|
|
162
|
-
if (this.queue.length > this.max_queue_size) {
|
|
163
|
-
this.queue.shift()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Flush queued records to database (async, non-blocking to caller)
|
|
169
|
-
*/
|
|
170
|
-
async flush() {
|
|
171
|
-
if (!this.enabled || !this.table_ready || this.queue.length === 0) return
|
|
172
|
-
|
|
173
|
-
const batch = this.queue.splice(0, 100) // Process up to 100 at a time
|
|
174
|
-
|
|
175
|
-
const upsert_all_orders_query = `
|
|
176
|
-
INSERT INTO all_orders (
|
|
177
|
-
id, exchange, pair, type, order_id, order_type, status,
|
|
178
|
-
price, amount, f_amount, f_percentage, t_amount,
|
|
179
|
-
fees, open_time, close_time, note, created_at, updated_at
|
|
180
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, (NOW() AT TIME ZONE 'UTC'), (NOW() AT TIME ZONE 'UTC'))
|
|
181
|
-
ON CONFLICT (exchange, pair, type, order_id) DO UPDATE SET
|
|
182
|
-
id = EXCLUDED.id,
|
|
183
|
-
order_type = EXCLUDED.order_type,
|
|
184
|
-
status = EXCLUDED.status,
|
|
185
|
-
price = EXCLUDED.price,
|
|
186
|
-
amount = EXCLUDED.amount,
|
|
187
|
-
f_amount = EXCLUDED.f_amount,
|
|
188
|
-
f_percentage = EXCLUDED.f_percentage,
|
|
189
|
-
t_amount = EXCLUDED.t_amount,
|
|
190
|
-
fees = EXCLUDED.fees,
|
|
191
|
-
open_time = EXCLUDED.open_time,
|
|
192
|
-
close_time = EXCLUDED.close_time,
|
|
193
|
-
note = EXCLUDED.note,
|
|
194
|
-
updated_at = (NOW() AT TIME ZONE 'UTC')
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
const delete_all_orders_query = `
|
|
198
|
-
DELETE FROM all_orders
|
|
199
|
-
WHERE exchange = $1 AND pair = $2 AND type = $3 AND order_id = $4
|
|
200
|
-
`
|
|
201
|
-
|
|
202
|
-
const client = await this.pool.connect()
|
|
203
|
-
try {
|
|
204
|
-
for (const record of batch) {
|
|
205
|
-
if (record.status === 'DELETED') {
|
|
206
|
-
// Physically delete rows for deleted orders to limit DB size
|
|
207
|
-
await client.query(delete_all_orders_query, [record.exchange, record.pair, record.type, record.order_id])
|
|
208
|
-
} else {
|
|
209
|
-
// Upsert active / updated orders
|
|
210
|
-
await client.query(upsert_all_orders_query, [
|
|
211
|
-
record.id,
|
|
212
|
-
record.exchange,
|
|
213
|
-
record.pair,
|
|
214
|
-
record.type,
|
|
215
|
-
record.order_id,
|
|
216
|
-
record.order_type,
|
|
217
|
-
record.status,
|
|
218
|
-
record.price,
|
|
219
|
-
record.amount,
|
|
220
|
-
record.f_amount,
|
|
221
|
-
record.f_percentage,
|
|
222
|
-
record.t_amount,
|
|
223
|
-
record.fees,
|
|
224
|
-
record.open_time,
|
|
225
|
-
record.close_time,
|
|
226
|
-
record.note,
|
|
227
|
-
])
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch (err) {
|
|
231
|
-
// Re-queue failed records
|
|
232
|
-
this.queue.unshift(...batch)
|
|
233
|
-
throw err
|
|
234
|
-
} finally {
|
|
235
|
-
client.release()
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async close() {
|
|
240
|
-
this.stop_flush_interval()
|
|
241
|
-
if (this.queue.length > 0) {
|
|
242
|
-
try {
|
|
243
|
-
await this.flush()
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error('AllOrdersStore final flush error:', err.message)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (this.pool) {
|
|
249
|
-
await this.pool.end()
|
|
250
|
-
}
|
|
251
|
-
this.enabled = false
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
module.exports = AllOrdersStore
|