Skip to content

Commit 4978222

Browse files
committed
feat: add Gateio
1 parent 456bef7 commit 4978222

File tree

2 files changed

+289
-1
lines changed

2 files changed

+289
-1
lines changed

src/exchanges/gateio.js

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
const Exchange = require('../exchange')
2+
const axios = require('axios')
3+
4+
const IS_USDT_SETTLEMENT_REGEX = /USDT$/
5+
6+
class Gateio extends Exchange {
7+
constructor() {
8+
super()
9+
this.id = 'GATEIO'
10+
this.liquidationApi = {}
11+
this.maxConnectionsPerApi = 100
12+
13+
this.endpoints = {
14+
PRODUCTS: [
15+
'https://api.gateio.ws/api/v4/spot/currency_pairs',
16+
'https://api.gateio.ws/api/v4/futures/usdt/contracts',
17+
'https://api.gateio.ws/api/v4/futures/btc/contracts'
18+
]
19+
}
20+
}
21+
22+
async getUrl(pair) {
23+
if (typeof this.specs[pair] === 'number') {
24+
if (IS_USDT_SETTLEMENT_REGEX.test(pair)) {
25+
return 'wss://fx-ws.gateio.ws/v4/ws/usdt'
26+
}
27+
return 'wss://fx-ws.gateio.ws/v4/ws/btc'
28+
}
29+
30+
return 'wss://api.gateio.ws/ws/v4/'
31+
}
32+
33+
formatProducts(responses) {
34+
const products = []
35+
const specs = {}
36+
37+
for (const response of responses) {
38+
for (const product of response) {
39+
if (product.id) {
40+
products.push(`${product.id}-SPOT`)
41+
} else {
42+
products.push(product.name)
43+
specs[product.name] = product.quanto_multiplier
44+
? parseFloat(product.quanto_multiplier)
45+
: 1
46+
}
47+
}
48+
}
49+
50+
return { products, specs }
51+
}
52+
53+
/**
54+
* Sub
55+
* @param {WebSocket} api
56+
* @param {string} pair
57+
*/
58+
async subscribe(api, pair) {
59+
const isFutures = typeof this.specs[pair] === 'number'
60+
61+
if (isFutures) {
62+
const settlement = IS_USDT_SETTLEMENT_REGEX.test(pair) ? 'usdt' : 'btc'
63+
64+
if (this.liquidationApi[settlement]) {
65+
const index = this.liquidationApi[settlement].pairs.indexOf(pair)
66+
if (index === -1) {
67+
this.liquidationApi[settlement].pairs.push(pair)
68+
}
69+
}
70+
}
71+
72+
if (!(await super.subscribe(api, pair))) {
73+
return
74+
}
75+
76+
api.send(
77+
JSON.stringify({
78+
channel: `${isFutures ? 'futures' : 'spot'}.trades`,
79+
event: 'subscribe',
80+
payload: [pair.replace('-SPOT', '')]
81+
})
82+
)
83+
84+
return true
85+
}
86+
87+
/**
88+
* Unsub
89+
* @param {WebSocket} api
90+
* @param {string} pair
91+
*/
92+
async unsubscribe(api, pair) {
93+
const isFutures = typeof this.specs[pair] === 'number'
94+
95+
if (isFutures) {
96+
const settlement = IS_USDT_SETTLEMENT_REGEX.test(pair) ? 'usdt' : 'btc'
97+
98+
if (this.liquidationApi[settlement]) {
99+
const index = this.liquidationApi[settlement].pairs.indexOf(pair)
100+
101+
if (index !== -1) {
102+
this.liquidationApi[settlement].pairs.splice(index, 1)
103+
}
104+
}
105+
}
106+
107+
if (!(await super.unsubscribe(api, pair))) {
108+
return
109+
}
110+
111+
api.send(
112+
JSON.stringify({
113+
channel: `${isFutures ? 'futures' : 'spot'}.trades`,
114+
event: 'unsubscribe',
115+
payload: [pair.replace('-SPOT', '')]
116+
})
117+
)
118+
119+
return true
120+
}
121+
122+
formatSpotTrade(trade) {
123+
return {
124+
exchange: this.id,
125+
pair: trade.currency_pair + '-SPOT',
126+
timestamp: +trade.create_time_ms,
127+
price: +trade.price,
128+
size: +trade.amount,
129+
side: trade.side
130+
}
131+
}
132+
133+
formatFuturesTrade(trade) {
134+
return {
135+
exchange: this.id,
136+
pair: trade.contract,
137+
timestamp: +trade.create_time_ms,
138+
price: +trade.price,
139+
size:
140+
this.specs[trade.contract] > 0
141+
? Math.abs(trade.size) * this.specs[trade.contract]
142+
: Math.abs(trade.size) / trade.price,
143+
side: trade.size > 0 ? 'buy' : 'sell'
144+
}
145+
}
146+
147+
formatLiquidation(trade) {
148+
return {
149+
...this.formatFuturesTrade(trade),
150+
side: trade.size > 0 ? 'sell' : 'buy',
151+
liquidation: true
152+
}
153+
}
154+
155+
onMessage(event, api) {
156+
const json = JSON.parse(event.data)
157+
158+
if (!json) {
159+
return
160+
}
161+
162+
if (json.event && json.event === 'update' && json.result) {
163+
if (json.result.length && json.channel === 'futures.trades') {
164+
const results = json.result
165+
.filter(trade => !trade.is_internal)
166+
.map(trade => this.formatFuturesTrade(trade))
167+
if (results.length) {
168+
return this.emitTrades(api.id, results)
169+
}
170+
}
171+
if (json.channel === 'spot.trades') {
172+
return this.emitTrades(api.id, [this.formatSpotTrade(json.result)])
173+
}
174+
}
175+
}
176+
177+
onApiCreated(api) {
178+
const isFutures = /fx-ws/.test(api.url)
179+
180+
if (isFutures) {
181+
const settlement = api.url.split('/').pop()
182+
this.startLiquidationInterval(settlement)
183+
}
184+
185+
this.startKeepAlive(
186+
api,
187+
isFutures ? { channel: 'futures.ping' } : { channel: 'spot.ping' },
188+
18000
189+
)
190+
}
191+
192+
onApiRemoved(api) {
193+
const isFutures = /fx-ws/.test(api.url)
194+
195+
if (isFutures) {
196+
const settlement = api.url.split('/').pop()
197+
this.stopLiquidationInterval(settlement)
198+
}
199+
200+
this.stopKeepAlive(api)
201+
}
202+
203+
startLiquidationInterval(settlement) {
204+
if (!this.liquidationApi[settlement]) {
205+
this.liquidationApi[settlement] = {
206+
count: 0,
207+
interval: null,
208+
timestamp: null,
209+
pairs: []
210+
}
211+
}
212+
213+
if (this.liquidationApi[settlement].interval) {
214+
this.liquidationApi[settlement].count++
215+
return
216+
}
217+
218+
this.liquidationApi[settlement].count = 1
219+
this.liquidationApi[settlement].timestamp = +new Date()
220+
this.liquidationApi[settlement].interval = setInterval(
221+
() => {
222+
this.fetchLiquidations(settlement)
223+
},
224+
2000 + Math.random() * 1000
225+
)
226+
}
227+
228+
stopLiquidationInterval(settlement) {
229+
if (!this.liquidationApi[settlement]) {
230+
return
231+
}
232+
233+
if (this.liquidationApi[settlement].count) {
234+
this.liquidationApi[settlement].count--
235+
}
236+
237+
if (
238+
!this.liquidationApi[settlement].count &&
239+
this.liquidationApi[settlement].interval
240+
) {
241+
clearInterval(this.liquidationApi[settlement].interval)
242+
this.liquidationApi[settlement].interval = null
243+
}
244+
}
245+
246+
async fetchLiquidations(settlement) {
247+
if (this.liquidationApi[settlement].loading) {
248+
return
249+
}
250+
this.liquidationApi[settlement].loading = true
251+
try {
252+
const response = await axios.get(
253+
`https://api.gateio.ws/api/v4/futures/${settlement}/liq_orders?from=${Math.round(
254+
this.liquidationApi[settlement].timestamp / 1000
255+
)}`
256+
)
257+
if (response.data && response.data.length) {
258+
this.emitLiquidations(
259+
null,
260+
response.data
261+
.filter(
262+
liquidation =>
263+
this.liquidationApi[settlement].pairs.indexOf(
264+
liquidation.contract
265+
) !== -1
266+
)
267+
.map(liquidation =>
268+
this.formatLiquidation({
269+
create_time_ms: liquidation.time * 1000,
270+
price: liquidation.fill_price,
271+
contract: liquidation.contract,
272+
size: liquidation.size
273+
})
274+
)
275+
)
276+
}
277+
278+
this.liquidationApi[settlement].timestamp = +new Date()
279+
280+
return []
281+
} catch (error) {
282+
console.error(`[${this.id}.fetchLiquidations] ${error.message}`)
283+
} finally {
284+
this.liquidationApi[settlement].loading = false
285+
}
286+
}
287+
}
288+
module.exports = Gateio

src/exchanges/kraken.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const Exchange = require('../exchange')
22
const WebSocket = require('websocket').w3cwebsocket
33
const axios = require('axios')
4-
const { getHms, sleep } = require('../helper')
4+
const { getHms } = require('../helper')
55

66
class Kraken extends Exchange {
77
constructor() {

0 commit comments

Comments
 (0)