@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.
- package/lib/order-status-store.js +233 -0
- package/middleware/orders.js +9 -0
- package/package.json +3 -2
|
@@ -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
|
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 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.
|
|
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.
|
|
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
|
}
|