@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.
@@ -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
@@ -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.226",
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.117",
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
  }