@icgio/icg-exchanges-wrapper 1.14.252 → 1.14.254
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/index.js +5 -0
- package/lib/ws-data-client.js +248 -0
- package/middleware/orders.js +48 -18
- package/package.json +1 -1
- package/lib/order-status-store.js +0 -255
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/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
|
|
@@ -244,9 +244,10 @@ module.exports = class Orders {
|
|
|
244
244
|
this.process_query_order(this.query_order)
|
|
245
245
|
}, PROCESS_OPEN_ORDERS_TRADERS_INTERVAL_MS)
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
247
|
+
// Initialize WS data client for streaming to dashboard backend
|
|
248
|
+
if (settings.ws_backend_secret && settings.client_id) {
|
|
249
|
+
ws_data_client.init(settings.ws_backend_secret, settings.client_id)
|
|
250
|
+
ws_data_client.set_order_info_ref(this.order_info)
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
init_account(balances, benchmarks) {
|
|
@@ -500,10 +501,12 @@ module.exports = class Orders {
|
|
|
500
501
|
trade.amount = round_amount(exchange, pair, trade.amount)
|
|
501
502
|
let { type, order_id } = trade
|
|
502
503
|
let order_info = _.get(this.order_info, [exchange, pair, type, order_id])
|
|
504
|
+
if (!order_info) {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
503
507
|
const trade_amount = round_amount(exchange, pair, trade.amount)
|
|
504
508
|
const order_info_f_amount = round_amount(exchange, pair, order_info.f_amount)
|
|
505
|
-
if (
|
|
506
|
-
} else if (_.includes(['CANCELED', 'CANCELING'], order_info.status) && trade_amount > order_info_f_amount) {
|
|
509
|
+
if (_.includes(['CANCELED', 'CANCELING'], order_info.status) && trade_amount > order_info_f_amount) {
|
|
507
510
|
// console.log('TRADES: %s ORDER FILLED', order_info.status, trade, order_info)
|
|
508
511
|
this.update_order_info(
|
|
509
512
|
exchange,
|
|
@@ -660,6 +663,9 @@ module.exports = class Orders {
|
|
|
660
663
|
open_orders[exchange][pair].open_orders.map((order) => {
|
|
661
664
|
if (_.get(this.order_info, [exchange, pair, order.type, order.order_id])) {
|
|
662
665
|
let order_info = this.order_info[exchange][pair][order.type][order.order_id]
|
|
666
|
+
if (!order_info) {
|
|
667
|
+
return
|
|
668
|
+
}
|
|
663
669
|
const order_f_amount = round_amount(exchange, pair, order.f_amount)
|
|
664
670
|
const order_info_f_amount = round_amount(exchange, pair, order_info.f_amount)
|
|
665
671
|
if (_.includes(['N-FILLED', 'P-FILLED'], order_info.status) && order_f_amount > order_info_f_amount && order_f_amount / order_info.t_amount > this.f_filled_threshold) {
|
|
@@ -776,6 +782,17 @@ module.exports = class Orders {
|
|
|
776
782
|
}
|
|
777
783
|
_.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.${key}`, dict[key], Object)
|
|
778
784
|
}
|
|
785
|
+
|
|
786
|
+
// Add trace entry for order history
|
|
787
|
+
const trace_entry = {
|
|
788
|
+
ts: Date.now(),
|
|
789
|
+
status: dict.status || _.get(this.order_info, [exchange, pair, type, order_id, 'status']),
|
|
790
|
+
note,
|
|
791
|
+
f_amount: dict.f_amount,
|
|
792
|
+
}
|
|
793
|
+
const existing_trace = _.get(this.order_info, [exchange, pair, type, order_id, 'trace'], [])
|
|
794
|
+
_.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.trace`, [...existing_trace, trace_entry], Object)
|
|
795
|
+
|
|
779
796
|
console.log('UPDATE %s: %s %s %s %s %j', note, exchange, pair, type, order_id, dict)
|
|
780
797
|
// start: log every f-filled trade for future analysis
|
|
781
798
|
if (dict.status === 'F-FILLED') {
|
|
@@ -784,21 +801,38 @@ module.exports = class Orders {
|
|
|
784
801
|
console.log(`f_filled_orders_info:${exchange},${pair},${type},${order_id},${price},${amount},${f_amount},${t_amount},${fees},${close_time},${note}`)
|
|
785
802
|
}
|
|
786
803
|
// end: log every f-filled trade for future analysis
|
|
787
|
-
|
|
804
|
+
|
|
805
|
+
// Send order update via WebSocket to dashboard backend
|
|
788
806
|
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
789
|
-
|
|
790
|
-
|
|
807
|
+
// Only send N-FILLED and later statuses (skip INIT which has temporary order_id)
|
|
808
|
+
if (dict.status !== 'INIT' && order_data.status !== 'INIT') {
|
|
809
|
+
ws_data_client.send_order({
|
|
810
|
+
action: 'update',
|
|
811
|
+
exchange,
|
|
812
|
+
pair,
|
|
813
|
+
side: type,
|
|
814
|
+
order_id,
|
|
815
|
+
data: order_data,
|
|
816
|
+
note,
|
|
817
|
+
})
|
|
791
818
|
}
|
|
792
819
|
}
|
|
793
820
|
delete_order_info(exchange, pair, type, order_id, note) {
|
|
794
821
|
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
795
822
|
this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
|
|
796
823
|
console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
|
|
797
|
-
|
|
798
|
-
//
|
|
799
|
-
if (order_data.status
|
|
800
|
-
|
|
801
|
-
|
|
824
|
+
|
|
825
|
+
// Send delete event via WebSocket (only for non-INIT orders)
|
|
826
|
+
if (order_data.status !== 'INIT') {
|
|
827
|
+
ws_data_client.send_order({
|
|
828
|
+
action: 'delete',
|
|
829
|
+
exchange,
|
|
830
|
+
pair,
|
|
831
|
+
side: type,
|
|
832
|
+
order_id,
|
|
833
|
+
data: order_data,
|
|
834
|
+
note,
|
|
835
|
+
})
|
|
802
836
|
}
|
|
803
837
|
}
|
|
804
838
|
check_balances() {
|
|
@@ -920,10 +954,6 @@ module.exports = class Orders {
|
|
|
920
954
|
}
|
|
921
955
|
// cancel previous orders
|
|
922
956
|
else if (this.cancel_previous_orders && !this.previous_orders_canceled) {
|
|
923
|
-
// remove all previous orders for this exchange/pair from all_orders (fire-and-forget)
|
|
924
|
-
if (this.function_enabled.all_orders_store) {
|
|
925
|
-
this.all_orders_store.clear_orders(exchange, pair).catch((err) => console.error('clear_all_orders error', exchange, pair, err.message))
|
|
926
|
-
}
|
|
927
957
|
if (this.cancel_previous_orders_with_delay) {
|
|
928
958
|
setTimeout(() => {
|
|
929
959
|
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
|