@icgio/icg-exchanges-wrapper 1.14.253 → 1.14.255

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/README.md CHANGED
@@ -1 +1,129 @@
1
- # icg-exchanges-wrapper
1
+ # ICG Exchanges Wrapper
2
+
3
+ High-level wrapper functions for the ICG exchanges package, providing simplified interfaces for common trading operations across multiple exchanges.
4
+
5
+ ## Features
6
+
7
+ - **Multi-Exchange Operations**: Execute operations across multiple exchanges in parallel
8
+ - **Unified Data Format**: Consistent response format regardless of exchange
9
+ - **Order Management Middleware**: Advanced order placement with retries and validation
10
+ - **WebSocket Data Client**: Real-time log streaming support
11
+ - **Rate Data Aggregation**: Combine orderbook data from multiple sources
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @icgio/icg-exchanges-wrapper
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Functions Module
22
+
23
+ ```javascript
24
+ const { functions } = require('@icgio/icg-exchanges-wrapper')
25
+
26
+ // Get rates (orderbook) from multiple exchanges
27
+ const rates = await functions.get_exchanges_rates(exchanges, pairs)
28
+
29
+ // Get balances from multiple exchanges
30
+ const balances = await functions.get_exchanges_balance(exchanges)
31
+
32
+ // Get trade history
33
+ const trades = await functions.get_exchanges_trades(exchanges, pairs)
34
+
35
+ // Get open orders
36
+ const open_orders = await functions.get_exchanges_open_orders(exchanges, pairs)
37
+
38
+ // Get deposit history
39
+ const deposits = await functions.get_exchanges_deposits(exchanges)
40
+
41
+ // Get withdrawal history
42
+ const withdrawals = await functions.get_exchanges_withdrawals(exchanges)
43
+
44
+ // Get OHLCV data
45
+ const ohlcv = await functions.get_exchanges_ohlcv(exchanges, pairs, interval)
46
+
47
+ // Get market history
48
+ const market_history = await functions.get_exchanges_market_history(exchanges, pairs)
49
+ ```
50
+
51
+ ### Orders Middleware
52
+
53
+ ```javascript
54
+ const Orders = require('@icgio/icg-exchanges-wrapper').orders
55
+
56
+ // Create an order manager instance
57
+ const orders = new Orders(exchanges_config)
58
+
59
+ // Place orders with advanced features
60
+ await orders.place_order(exchange, pair, side, amount, price, options)
61
+
62
+ // Cancel orders
63
+ await orders.cancel_order(exchange, pair, order_id)
64
+
65
+ // Get order status
66
+ const status = await orders.get_order_status(exchange, pair, order_id)
67
+ ```
68
+
69
+ ### WebSocket Data Client
70
+
71
+ ```javascript
72
+ const { ws_data_client, send_log } = require('@icgio/icg-exchanges-wrapper')
73
+
74
+ // Send log messages via WebSocket
75
+ send_log('info', 'Operation completed successfully')
76
+ send_log('error', 'An error occurred')
77
+ ```
78
+
79
+ ## Project Structure
80
+
81
+ ```
82
+ icg-exchanges-wrapper/
83
+ ├── index.js # Main entry point
84
+ ├── functions/
85
+ │ ├── get-exchanges-balance.js # Balance fetching
86
+ │ ├── get-exchanges-deposits.js # Deposit history
87
+ │ ├── get-exchanges-open-orders.js # Open orders
88
+ │ ├── get-exchanges-rates-market-history-ohlcv.js # Market data
89
+ │ ├── get-exchanges-trades.js # Trade history
90
+ │ └── get-exchanges-withdrawals.js # Withdrawal history
91
+ ├── middleware/
92
+ │ ├── account.js # Account middleware
93
+ │ └── orders.js # Order management middleware
94
+ ├── lib/
95
+ │ └── ws-data-client.js # WebSocket data client
96
+ └── package.json
97
+ ```
98
+
99
+ ## Available Functions
100
+
101
+ | Function | Description |
102
+ |----------|-------------|
103
+ | `get_exchanges_rates` | Fetch orderbook/rates from exchanges |
104
+ | `get_exchanges_market_history` | Fetch recent trade history |
105
+ | `get_exchanges_ohlcv` | Fetch OHLCV candlestick data |
106
+ | `get_exchanges_balance` | Fetch account balances |
107
+ | `get_exchanges_trades` | Fetch user trade history |
108
+ | `get_exchanges_open_orders` | Fetch open orders |
109
+ | `get_exchanges_deposits` | Fetch deposit history |
110
+ | `get_exchanges_withdrawals` | Fetch withdrawal history |
111
+
112
+ ## Dependencies
113
+
114
+ | Package | Purpose |
115
+ |---------|---------|
116
+ | `@icgio/icg-exchanges` | Exchange implementations |
117
+ | `@icgio/icg-utils` | Utility functions |
118
+ | `lodash` | Utility library |
119
+ | `pg` | PostgreSQL client |
120
+
121
+ ## Code Style
122
+
123
+ - CommonJS modules (`require()` / `module.exports`)
124
+ - Variable naming: `snake_case`
125
+ - Function naming: `snake_case`
126
+
127
+ ## License
128
+
129
+ ISC License
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 }
@@ -130,11 +130,11 @@ module.exports = class Account {
130
130
  if (_.includes(constants.stablecoins, cur)) {
131
131
  profit.USD += position
132
132
  } else if (_.includes(['KRW'], cur)) {
133
- profit.USD += position / this.forex_rates['KRWUSD']
133
+ profit.USD += position * this.forex_rates['KRWUSD']
134
134
  } else if (_.includes(['HKD'], cur)) {
135
- profit.USD += position / this.forex_rates['HKDUSD']
135
+ profit.USD += position * this.forex_rates['HKDUSD']
136
136
  } else if (_.includes(['CNYT'], cur)) {
137
- profit.USD += position / this.forex_rates['CNYUSD']
137
+ profit.USD += position * this.forex_rates['CNYUSD']
138
138
  } else {
139
139
  if (!cmc_rates[cur]) {
140
140
  console.log('get_exchange_profit rates_cmc %s does not exist', cur)
@@ -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 AllOrdersStore = require('../lib/order-status-store.js')
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
@@ -199,7 +199,6 @@ module.exports = class Orders {
199
199
  market_history_check: true,
200
200
  ohlcv_check: false,
201
201
  cmc_rates_check: true,
202
- all_orders_store: false,
203
202
  }
204
203
  if (this.function_enabled.balances_check) {
205
204
  this.check_balances()
@@ -244,9 +243,10 @@ module.exports = class Orders {
244
243
  this.process_query_order(this.query_order)
245
244
  }, PROCESS_OPEN_ORDERS_TRADERS_INTERVAL_MS)
246
245
  }
247
- if (this.function_enabled.all_orders_store) {
248
- // Initialize all orders store for PostgreSQL persistence (async, non-blocking)
249
- this.all_orders_store = new AllOrdersStore(cd.all_orders_store_connection_string)
246
+ // Initialize WS data client for streaming to dashboard backend
247
+ if (settings.ws_backend_secret && settings.client_id) {
248
+ ws_data_client.init(settings.ws_backend_secret, settings.client_id)
249
+ ws_data_client.set_order_info_ref(this.order_info)
250
250
  }
251
251
  }
252
252
  init_account(balances, benchmarks) {
@@ -781,6 +781,17 @@ module.exports = class Orders {
781
781
  }
782
782
  _.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.${key}`, dict[key], Object)
783
783
  }
784
+
785
+ // Add trace entry for order history
786
+ const trace_entry = {
787
+ ts: Date.now(),
788
+ status: dict.status || _.get(this.order_info, [exchange, pair, type, order_id, 'status']),
789
+ note,
790
+ f_amount: dict.f_amount,
791
+ }
792
+ const existing_trace = _.get(this.order_info, [exchange, pair, type, order_id, 'trace'], [])
793
+ _.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.trace`, [...existing_trace, trace_entry], Object)
794
+
784
795
  console.log('UPDATE %s: %s %s %s %s %j', note, exchange, pair, type, order_id, dict)
785
796
  // start: log every f-filled trade for future analysis
786
797
  if (dict.status === 'F-FILLED') {
@@ -789,21 +800,38 @@ module.exports = class Orders {
789
800
  console.log(`f_filled_orders_info:${exchange},${pair},${type},${order_id},${price},${amount},${f_amount},${t_amount},${fees},${close_time},${note}`)
790
801
  }
791
802
  // end: log every f-filled trade for future analysis
792
- // Store order status to PostgreSQL asynchronously (non-blocking)
803
+
804
+ // Send order update via WebSocket to dashboard backend
793
805
  const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
794
- if (this.function_enabled.all_orders_store) {
795
- this.all_orders_store.store(exchange, pair, type, order_id, order_data, note)
806
+ // Only send N-FILLED and later statuses (skip INIT which has temporary order_id)
807
+ if (dict.status !== 'INIT' && order_data.status !== 'INIT') {
808
+ ws_data_client.send_order({
809
+ action: 'update',
810
+ exchange,
811
+ pair,
812
+ side: type,
813
+ order_id,
814
+ data: order_data,
815
+ note,
816
+ })
796
817
  }
797
818
  }
798
819
  delete_order_info(exchange, pair, type, order_id, note) {
799
820
  const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
800
821
  this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
801
822
  console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
802
- // Store deletion to PostgreSQL (non-blocking)
803
- // Keep final F-FILLED / P-FILLED states in DB (do NOT mark them as DELETED)
804
- if (order_data.status === 'F-FILLED' || order_data.status === 'P-FILLED') {
805
- } else if (this.function_enabled.all_orders_store) {
806
- this.all_orders_store.store(exchange, pair, type, order_id, { ...order_data, status: 'DELETED' }, note)
823
+
824
+ // Send delete event via WebSocket (only for non-INIT orders)
825
+ if (order_data.status !== 'INIT') {
826
+ ws_data_client.send_order({
827
+ action: 'delete',
828
+ exchange,
829
+ pair,
830
+ side: type,
831
+ order_id,
832
+ data: order_data,
833
+ note,
834
+ })
807
835
  }
808
836
  }
809
837
  check_balances() {
@@ -925,10 +953,6 @@ module.exports = class Orders {
925
953
  }
926
954
  // cancel previous orders
927
955
  else if (this.cancel_previous_orders && !this.previous_orders_canceled) {
928
- // remove all previous orders for this exchange/pair from all_orders (fire-and-forget)
929
- if (this.function_enabled.all_orders_store) {
930
- this.all_orders_store.clear_orders(exchange, pair).catch((err) => console.error('clear_all_orders error', exchange, pair, err.message))
931
- }
932
956
  if (this.cancel_previous_orders_with_delay) {
933
957
  setTimeout(() => {
934
958
  orders.map((order) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges-wrapper",
3
- "version": "1.14.253",
3
+ "version": "1.14.255",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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