molestiasconsectetur
Version:
Multi Exchange Crypto Currency Trading bot, Data Analysis Library and Strategy Back testing Engine
249 lines (224 loc) • 11 kB
JavaScript
const {Strategy} = require("./Strategy")
/**
* Class MarketMaker
*
<pre>
* This class is a simple implementation for a Market Maker Strategy
*
* This class is a little Different from all other strategy implementations as it is
* an independent execution strategy, Independent strategies signal to the BitFox Engine that this strategy does not require
* the Engine to manage Trade entries and exits it is handled from within the strategy itself.
*
* The MarketMaker strategy signal to the BitFox engine that is a independent strategy by always returning
* this.states.STATE_CONTEXT_INDEPENDENT state back to the calling engine.
*
* The MarketMakerStrategy is configured the following way
*
* 1. you can set a flag stepThrough this would indicate that you don't want to calculate a spread value
* to calculate entry end exit order but would like t step through the order book by a given number and se the values at the order book's index
* 2. If you don't want to stepThrough the entry and exit orders placed at the spread of the order book
* 3. The strategy allows you to set the accumulation mode i.e. whether you like to accumulate base or quote currency,
* in the case of base accumulation only short orders are prioritised,
* a sell order is placed, the strategy waits until the order is filled and only then places the buy-back order
* in the case of quote accumulation only long orders are prioritised,
* a buy order is placed, the strategy waits until the order is filled and only then places the sell-back order
*
*
</pre>
*/
class MarketMaker extends Strategy{
/**
* @typedef {Object} marketMakerExtras Engine configuration options
* @property {Number} stepThrough Strategy property, flag to indicate whether we should step through the order book
* @property {Number} steps the index pointing to the bids and asks prices in the order book to place sell/buy order
* @property {String} accumulate Strategy property, the accumulation preference base|quote
* @property {Number} spread Strategy property, a calculated distance between an ask and bid price
*
*/
/**
* @typedef {Object} marketMakerConfig Strategy configuration options
* @property {number} sidePreference Strategy property, the trading preference long|short/biDirectional
* @property {marketMakerExtras} strategyExtras Strategy property, strategy specific arguments for custom implementations
*/
/**
*
* @param args {marketMakerConfig} - Strategy configuration options
* @return {MarketMaker}
*/
static init(args) {
return new MarketMaker(args);
}
/**
*
* @param args {marketMakerConfig} - Strategy configuration options
*/
constructor(args) {
super(args);
this.setContext("MarketMaker")
this.stepThrough = this.custom.stepThrough || false;
this.steps = this.custom.steps || 4;
this.spread = this.custom.spread ? Number(this.custom.spread) : 0.01
this.symbol = args.symbol;
this.amount = args.amount;
this.accumulate = this.custom.accumulate || 'base'
this.exchange = null;
this.params= args;
this.startTracking = {entryFill:false,exitFill:false};
this.buyOrder = null;
this.sellOrder = null;
this.nextOrder = {};
}
/**
*
* @param {ExchangeService} exchange The client to allow us to make API requests to the exchange
*/
setExchange(exchange) {
this.exchange = exchange;
}
/**
*
* @param klineCandles {Array<Array<Number>>} Sets up the Strategy with Indicator Data and Historical Candle data
*/
async setup(klineCandles){
}
/**
*
* @return {Promise<MarketMaker>} Sets up the Exchange client and loads the market structure
*/
async setUpClient(){
await this.exchange.setUpClient(this.params.exchangeName,this.params);
return this;
}
/**
*
* @param {number} _index
* @param {boolean} isBackTest
* @param {ticker} ticker
* @return {Promise<{custom: {}, context: null, state, timestamp: number}>}
*/
async run(_index=0, isBackTest=false, ticker=null) {
try{
const orderBook = await this.exchange.fetchOrderBook(`${this.symbol}`, 25, {});
// Calculate the midpoint of the order book.
const midpoint = (orderBook.asks[0][0] + orderBook.bids[0][0]) / 2;
// Calculate the desired bid and ask prices, taking fees into consideration.
const bidPrice = !this.stepThrough ? (midpoint * (1 - this.spread / 2)) : orderBook.bids[this.steps-1][0];
const askPrice = !this.stepThrough ? (midpoint * (1 + this.spread / 2)) : orderBook.asks[this.steps-1][0];
if (this.noOrdersPlaced()) {
// Place orders at the calculated bid and ask prices.
// We don't want the traditional Market maker flow we want a more controlled flow where we can explicitly force direction towards accumulation
if (this.isBaseAccumulation()){
await this.handleBaseAccumulationEntryFlow(bidPrice, askPrice);
}else{
await this.handleQuoteAccumulationEntryFlow(bidPrice,askPrice);
}
return this.getStrategyResult(this.states.STATE_CONTEXT_INDEPENDENT, {}, )
}else{
// We at least One Order placed now we need track the order Status
if(this.startTracking.entryFill){
let id = this.isBaseAccumulation() ? this.sellOrder.id : this.buyOrder.id;
let order = await this.exchange.getFilledOrder(id, this.symbol,orderBook.asks[0][0]);
if(order.status === 'closed'){
// Send the next order
if(this.isBaseAccumulation()){
this.buyOrder = await this.nextOrder.apply();
this.eventHandler.fireEvent(`on${this.context}`,`Place Exit Buy Order for ${this.symbol} ${this.buyOrder.amount} @${this.buyOrder.price}`)
}else{
this.sellOrder = await this.nextOrder.apply();
this.eventHandler.fireEvent(`on${this.context}`,`Place Exit Sell Order for ${this.symbol} ${this.sellOrder.amount} @${this.sellOrder.price}`)
}
this.startTracking.entryFill = false;
this.startTracking.exitFill = true;
}
return this.getStrategyResult(this.states.STATE_CONTEXT_INDEPENDENT, {}, )
}
if(this.startTracking.exitFill){
let id = this.isBaseAccumulation() ? this.buyOrder.id : this.sellOrder.id;
let order = await this.exchange.getFilledOrder(id, this.symbol,orderBook.asks[0][0]);
if(order.status === 'closed'){
this.reset();
this.eventHandler.fireEvent(`on${this.context}`,`Strategy Context refreshed`)
}
return this.getStrategyResult(this.states.STATE_CONTEXT_INDEPENDENT, {}, )
}
}
return this.getStrategyResult(this.states.STATE_CONTEXT_INDEPENDENT, {}, )
}catch( e ){
console.log('Error:\n',e)
return this.getStrategyResult(this.states.STATE_CONTEXT_INDEPENDENT, {}, )
}
}
reset() {
this.startTracking.exitFill = false;
this.sellOrder = null;
this.buyOrder = null;
}
/**
*
* @param {Number} bidPrice the current identified ask price
* @param {Number} askPrice the current identified bid price
* @return {Promise<void>} executes a limit sell order and stores the sell order in the strategy scope,
* creates a callback function to execute a limit buy order soon as the sell order has ben filled
*/
async handleBaseAccumulationEntryFlow(bidPrice, askPrice) {
let me = this;
this.sellOrder = await this.exchange.limitSellOrder(this.symbol, this.amount, askPrice, {});
this.eventHandler.fireEvent(`on${this.context}`,`Placed Sell Order for:${this.amount} ${this.symbol} @${askPrice.toFixed(4)} Awaiting Order fill to place Buy Order for: ${me.getExitBuyOrderMount(bidPrice)} ${this.symbol} @${bidPrice.toFixed(4)}`)
this.nextOrder.apply = async function(){ return await me.exchange.limitBuyOrder(me.symbol, me.getExitBuyOrderMount(bidPrice), bidPrice, {});}
this.startTracking.entryFill = true;
}
/**
*
* @param {Number} bidPrice the current identified ask price
* @param {Number} askPrice the current identified bid price
* @return {Promise<void>} executes a limit buy order and stores the buy order in the strategy scope,
* creates a callback function to execute a limit sell order soon as the buy order has ben filled
*/
async handleQuoteAccumulationEntryFlow(bidPrice, askPrice) {
let me = this;
this.buyOrder = await this.exchange.limitBuyOrder(this.symbol, this.amount, askPrice, {});
this.eventHandler.fireEvent(`on${this.context}`,`Placed Buy Order for:${this.amount} ${this.symbol} @${bidPrice.toFixed(4)} Awaiting Order fill to Sell Order for: ${this.amount} ${this.symbol} @${askPrice.toFixed(4)}`)
this.nextOrder.apply = async function(){ return await me.exchange.limitSellOrder(me.symbol, me.amount, askPrice, {});}
this.startTracking.entryFill = true;
}
/**
*
* @return {boolean} helper function to determine if the current accumulation mode is quote currency
*/
isQuoteAccumulation(){return this.accumulate === 'quote'}
/**
*
* @return {boolean} helper function to determine if the current accumulation mode is base currency
*/
isBaseAccumulation(){return this.accumulate === 'base'}
/**
*
* @param bidPrice the current identified bidPrice
* @return {number} calculates an amount for a buy order
*/
getExitBuyOrderMount(bidPrice){
let spend = (this.sellOrder.price * this.sellOrder.amount);
return spend / bidPrice;
}
/**
*
* @param askPrice the current identified askPrice
* @return {number} calculates an amount for a sell order
*/
getExitSellOrderAmount(askPrice){
let spend = (this.buyOrder.price * this.sellOrder.amount);
return spend / askPrice;
}
/**
*
* @return {boolean} helper function to identify if this strategy has orders cached
*/
noOrdersPlaced() {
return this.sellOrder === null && this.buyOrder === null;
}
}
/**
*
* @type {{MarketMaker: MarketMaker}}
*/
module.exports = {MarketMaker: MarketMaker}