import { ExchangeAuth } from '@m8bTypes' ; import ccxt , { Trade } from 'ccxt' ; import { ExchangeWrapper } from './exchange.wrapper' ; import { ExchangeErrorHandler } from './exchange.error' ; import { ExchangeConfig } from '@lib' ; import { sleep , logger } from '@util' ; import { ChaosXcg } from '../seed/chaos.exchange' ; const verbose = false ; export interface OrderOpts { symbol : string ; type : 'market' | 'limit' ; side : 'buy' | 'sell' ; amount : number ; price ? : number ; } export class ExchangeProvider { private exchanges : Map < string , ExchangeWrapper > = new Map ( ) ; private errorHandler = new ExchangeErrorHandler ( ) ; public async addExchange ( { auth , exchange } ) { if ( this . exchanges . has ( exchange ) ) return this . error ( `Exchange already added: ${ exchange }` ) ; if ( exchange === 'chaos' ) { const chaos = new ChaosXcg ( ) ; await chaos . connect ( { } ) ; this . exchanges . set ( exchange , new ExchangeWrapper ( exchange , chaos ) ) ; return true ; } if ( ccxt . exchanges . indexOf ( exchange ) === - 1 ) return this . error ( `Invalid exchange: ${ exchange }` ) ; const reqKeys = this . getReqCreds ( ( new ccxt [ exchange ] ( ) as ccxt . Exchange ) . describe ( ) . requiredCredentials ) ; if ( ! this . hasAllReqCreds ( auth , reqKeys ) ) return this . error ( `${ exchange } missing required credentials. Requires: ${ reqKeys . join ( ', ' ) }` ) ; const exchangeConnectionFn = ( opts ) => new ccxt [ exchange ] ( { ... auth , verbose , ... opts } ) ; this . exchanges . set ( exchange , new ExchangeWrapper ( exchange , exchangeConnectionFn ) ) ; return true ; } public async replaceExchange ( exchangeConfig ) { this . exchanges . delete ( exchangeConfig . exchange ) ; await this . addExchange ( exchangeConfig ) ; } public getSymbols ( exchange ) { return this . exchanges . get ( exchange ) . getSymbols ( ) ; } public getTrades ( exchange , symbol , since ) { const fn = ( ) => this . exchanges . get ( exchange ) . fetchTrades ( symbol , since ) ; return this . retry ( fn ) ; } public getMyTrades ( exchange , symbol ) { const fn = ( ) => this . exchanges . get ( exchange ) . fetchMyTrades ( symbol ) ; return this . retry ( fn ) ; } public getBalances ( exchange ) { const fn = ( ) => this . exchanges . get ( exchange ) . fetchBalance ( ) ; return this . retry ( fn ) ; } public getOrderbook ( exchange , symbol ) { const fn = ( ) => this . exchanges . get ( exchange ) . fetchOrderBook ( symbol ) ; return this . retry ( fn ) ; } public placeOrder ( exchangeName , { symbol , type , side , amount , price } ) { const fn = ( ) => this . exchanges . get ( exchangeName ) . createOrder ( symbol , type , side , amount , price ) ; return this . retry ( fn ) ; } public checkOrder ( exchange , orderId , symbol ) { const fn = ( ) => this . exchanges . get ( exchange ) . checkOrder ( orderId , symbol ) ; return this . retry ( fn ) ; } public cancelOrder ( exchange , orderId , symbol ) { const fn = ( ) => this . exchanges . get ( exchange ) . cancelOrder ( orderId , symbol ) ; return this . retry ( fn ) ; } public fetchTicker ( exchange , symbol ) { const fn = ( ) => this . exchanges . get ( exchange ) . fetchTicker ( symbol ) ; return this . retry ( fn ) ; } public getScan ( exchange ) { return this . exchanges . get ( exchange ) . scan ; } public getTradeCursor ( exchange , trade ) { return this . exchanges . get ( exchange ) . getTradeCursor ( trade ) ; } public amountToPrecision ( amount ) { return Math . floor ( amount * 100000000 ) / 100000000 ; } public priceToPrecision ( exchange , symbol , price ) { return this . exchanges . get ( exchange ) . priceToPrecision ( symbol , price ) ; } public getLimits ( exchangeName , symbol ) { return this . exchanges . get ( exchangeName ) . getLimits ( symbol ) ; } private async retry < T > ( fn , attempt = 0 ) { if ( attempt > 5 ) return ; try { return await fn ( ) ; } catch ( e ) { if ( this . errorHandler . catch ( e ) ) { const backoff = this . getExponentialBackoff ( attempt ) ; await sleep ( backoff ) ; return this . retry ( fn , attempt + 1 ) ; } } } private getExponentialBackoff ( attempt ) { return Array ( attempt ) . fill ( 3 ) . reduce ( ( acc , curr ) => acc * curr , 50 ) ; } private getReqCreds ( requiredCredentials ) { return Object . entries ( requiredCredentials ) . filter ( ( [ key , value ] ) => value ) . map ( ( [ key ] ) => key ) ; } private hasAllReqCreds ( auth , reqKeys ) { return reqKeys . filter ( ( key ) => Boolean ( auth [ key ] ) ) . length === reqKeys . length ; } private error ( error ) { logger . error ( error ) ; return { error } ; } }