@icgio/icg-exchanges-wrapper 1.14.227 → 1.14.229

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,233 @@
1
+ const { Pool } = require('pg')
2
+
3
+ /**
4
+ * Async PostgreSQL store for open orders
5
+ * Non-blocking: all writes are fire-and-forget to avoid blocking the main process
6
+ */
7
+ class OpenOrdersStore {
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('OpenOrdersStore 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('OpenOrdersStore initialized successfully')
45
+ } catch (err) {
46
+ console.error('OpenOrdersStore init failed:', err.message)
47
+ this.enabled = false
48
+ }
49
+ }
50
+
51
+ async ensure_table() {
52
+ const create_table_query = `
53
+ CREATE TABLE IF NOT EXISTS open_orders (
54
+ exchange VARCHAR(64) NOT NULL,
55
+ pair VARCHAR(32) NOT NULL,
56
+ order_type VARCHAR(64),
57
+ type VARCHAR(16) NOT NULL,
58
+ order_id VARCHAR(128) NOT NULL,
59
+ status VARCHAR(32) NOT NULL,
60
+ price DECIMAL(32, 16),
61
+ amount DECIMAL(32, 16),
62
+ f_amount DECIMAL(32, 16),
63
+ f_percentage DECIMAL(8, 4),
64
+ t_amount DECIMAL(32, 16),
65
+ fees JSONB,
66
+ open_time TIMESTAMPTZ,
67
+ close_time TIMESTAMPTZ,
68
+ note VARCHAR(256),
69
+ updated_at TIMESTAMPTZ DEFAULT (NOW() AT TIME ZONE 'UTC'),
70
+ PRIMARY KEY (exchange, pair, type, order_id)
71
+ );
72
+ CREATE INDEX IF NOT EXISTS idx_open_orders_status ON open_orders(status);
73
+ CREATE INDEX IF NOT EXISTS idx_open_orders_exchange ON open_orders(exchange);
74
+ `
75
+
76
+ const client = await this.pool.connect()
77
+ try {
78
+ await client.query(create_table_query)
79
+ this.table_ready = true
80
+ } finally {
81
+ client.release()
82
+ }
83
+ }
84
+
85
+ start_flush_interval() {
86
+ if (this.flush_interval) return
87
+
88
+ this.flush_interval = setInterval(() => {
89
+ this.flush().catch((err) => {
90
+ console.error('OpenOrdersStore flush error:', err.message)
91
+ })
92
+ }, this.flush_interval_ms)
93
+ }
94
+
95
+ stop_flush_interval() {
96
+ if (this.flush_interval) {
97
+ clearInterval(this.flush_interval)
98
+ this.flush_interval = null
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Queue an order status update (non-blocking)
104
+ * @param {string} exchange
105
+ * @param {string} pair
106
+ * @param {string} type - 'buy' or 'sell'
107
+ * @param {string} order_id
108
+ * @param {object} data - order data
109
+ * @param {string} note - update note
110
+ */
111
+ store(exchange, pair, type, order_id, data, note) {
112
+ if (!this.enabled) return
113
+
114
+ // Convert Date objects to UTC ISO strings for consistent timezone handling
115
+ const to_utc_iso = (date) => {
116
+ if (!date) return null
117
+ if (date instanceof Date) {
118
+ return date.toISOString()
119
+ }
120
+ return date
121
+ }
122
+
123
+ const record = {
124
+ exchange,
125
+ pair,
126
+ type,
127
+ order_id: order_id,
128
+ order_type: data.order_type || null,
129
+ status: data.status || null,
130
+ price: data.price || null,
131
+ amount: data.amount || null,
132
+ f_amount: data.f_amount || null,
133
+ f_percentage: data.f_percentage || null,
134
+ t_amount: data.t_amount || null,
135
+ fees: data.fees ? JSON.stringify(data.fees) : null,
136
+ open_time: to_utc_iso(data.open_time),
137
+ close_time: to_utc_iso(data.close_time),
138
+ note: note || null,
139
+ }
140
+
141
+ this.queue.push(record)
142
+
143
+ // Prevent memory overflow
144
+ if (this.queue.length > this.max_queue_size) {
145
+ this.queue.shift()
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Flush queued records to database (async, non-blocking to caller)
151
+ */
152
+ async flush() {
153
+ if (!this.enabled || !this.table_ready || this.queue.length === 0) return
154
+
155
+ const batch = this.queue.splice(0, 100) // Process up to 100 at a time
156
+
157
+ const upsert_query = `
158
+ INSERT INTO open_orders (
159
+ exchange, pair, type, order_id, order_type, status,
160
+ price, amount, f_amount, f_percentage, t_amount,
161
+ fees, open_time, close_time, note, updated_at
162
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, (NOW() AT TIME ZONE 'UTC'))
163
+ ON CONFLICT (exchange, pair, type, order_id) DO UPDATE SET
164
+ order_type = EXCLUDED.order_type,
165
+ status = EXCLUDED.status,
166
+ price = EXCLUDED.price,
167
+ amount = EXCLUDED.amount,
168
+ f_amount = EXCLUDED.f_amount,
169
+ f_percentage = EXCLUDED.f_percentage,
170
+ t_amount = EXCLUDED.t_amount,
171
+ fees = EXCLUDED.fees,
172
+ open_time = EXCLUDED.open_time,
173
+ close_time = EXCLUDED.close_time,
174
+ note = EXCLUDED.note,
175
+ updated_at = (NOW() AT TIME ZONE 'UTC')
176
+ `
177
+
178
+ const delete_query = `
179
+ DELETE FROM open_orders
180
+ WHERE exchange = $1 AND pair = $2 AND type = $3 AND order_id = $4
181
+ `
182
+
183
+ const client = await this.pool.connect()
184
+ try {
185
+ for (const record of batch) {
186
+ if (record.status === 'DELETED') {
187
+ await client.query(delete_query, [record.exchange, record.pair, record.type, record.order_id])
188
+ } else {
189
+ await client.query(upsert_query, [
190
+ record.exchange,
191
+ record.pair,
192
+ record.type,
193
+ record.order_id,
194
+ record.order_type,
195
+ record.status,
196
+ record.price,
197
+ record.amount,
198
+ record.f_amount,
199
+ record.f_percentage,
200
+ record.t_amount,
201
+ record.fees,
202
+ record.open_time,
203
+ record.close_time,
204
+ record.note,
205
+ ])
206
+ }
207
+ }
208
+ } catch (err) {
209
+ // Re-queue failed records
210
+ this.queue.unshift(...batch)
211
+ throw err
212
+ } finally {
213
+ client.release()
214
+ }
215
+ }
216
+
217
+ async close() {
218
+ this.stop_flush_interval()
219
+ if (this.queue.length > 0) {
220
+ try {
221
+ await this.flush()
222
+ } catch (err) {
223
+ console.error('OpenOrdersStore final flush error:', err.message)
224
+ }
225
+ }
226
+ if (this.pool) {
227
+ await this.pool.end()
228
+ }
229
+ this.enabled = false
230
+ }
231
+ }
232
+
233
+ module.exports = OpenOrdersStore
@@ -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 OpenOrdersStore = 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 OpenOrdersStore(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,10 +756,16 @@ 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) {
764
+ const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
758
765
  this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
759
766
  console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
767
+ // Store deletion to PostgreSQL (non-blocking)
768
+ this.order_status_store.store(exchange, pair, type, order_id, { ...order_data, status: 'DELETED' }, note)
760
769
  }
761
770
  check_balances() {
762
771
  let balances_handler = (exchange, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges-wrapper",
3
- "version": "1.14.227",
3
+ "version": "1.14.229",
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.118",
20
+ "@icgio/icg-exchanges": "^1.32.119",
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
  }