Skip to content

Commit a842402

Browse files
adeacetislmvdz
andcommitted
feat: add bitmart
Co-authored-by: lmvdz <lmvanderzande@gmail.com>
1 parent f4c1173 commit a842402

File tree

3 files changed

+235
-2
lines changed

3 files changed

+235
-2
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"express-rate-limit": "^5.1.1",
3232
"influx": "^5.9.3",
3333
"jsoning": "^0.13.23",
34-
"pako": "^1.0.6",
34+
"pako": "^1.0.11",
3535
"pm2": "^5.3.0",
3636
"tx2": "^1.0.5",
3737
"uuid": "^9.0.0",

src/exchanges/bitmart.js

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
const Exchange = require('../exchange')
2+
const { sleep, getHms } = require('../helper')
3+
const axios = require('axios')
4+
5+
const { inflateRaw } = require('pako')
6+
7+
/**
8+
* Bitmart class representing the Bitmart exchange.
9+
* @extends Exchange
10+
*/
11+
class Bitmart extends Exchange {
12+
constructor() {
13+
super()
14+
15+
/**
16+
* @type {string}
17+
*/
18+
this.id = 'BITMART'
19+
20+
/**
21+
* @type {object}
22+
*/
23+
this.subscriptions = {}
24+
25+
/**
26+
* @type {object}
27+
*/
28+
this.endpoints = {
29+
/**
30+
* URLs for product details on the Bitmart spot and contract markets.
31+
* @type {string[]}
32+
*/
33+
PRODUCTS: [
34+
'https://api-cloud.bitmart.com/spot/v1/symbols/details',
35+
'https://api-cloud.bitmart.com/contract/public/details'
36+
],
37+
SPOT: {
38+
RECENT_TRADES: 'https://api-cloud.bitmart.com/spot/quotation/v3/trades'
39+
},
40+
FUTURES: {
41+
RECENT_TRADES: ''
42+
}
43+
}
44+
}
45+
46+
async getUrl(pair) {
47+
if (this.specs[pair]) {
48+
return 'wss://openapi-ws.bitmart.com/api?protocol=1.1'
49+
}
50+
return 'wss://ws-manager-compress.bitmart.com/api?protocol=1.1'
51+
}
52+
53+
validateProducts(data) {
54+
if (!data.specs) {
55+
return false
56+
}
57+
58+
return true
59+
}
60+
61+
formatProducts(responses) {
62+
const products = []
63+
const specs = {}
64+
const types = {}
65+
66+
for (const response of responses) {
67+
for (const product of response.data.symbols) {
68+
products.push(product.symbol)
69+
70+
if (product.contract_size) {
71+
specs[product.symbol] = +product.contract_size
72+
}
73+
}
74+
}
75+
76+
return { products, specs, types }
77+
}
78+
79+
/**
80+
* Sub
81+
* @param {WebSocket} api
82+
* @param {string} pair
83+
*/
84+
async subscribe(api, pair) {
85+
if (!(await super.subscribe(api, pair))) {
86+
return
87+
}
88+
89+
const isContract = !!this.specs[pair]
90+
91+
const typeImpl = isContract
92+
? {
93+
prefix: 'futures',
94+
arg: 'action'
95+
}
96+
: {
97+
prefix: 'spot',
98+
arg: 'op'
99+
}
100+
101+
api.send(
102+
JSON.stringify({
103+
[typeImpl.arg]: `subscribe`,
104+
args: [`${typeImpl.prefix}/trade:${pair}`]
105+
})
106+
)
107+
108+
return true
109+
}
110+
111+
/**
112+
* Sub
113+
* @param {WebSocket} api
114+
* @param {string} pair
115+
*/
116+
async unsubscribe(api, pair) {
117+
if (!(await super.unsubscribe(api, pair))) {
118+
return
119+
}
120+
121+
const isContract = !!this.specs[pair]
122+
123+
const typeImpl = isContract
124+
? {
125+
prefix: 'futures',
126+
arg: 'action'
127+
}
128+
: {
129+
prefix: 'spot',
130+
arg: 'op'
131+
}
132+
133+
api.send(
134+
JSON.stringify({
135+
[typeImpl.arg]: `unsubscribe`,
136+
args: [`${typeImpl.prefix}/trade:${pair}`]
137+
})
138+
)
139+
140+
return true
141+
}
142+
143+
formatTrade(trade) {
144+
if (this.specs[trade.symbol]) {
145+
return {
146+
exchange: this.id,
147+
pair: trade.symbol,
148+
timestamp: trade.create_time_mill,
149+
price: +trade.deal_price,
150+
size:
151+
(trade.deal_vol * this.specs[trade.symbol]) /
152+
(this.specs[trade.symbol] > 1 ? trade.deal_price : 1),
153+
side: trade.way < 4 ? 'buy' : 'sell'
154+
}
155+
} else {
156+
return {
157+
exchange: this.id,
158+
pair: trade.symbol,
159+
timestamp: trade.s_t * 1000,
160+
price: +trade.price,
161+
size: +trade.size,
162+
side: trade.side
163+
}
164+
}
165+
}
166+
167+
onMessage(message, api) {
168+
let data = message.data
169+
170+
if (typeof data !== 'string') {
171+
data = inflateRaw(message.data, { to: 'string' })
172+
}
173+
174+
const json = JSON.parse(data)
175+
176+
if (!json || !json.data) {
177+
throw Error(`${this.id}: Can't parse json data from messageEvent`)
178+
}
179+
const trades = json.data.map(trade => this.formatTrade(trade))
180+
this.emitTrades(
181+
api.id,
182+
json.data.map(trade => this.formatTrade(trade))
183+
)
184+
}
185+
186+
getMissingTrades(range, totalRecovered = 0) {
187+
// limit to 50 trades and no way to go back further
188+
let endpoint =
189+
this.endpoints.SPOT.RECENT_TRADES + `?symbol=${range.pair.toUpperCase()}`
190+
191+
const mapResponseToTradeFormat = trade => {
192+
return {
193+
exchange: this.id,
194+
pair: trade[0],
195+
timestamp: trade[1],
196+
price: trade[2],
197+
size: trade[3],
198+
side: trade[4]
199+
}
200+
}
201+
return axios
202+
.get(endpoint)
203+
.then(response => {
204+
if (response.data.length) {
205+
const trades = response.data.map(trade =>
206+
mapResponseToTradeFormat(trade)
207+
)
208+
209+
this.emitTrades(null, trades)
210+
211+
totalRecovered += trades.length
212+
213+
console.log(
214+
`[${this.id}.recoverMissingTrades] +${trades.length} ${
215+
range.pair
216+
} (${getHms(remainingMissingTime)} remaining)`
217+
)
218+
}
219+
220+
return totalRecovered
221+
})
222+
.catch(err => {
223+
console.error(
224+
`[${this.id}] failed to get missing trades on ${range.pair}`,
225+
err.message
226+
)
227+
228+
return totalRecovered
229+
})
230+
}
231+
}
232+
233+
module.exports = Bitmart

0 commit comments

Comments
 (0)