@icgio/icg-exchanges-wrapper 1.14.226 → 1.14.228
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/lib/order-status-store.js +318 -0
- package/middleware/orders.js +6 -0
- package/package.json +3 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
const { Pool } = require('pg')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Async PostgreSQL store for order status
|
|
5
|
+
* Non-blocking: all writes are fire-and-forget to avoid blocking the main process
|
|
6
|
+
*/
|
|
7
|
+
class OrderStatusStore {
|
|
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('OrderStatusStore pool error:', err.message)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await this.ensure_table()
|
|
37
|
+
this.enabled = true
|
|
38
|
+
this.start_flush_interval()
|
|
39
|
+
console.log('OrderStatusStore initialized successfully')
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('OrderStatusStore init failed:', err.message)
|
|
42
|
+
this.enabled = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async ensure_table() {
|
|
47
|
+
const create_table_query = `
|
|
48
|
+
CREATE TABLE IF NOT EXISTS order_status (
|
|
49
|
+
id SERIAL PRIMARY KEY,
|
|
50
|
+
exchange VARCHAR(64) NOT NULL,
|
|
51
|
+
pair VARCHAR(32) NOT NULL,
|
|
52
|
+
order_type VARCHAR(64),
|
|
53
|
+
type VARCHAR(16) NOT NULL,
|
|
54
|
+
order_id VARCHAR(128) NOT NULL,
|
|
55
|
+
status VARCHAR(32) NOT NULL,
|
|
56
|
+
price DECIMAL(32, 16),
|
|
57
|
+
amount DECIMAL(32, 16),
|
|
58
|
+
f_amount DECIMAL(32, 16),
|
|
59
|
+
f_percentage DECIMAL(8, 4),
|
|
60
|
+
t_amount DECIMAL(32, 16),
|
|
61
|
+
fees JSONB,
|
|
62
|
+
open_time TIMESTAMPTZ,
|
|
63
|
+
close_time TIMESTAMPTZ,
|
|
64
|
+
note VARCHAR(256),
|
|
65
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
66
|
+
UNIQUE(exchange, pair, type, order_id, status, created_at)
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_order_status_exchange_pair ON order_status(exchange, pair);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_order_status_order_id ON order_status(order_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_order_status_created_at ON order_status(created_at);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_order_status_status ON order_status(status);
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
const client = await this.pool.connect()
|
|
75
|
+
try {
|
|
76
|
+
await client.query(create_table_query)
|
|
77
|
+
this.table_ready = true
|
|
78
|
+
} finally {
|
|
79
|
+
client.release()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
start_flush_interval() {
|
|
84
|
+
if (this.flush_interval) return
|
|
85
|
+
|
|
86
|
+
this.flush_interval = setInterval(() => {
|
|
87
|
+
this.flush().catch((err) => {
|
|
88
|
+
console.error('OrderStatusStore flush error:', err.message)
|
|
89
|
+
})
|
|
90
|
+
}, this.flush_interval_ms)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
stop_flush_interval() {
|
|
94
|
+
if (this.flush_interval) {
|
|
95
|
+
clearInterval(this.flush_interval)
|
|
96
|
+
this.flush_interval = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Queue an order status update (non-blocking)
|
|
102
|
+
* @param {string} exchange
|
|
103
|
+
* @param {string} pair
|
|
104
|
+
* @param {string} type - 'buy' or 'sell'
|
|
105
|
+
* @param {string} order_id
|
|
106
|
+
* @param {object} data - order data
|
|
107
|
+
* @param {string} note - update note
|
|
108
|
+
*/
|
|
109
|
+
store(exchange, pair, type, order_id, data, note) {
|
|
110
|
+
if (!this.enabled) return
|
|
111
|
+
|
|
112
|
+
const record = {
|
|
113
|
+
exchange,
|
|
114
|
+
pair,
|
|
115
|
+
type,
|
|
116
|
+
order_id: order_id,
|
|
117
|
+
order_type: data.order_type || null,
|
|
118
|
+
status: data.status || null,
|
|
119
|
+
price: data.price || null,
|
|
120
|
+
amount: data.amount || null,
|
|
121
|
+
f_amount: data.f_amount || null,
|
|
122
|
+
f_percentage: data.f_percentage || null,
|
|
123
|
+
t_amount: data.t_amount || null,
|
|
124
|
+
fees: data.fees ? JSON.stringify(data.fees) : null,
|
|
125
|
+
open_time: data.open_time || null,
|
|
126
|
+
close_time: data.close_time || null,
|
|
127
|
+
note: note || null,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.queue.push(record)
|
|
131
|
+
|
|
132
|
+
// Prevent memory overflow
|
|
133
|
+
if (this.queue.length > this.max_queue_size) {
|
|
134
|
+
this.queue.shift()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Flush queued records to database (async, non-blocking to caller)
|
|
140
|
+
*/
|
|
141
|
+
async flush() {
|
|
142
|
+
if (!this.enabled || !this.table_ready || this.queue.length === 0) return
|
|
143
|
+
|
|
144
|
+
const batch = this.queue.splice(0, 100) // Process up to 100 at a time
|
|
145
|
+
|
|
146
|
+
const insert_query = `
|
|
147
|
+
INSERT INTO order_status (
|
|
148
|
+
exchange, pair, type, order_id, order_type, status,
|
|
149
|
+
price, amount, f_amount, f_percentage, t_amount,
|
|
150
|
+
fees, open_time, close_time, note
|
|
151
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
152
|
+
ON CONFLICT (exchange, pair, type, order_id, status, created_at) DO NOTHING
|
|
153
|
+
`
|
|
154
|
+
|
|
155
|
+
const client = await this.pool.connect()
|
|
156
|
+
try {
|
|
157
|
+
for (const record of batch) {
|
|
158
|
+
await client.query(insert_query, [
|
|
159
|
+
record.exchange,
|
|
160
|
+
record.pair,
|
|
161
|
+
record.type,
|
|
162
|
+
record.order_id,
|
|
163
|
+
record.order_type,
|
|
164
|
+
record.status,
|
|
165
|
+
record.price,
|
|
166
|
+
record.amount,
|
|
167
|
+
record.f_amount,
|
|
168
|
+
record.f_percentage,
|
|
169
|
+
record.t_amount,
|
|
170
|
+
record.fees,
|
|
171
|
+
record.open_time,
|
|
172
|
+
record.close_time,
|
|
173
|
+
record.note,
|
|
174
|
+
])
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Re-queue failed records
|
|
178
|
+
this.queue.unshift(...batch)
|
|
179
|
+
throw err
|
|
180
|
+
} finally {
|
|
181
|
+
client.release()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Query order status history
|
|
187
|
+
* @param {object} filters - { exchange, pair, order_id, status, limit, offset }
|
|
188
|
+
* @returns {Promise<Array>}
|
|
189
|
+
*/
|
|
190
|
+
async query(filters = {}) {
|
|
191
|
+
if (!this.enabled || !this.table_ready) {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const conditions = []
|
|
196
|
+
const params = []
|
|
197
|
+
let param_index = 1
|
|
198
|
+
|
|
199
|
+
if (filters.exchange) {
|
|
200
|
+
conditions.push(`exchange = $${param_index++}`)
|
|
201
|
+
params.push(filters.exchange)
|
|
202
|
+
}
|
|
203
|
+
if (filters.pair) {
|
|
204
|
+
conditions.push(`pair = $${param_index++}`)
|
|
205
|
+
params.push(filters.pair)
|
|
206
|
+
}
|
|
207
|
+
if (filters.order_id) {
|
|
208
|
+
conditions.push(`order_id = $${param_index++}`)
|
|
209
|
+
params.push(filters.order_id)
|
|
210
|
+
}
|
|
211
|
+
if (filters.status) {
|
|
212
|
+
conditions.push(`status = $${param_index++}`)
|
|
213
|
+
params.push(filters.status)
|
|
214
|
+
}
|
|
215
|
+
if (filters.from_time) {
|
|
216
|
+
conditions.push(`created_at >= $${param_index++}`)
|
|
217
|
+
params.push(filters.from_time)
|
|
218
|
+
}
|
|
219
|
+
if (filters.to_time) {
|
|
220
|
+
conditions.push(`created_at <= $${param_index++}`)
|
|
221
|
+
params.push(filters.to_time)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const where_clause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
225
|
+
const limit = filters.limit || 100
|
|
226
|
+
const offset = filters.offset || 0
|
|
227
|
+
|
|
228
|
+
const query_str = `
|
|
229
|
+
SELECT * FROM order_status
|
|
230
|
+
${where_clause}
|
|
231
|
+
ORDER BY created_at DESC
|
|
232
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
233
|
+
`
|
|
234
|
+
|
|
235
|
+
const client = await this.pool.connect()
|
|
236
|
+
try {
|
|
237
|
+
const result = await client.query(query_str, params)
|
|
238
|
+
return result.rows
|
|
239
|
+
} finally {
|
|
240
|
+
client.release()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get aggregated stats for orders
|
|
246
|
+
* @param {object} filters - { exchange, pair, from_time, to_time }
|
|
247
|
+
* @returns {Promise<object>}
|
|
248
|
+
*/
|
|
249
|
+
async get_stats(filters = {}) {
|
|
250
|
+
if (!this.enabled || !this.table_ready) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const conditions = []
|
|
255
|
+
const params = []
|
|
256
|
+
let param_index = 1
|
|
257
|
+
|
|
258
|
+
if (filters.exchange) {
|
|
259
|
+
conditions.push(`exchange = $${param_index++}`)
|
|
260
|
+
params.push(filters.exchange)
|
|
261
|
+
}
|
|
262
|
+
if (filters.pair) {
|
|
263
|
+
conditions.push(`pair = $${param_index++}`)
|
|
264
|
+
params.push(filters.pair)
|
|
265
|
+
}
|
|
266
|
+
if (filters.from_time) {
|
|
267
|
+
conditions.push(`created_at >= $${param_index++}`)
|
|
268
|
+
params.push(filters.from_time)
|
|
269
|
+
}
|
|
270
|
+
if (filters.to_time) {
|
|
271
|
+
conditions.push(`created_at <= $${param_index++}`)
|
|
272
|
+
params.push(filters.to_time)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const where_clause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
276
|
+
|
|
277
|
+
const query_str = `
|
|
278
|
+
SELECT
|
|
279
|
+
status,
|
|
280
|
+
COUNT(*) as count,
|
|
281
|
+
SUM(COALESCE(f_amount, 0)) as total_filled_amount
|
|
282
|
+
FROM order_status
|
|
283
|
+
${where_clause}
|
|
284
|
+
GROUP BY status
|
|
285
|
+
`
|
|
286
|
+
|
|
287
|
+
const client = await this.pool.connect()
|
|
288
|
+
try {
|
|
289
|
+
const result = await client.query(query_str, params)
|
|
290
|
+
return result.rows.reduce((acc, row) => {
|
|
291
|
+
acc[row.status] = {
|
|
292
|
+
count: parseInt(row.count, 10),
|
|
293
|
+
total_filled_amount: parseFloat(row.total_filled_amount) || 0,
|
|
294
|
+
}
|
|
295
|
+
return acc
|
|
296
|
+
}, {})
|
|
297
|
+
} finally {
|
|
298
|
+
client.release()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async close() {
|
|
303
|
+
this.stop_flush_interval()
|
|
304
|
+
if (this.queue.length > 0) {
|
|
305
|
+
try {
|
|
306
|
+
await this.flush()
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error('OrderStatusStore final flush error:', err.message)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (this.pool) {
|
|
312
|
+
await this.pool.end()
|
|
313
|
+
}
|
|
314
|
+
this.enabled = false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = OrderStatusStore
|
package/middleware/orders.js
CHANGED
|
@@ -7,6 +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 OrderStatusStore = require('../lib/order-status-store.js')
|
|
10
11
|
|
|
11
12
|
// Default precision functions - will be overridden if provided in settings
|
|
12
13
|
let round_price = (exchange, pair, price) => price
|
|
@@ -124,6 +125,8 @@ module.exports = class Orders {
|
|
|
124
125
|
this.accounting_benchmark = settings.accounting_benchmark
|
|
125
126
|
this.balance_benchmark = settings.balance_benchmark || {}
|
|
126
127
|
this.min_reserve_balance = settings.min_reserve_balance || {}
|
|
128
|
+
// Initialize order status store for PostgreSQL persistence (async, non-blocking)
|
|
129
|
+
this.order_status_store = new OrderStatusStore(cd.order_status_store_connection_string)
|
|
127
130
|
for (let exchange in exchange_pairs) {
|
|
128
131
|
let api_key_temp = cd[exchange]['api_keys'] || cd[exchange]['api_key'],
|
|
129
132
|
secret_key_temp = cd[exchange]['secret_keys'] || cd[exchange]['secret_key'],
|
|
@@ -753,6 +756,9 @@ module.exports = class Orders {
|
|
|
753
756
|
console.log(`f_filled_orders_info:${exchange},${pair},${type},${order_id},${price},${amount},${f_amount},${t_amount},${fees},${close_time},${note}`)
|
|
754
757
|
}
|
|
755
758
|
// end: log every f-filled trade for future analysis
|
|
759
|
+
// Store order status to PostgreSQL asynchronously (non-blocking)
|
|
760
|
+
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
761
|
+
this.order_status_store.store(exchange, pair, type, order_id, order_data, note)
|
|
756
762
|
}
|
|
757
763
|
delete_order_info(exchange, pair, type, order_id, note) {
|
|
758
764
|
this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icgio/icg-exchanges-wrapper",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.228",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://github.com/icgio/icg-exchanges-wrapper#readme",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@icgio/icg-exchanges": "^1.32.
|
|
20
|
+
"@icgio/icg-exchanges": "^1.32.118",
|
|
21
21
|
"@icgio/icg-utils": "^1.9.38",
|
|
22
22
|
"lodash": "^4.17.20",
|
|
23
|
+
"pg": "^8.13.1",
|
|
23
24
|
"uuid": "^11.1.0"
|
|
24
25
|
}
|
|
25
26
|
}
|