UNPKG

blinktrade

Version:

BlinkTrade client for node.js

449 lines (388 loc) 13.9 kB
/** * BlinkTradeJS SDK * (c) 2016-present BlinkTrade, Inc. * * This file is part of BlinkTradeJS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * @flow */ import os from 'os'; import nodeify from 'nodeify'; import { EventEmitter2 as EventEmitter } from 'eventemitter2'; import { generateRequestId } from './listener'; import type { Message, PromiseEmitter, BlinkTradeWSParams, BlinkTradeWSTransport, MarketDataParams, OrderBookSync, } from './types'; import { EVENTS } from './constants/utils'; import { BALANCE, DEPOSIT_REFRESH, WITHDRAW_REFRESH, EXECUTION_REPORT, } from './constants/actionTypes'; import { ActionMsgReq, ActionMsgRes } from './constants/messages'; import { formatOrderBook, formatTradeHistory, } from './util/utils'; import TradeBase from './trade'; import WebSocketTransport from './transports/websocket'; import { IS_NODE } from './transports/transport'; class BlinkTradeWS extends TradeBase { /** * Session to store login information */ session: Object; orderbook: OrderBookSync; syncReqId: number; isOrderBookSynced: boolean; transport: BlinkTradeWSTransport; constructor(params?: BlinkTradeWSParams = {}) { super(params); this.transport = params.transport || new WebSocketTransport(params); this.session = {}; this.orderbook = {}; this.isOrderBookSynced = false; this.syncReqId = 0; } connect(callback?: Function) { return this.emitterPromise(this.transport.connect(callback)); } disconnect() { return this.transport.disconnect(); } send(msg: Message): Promise<Object> { return this.transport.sendMessageAsPromise(msg); } on(event: string, callback: ?Function) { if (this.transport.eventEmitter) { return this.transport.eventEmitter.on(event, callback); } } emit(event: string, data: Object) { if (this.transport.eventEmitter) { return this.transport.eventEmitter.emit(event, data); } } emitterPromise(promise: Promise<Object> & Object, callback?: Function) { return this.transport.emitterPromise ? this.transport.emitterPromise(promise, callback) : promise; } heartbeat(callback?: Function): Promise<Object> { const d = new Date(); const msg: Message = { MsgType: ActionMsgReq.HEARTBEAT, TestReqID: d.getTime(), SendTime: d.getTime(), }; return nodeify.extend(new Promise((resolve, reject) => { return this.send(msg).then(data => { return resolve({ ...data, Latency: new Date(Date.now()) - data.SendTime, }); }).catch(reject); })).nodeify(callback); } login({ username, password, secondFactor, cancelOnDisconnect, brokerId, ...extraData }: { username: string, password: string, secondFactor?: string, brokerId?: number, cancelOnDisconnect?: boolean, }, callback?: Function): Promise<Object> { let userAgent; if (!IS_NODE) { userAgent = { UserAgent: window.navigator.userAgent, UserAgentLanguage: window.navigator.language, UserAgentPlatform: window.navigator.platform, UserAgentTimezoneOffset: new Date().getTimezoneOffset(), }; } else { userAgent = { UserAgent: `${os.type()} ${os.release()}`, UserAgentLanguage: 'en_US', UserAgentPlatform: `${os.platform()} (${os.arch()})`, UserAgentTimezoneOffset: new Date().getTimezoneOffset(), }; } const msg: Message = { MsgType: ActionMsgReq.LOGIN, UserReqID: generateRequestId(), BrokerID: brokerId || this.brokerId, Username: username, Password: password, UserReqTyp: '1', CancelOnDisconnect: cancelOnDisconnect ? '1' : '0', ...userAgent, ...extraData, }; if (secondFactor) { msg.SecondFactor = secondFactor; } return nodeify.extend(new Promise((resolve, reject) => { return this.send(msg).then(data => { if (data.UserStatus === 1) { this.session = data; return resolve(data); } return reject(data); }).catch(reject); })).nodeify(callback); } logout(callback?: Function): Promise<Object> { const msg: Message = { MsgType: ActionMsgReq.LOGIN, BrokerID: this.brokerId, UserReqID: generateRequestId(), Username: this.session.Username, UserReqTyp: '2', }; return nodeify.extend(this.send(msg)).nodeify(callback); } profile(callback?: Function): Promise<Object> { const { VerificationData, ...profile } = this.session.Profile; return nodeify.extend(Promise.resolve(profile)).nodeify(callback); } balance(clientId?: string, callback?: Function): PromiseEmitter<Object> { return this.emitterPromise(new Promise((resolve, reject) => { return super.balance(clientId, callback).then((data) => { this.on(ActionMsgRes.BALANCE, (balance) => { callback && callback(null, balance); return this.emit(BALANCE, balance); }); return resolve(data); }).catch(reject); })); } onBalanceUpdate(callback: Function) { return this.on(ActionMsgRes.BALANCE, callback); } subscribeTicker(symbols: Array<string>, callback?: Function): PromiseEmitter<Object> { const msg: Message = { MsgType: ActionMsgReq.SECURITY_STATUS_SUBSCRIBE, SecurityStatusReqID: generateRequestId(), SubscriptionRequestType: '1', Instruments: symbols, }; const formatTicker = (data) => ({ ...data, SellVolume: data.SellVolume / 1e8, LowPx: data.LowPx / 1e8, LastPx: data.LastPx / 1e8, BestAsk: data.BestAsk / 1e8, HighPx: data.HighPx / 1e8, BuyVolume: data.BuyVolume / 1e8, BestBid: data.BestBid / 1e8, }); return this.emitterPromise(new Promise((resolve, reject) => { return this.send(msg).then(data => { const event = ActionMsgRes.SECURITY_STATUS_SUBSCRIBE + ':' + data.SecurityStatusReqID; this.on(event, (ticker) => { const tickerFormatted = formatTicker(ticker); callback && callback(null, tickerFormatted); return this.emit(`${ticker.Market}:${ticker.Symbol}`, tickerFormatted); }); return resolve(formatTicker(data)); }).catch(reject); }), callback); } unSubscribeTicker(SecurityStatusReqID: number): number { const msg: Message = { MsgType: ActionMsgReq.SECURITY_STATUS_SUBSCRIBE, SubscriptionRequestType: '2', SecurityStatusReqID, }; this.transport.sendMessage(msg); return SecurityStatusReqID; } subscribeOrderbook(options: MarketDataParams, callback?: Function): PromiseEmitter<Object> { console.warn('Warning: subscribeOrderbook is DEPRECATED, use subscribeMarketData instead'); return this.subscribeMarketData(options, callback); } subscribeMarketData(options: MarketDataParams, callback?: Function): PromiseEmitter<Object> { const msg: Message = { MsgType: ActionMsgReq.MD_FULL_REFRESH, MDReqID: generateRequestId(), SubscriptionRequestType: '1', MarketDepth: 0, MDUpdateType: '1', // Incremental refresh MDEntryTypes: ['0', '1'], BrokerID: this.brokerId, }; if (Array.isArray(options)) { msg.Instruments = options; } else { msg.Instruments = options.instruments; msg.MDEntryTypes = options.entryTypes || msg.MDEntryTypes; msg.MarketDepth = options.marketDepth || msg.MarketDepth; } if (options.columns) { msg.Columns = options.columns; } const level = (!Array.isArray(options) && typeof options.level !== 'undefined') ? options.level : this.level; const subscribeEvent = (data) => { if (data.MDBkTyp === '3') { data.MDIncGrp.map(order => { switch (order.MDEntryType) { case '0': case '1': const orderbookEvent = `OB:${EVENTS.ORDERBOOK[order.MDUpdateAction]}`; const bidOfferData = { ...order, MDReqID: data.MDReqID, type: orderbookEvent }; callback && callback(null, bidOfferData); return this.emit(orderbookEvent, bidOfferData); case '2': const tradeEvent = `OB:${EVENTS.TRADES[order.MDUpdateAction]}`; const tradeData = { ...order, type: tradeEvent }; callback && callback(null, tradeData); return this.emit(tradeEvent, tradeData); case '4': break; default: return null; } return null; }); } }; return this.emitterPromise(new Promise((resolve, reject) => { return this.send(msg).then(data => { this.on(ActionMsgRes.MD_INCREMENT + ':' + data.MDReqID, subscribeEvent); return resolve(formatOrderBook(data, level)); }).catch(err => reject(err)); }), callback); } syncOrderbook(options: MarketDataParams): Promise<Object> { if (!this.isOrderBookSynced) { this.isOrderBookSynced = true; const sides = { '0': 'bids', '1': 'asks' }; const instruments = Array.isArray(options) ? options : options.instruments; return this.subscribeMarketData({ instruments, level: 2 }) .on('OB:NEW_ORDER', (order) => { if (order.MDReqID === this.syncReqId) { const index = order.MDEntryPositionNo - 1; this.orderbook[order.Symbol][sides[order.MDEntryType]].splice(index, 0, order); } }).on('OB:UPDATE_ORDER', (order) => { if (order.MDReqID === this.syncReqId) { const index = order.MDEntryPositionNo - 1; this.orderbook[order.Symbol][sides[order.MDEntryType]].splice(index, 1, order); } }).on('OB:DELETE_ORDER', (order) => { if (order.MDReqID === this.syncReqId) { const index = order.MDEntryPositionNo - 1; this.orderbook[order.Symbol][sides[order.MDEntryType]].splice(index, 1); } }).on('OB:DELETE_ORDERS_THRU', (order) => { if (order.MDReqID === this.syncReqId) { const index = order.MDEntryPositionNo; this.orderbook[order.Symbol][sides[order.MDEntryType]].splice(0, index); } }).then((data) => { this.syncReqId = data.MDReqID; this.orderbook = data.MDFullGrp; return this.orderbook; }); } return Promise.resolve(this.orderbook); } unSubscribeOrderbook(MDReqID: number): number { const msg: Message = { MsgType: ActionMsgReq.MD_FULL_REFRESH, MDReqID, MarketDepth: 0, SubscriptionRequestType: '2', }; this.transport.sendMessage(msg); return MDReqID; } executionReport(callback?: Function): EventEmitter { return this.on(ActionMsgRes.EXECUTION_REPORT, (data) => { callback && callback(data); const event = EVENTS.EXECUTION_REPORT[data.ExecType]; return this.emit(`${EXECUTION_REPORT}:${event}`, data); }); } tradeHistory({ since, symbols, page: Page = 0, pageSize: PageSize = 100 }: { since?: number, symbols?: Array<string>, page?: number, pageSize?: number, } = {}, callback?: Function): Promise<Object> { const msg: Message = { MsgType: ActionMsgReq.TRADE_HISTORY, TradeHistoryReqID: generateRequestId(), Page, PageSize, }; if (symbols && symbols.length > 0) { msg.SymbolList = symbols; } if (since && typeof since === 'number') { msg.Since = since; } const format = formatTradeHistory(this.level); return nodeify.extend(this.send(msg).then(format)).nodeify(callback); } requestDeposit({ currency = 'BTC', value, depositMethodId }: { value?: number, currency?: string, depositMethodId?: number, } = {}, callback?: Function): PromiseEmitter<Object> { const subscribeEvent = (deposit) => { callback && callback(null, deposit); return this.emit(DEPOSIT_REFRESH, deposit); }; return this.emitterPromise(new Promise((resolve, reject) => { return super.requestDeposit({ currency, value, depositMethodId }).then(deposit => { const event = ActionMsgRes.DEPOSIT_REFRESH + ':' + deposit.ClOrdID; this.on(event, subscribeEvent); return resolve(deposit); }).catch(reject); }), callback); } onDepositRefresh(callback?: Function) { return this.on(ActionMsgRes.DEPOSIT_REFRESH, callback); } requestWithdraw({ amount, data, currency = 'BTC', method = 'bitcoin' }: { data: Object, amount: number, method?: string, currency?: string, }, callback?: Function): PromiseEmitter<Object> { const subscribeEvent = (withdraw) => { callback && callback(null, withdraw); return this.emit(WITHDRAW_REFRESH, withdraw); }; return this.emitterPromise(new Promise((resolve, reject) => { return super.requestWithdraw({ amount, data, currency, method }).then(withdraw => { this.on(ActionMsgRes.WITHDRAW_REFRESH + ':' + withdraw.ClOrdID, subscribeEvent); return resolve(withdraw); }).catch(reject); }), callback); } onWithdrawRefresh(callback?: Function) { return this.on(ActionMsgRes.WITHDRAW_REFRESH, callback); } } export default BlinkTradeWS;