Package: aat
AATAsyncAlgoTradingaatis a framework for writing algorithmic trading strategies in python. It is designed to be modular and extensible, and is the core engine poweringAlgoCoin.It comes with support for live trading across (and between) multiple exchanges, fully integrated backtesting support, slippage and transaction cost modeling, and robust reporting and risk mitigation through manual and programatic algorithm controls.Like Zipline, the inspriation for this system,aatexposes a single strategy class which is utilized for both live trading and backtesting. The strategy class is simple enough to write and test algorithms quickly, but extensible enough to allow for complex slippage and transaction cost modeling, as well as mid- and post- trade analysis.Overviewaatis composed of 4 major parts.trading enginerisk management engineexecution enginebacktest engineTrading EngineThe trading engine initializes all exchanges and strategies, then martials data, trade requests, and trade responses between the strategy, risk, execution, and exchange objects, while keeping track of high-level statistics on the systemRisk Management EngineThe risk management engine enforces trading limits, making sure that stategies are limited to certain risk profiles. It can modify or remove trade requests prior to execution depending on user preferences and outstanding positions and orders.Execution engineThe execution engine is a simple passthrough to the underlying exchanges. It provides a unified interface for creating various types of orders.Backtest engineThe backtest engine provides the ability to run the same stragegy offline against historical data.Trading StrategyThe core element ofaatis the trading strategy interface. It is the union of theStrategyinterface, which provides methods to buy and sell, with theCallbackinterface, which provides callbacks in response to data. Users subclass this class in order to implement their strategiesCallbackclassCallback(metaclass=ABCMeta):@abstractmethoddefonTrade(self,data:MarketData):'''onTrade'''@abstractmethoddefonOpen(self,data:MarketData):'''onOpen'''@abstractmethoddefonFill(self,resp:TradeResponse):'''onFill'''@abstractmethoddefonCancel(self,data:MarketData):'''onCancel'''@abstractmethoddefonChange(self,data:MarketData):'''onChange'''@abstractmethoddefonError(self,data:MarketData):'''onError'''StrategyclassStrategy(metaclass=ABCMeta):@abstractmethoddefrequestBuy(self,callback:Callback,data:MarketData):'''requestBuy'''@abstractmethoddefrequestSell(self,callback:Callback,data:MarketData):'''requestSell'''Example StrategyHere is a simple trading strategy that buys once and holds.fromaat.strategyimportTradingStrategyfromaat.structsimportMarketData,TradeRequest,TradeResponsefromaat.enumsimportSide,OrderTypefromaat.loggingimportSTRATasslog,ERRORaselogclassBuyAndHoldStrategy(TradingStrategy):def__init__(self)->None:super(BuyAndHoldStrategy,self).__init__()self.bought=NonedefonFill(self,res:TradeResponse)->None:self.bought=reslog.info('d->g:bought%.2f@%.2f'%(res.volume,res.price))defonTrade(self,data:MarketData)->bool:ifself.boughtisNone:req=TradeRequest(side=Side.BUY,volume=1,instrument=data.instrument,order_type=OrderType.MARKET,exchange=data.exchange,price=data.price,time=data.time)log.info("requesting buy :%s",req)self.requestBuy(req)self.bought='pending'defonError(self,e)->None:elog.critical(e)defonChange(self,data:MarketData)->None:passdefonCancel(self,data:MarketData)->None:passdefonOpen(self,data:MarketData)->None:passTrading strategies have a number of required methods for handling messages:onTrade: Called when a trade occursonChange: Called when an order is modifiedonFill: Called when a strategy's trade executesonCancel: Called when an order is cancelledonError: Called when an error occursonOpen: Called when a new order occursThere are other optional callbacks for more granular processing:onStart: Called when the program startsonHalt: Called when trading is haltedonContinue: Called when trading continuesonExit: Called when the program shuts downThere are also several optional callbacks for backtesting:slippagetransactionCostonAnalyzecalled after trading engine has processed all data, used to visualize algorithm performanceSetting up and runningAn instance ofTradingStrategyclass is able to run live or against a set of historical trade/quote data. When instantiating aTradingEngineobject with aTradingEngineConfigobject, theTradingEngineConfighas atypewhich can be set to:live- live trading against the exchangesimulation- live trading against the exchange, but with order entry disabledsandbox- live trading against the exchange's sandbox instancebacktest- offline trading against historical OHLCV dataTo test our strategy in any mode, we will need to setup exchange keys to get historical data, stream market data, and make new orders.API KeysYou should creat API keys for exchanges you wish to trade on. For this example, we will assume a Coinbase Pro account with trading enabled. I usually put my keys in a set of shell scripts that are gitignored, so I don't post anything by accident. My scripts look something like:exportCOINBASE_API_KEY=...exportCOINBASE_API_SECRET=...exportCOINBASE_API_PASS=...Prior to running, I source the keys I need.SandboxesCurrently only the Gemini sandbox is supported, the other exchanges have discontinued theirs. To run in sandbox, setTradingEngineConfig.typeto Sandbox.Live TradingWhen you want to run live, setTradingEngineConfig.typeto Live. You will want to become familiar with the risk and execution engines, as these control things like max drawdown, max risk accrual, execution eagerness, etc.Simulation TradingWhen you want to run an algorithm live, but don't yet trust that it can make money, setTradingEngineConfig.typeto simulation. This will let it run against real money, but disallow order entry. You can then set things like slippage and transaction costs as you would in a backtest.TestingLet's make sure everything worked out by running a sample strategy (that doesnt make and trades!) on the Coinbase Pro exchange:python3-malgocoin--simulation--exchanges=coinbaseYou should see the following output:python3-malgocoin--simulation--exchanges=coinbase2019-06-0117:54:17,468CRITICAL--MainProcessparser.py:151--2019-06-0117:54:17,469CRITICAL--MainProcessparser.py:152--Simulationtrading2019-06-0117:54:17,469CRITICAL--MainProcessparser.py:153--2019-06-0117:54:34,570CRITICAL--MainProcesstrading.py:194--2019-06-0117:54:34,570CRITICAL--MainProcesstrading.py:195--Serverlisteningonport:80812019-06-0117:54:34,571CRITICAL--MainProcesstrading.py:196--2019-06-0117:54:34,998CRITICAL--MainProcessmarket_data.py:68--Startingalgotrading:ExchangeType.COINBASEConfigBecause there are a variety of options, a config file is generally more usable. Here is an example configuration for backtesting the Buy-and-hold strategy above on CoinbasePro:>catbacktest.cfg[general]verbose=1print=0TradingType=backtest[exchange]exchanges=coinbasecurrency_pairs=BTC/USD[strategy]strategies=aat.strategies.buy_and_hold.BuyAndHoldStrategy[risk]max_drawdown=100.0max_risk=100.0total_funds=10.0Analyzing an algorithmWe can run the above config by running:python3-malgocoin--config=./backtest.cfgWe should see the following output:2019-06-0117:58:40,173INFO--MainProcessutils.py:247--runninginverbosemode!2019-06-0117:58:40,174CRITICAL--MainProcessparser.py:165--2019-06-0117:58:40,174CRITICAL--MainProcessparser.py:166--Backtesting2019-06-0117:58:40,174CRITICAL--MainProcessparser.py:167--2019-06-0117:58:40,176CRITICAL--MainProcesstrading.py:106--Registeringstrategy:<class'aat.strategies.buy_and_hold.BuyAndHoldStrategy'>2019-06-0117:58:40,177INFO--MainProcessbacktest.py:25--Starting....2019-06-0117:58:41,338INFO--MainProcessbuy_and_hold.py:28--requestingbuy:<BTC/USD-Side.BUY:1.0@8567.06-OrderType.MARKET-ExchangeType.COINBASE>2019-06-0117:58:41,339INFO--MainProcessrisk.py:59--Requesting1.000000@8567.0600002019-06-0117:58:41,339INFO--MainProcessrisk.py:80--Riskcheckpassedforpartialorder:<BTC/USD-Side.BUY:1.0@8567.06-OrderType.MARKET-ExchangeType.COINBASE>2019-06-0117:58:41,339INFO--MainProcesstrading.py:244--Riskcheckpassed2019-06-0117:58:41,339INFO--MainProcesstrading.py:292--SlippageBT-<BTC/USD-Side.BUY:1.0@8567.916706-TradeResult.FILLED-ExchangeType.COINBASE>2019-06-0117:58:41,340INFO--MainProcesstrading.py:295--TXNcostBT-<BTC/USD-Side.BUY:1.0@8589.336497765-TradeResult.FILLED-ExchangeType.COINBASE>2019-06-0117:58:41,340INFO--MainProcessbuy_and_hold.py:14--d->g:bought1.00@8589.342019-06-0117:58:41,340INFO--MainProcessbacktest.py:42--<BTC/USD-1.29050038@8567.06-TickType.TRADE-ExchangeType.COINBASE>
...2019-06-0117:58:41,474INFO--MainProcessbacktest.py:42--<BTC/USD-2.35773043@8595.0-TickType.TRADE-ExchangeType.COINBASE>2019-06-0117:58:41,474INFO--MainProcessbacktest.py:33--Backtestdone,runninganalysis.This will call ouronAnalyzefunction, which in this case is implemented to plot some performance characteristics withmatplotlib.importpandasimportnumpyasnpimportmatplotlib,matplotlib.pyplotaspltimportseabornassnsmatplotlib.rc('font',**{'size':5})# extract data from trading engineportfolio_value=engine.portfolio_value()requests=engine.query().query_tradereqs()responses=engine.query().query_traderesps()trades=pandas.DataFrame([{'time':x.time,'price':x.price}forxinengine.query().query_trades(instrument=requests[0].instrument,page=None)])trades.set_index(['time'],inplace=True)# format into pandaspd=pandas.DataFrame(portfolio_value,columns=['time','value','pnl'])pd.set_index(['time'],inplace=True)# setup chartingsns.set_style('darkgrid')fig=plt.figure()ax1=fig.add_subplot(311)ax2=fig.add_subplot(312)ax3=fig.add_subplot(313)# plot algo performancepd.plot(ax=ax1,y=['value'],legend=False,fontsize=5,rot=0)# plot up/down chartpd['pos']=pd['pnl']pd['neg']=pd['pnl']pd['pos'][pd['pos']<=0]=np.nanpd['neg'][pd['neg']>0]=np.nanpd.plot(ax=ax2,y=['pos','neg'],kind='area',stacked=False,color=['green','red'],legend=False,linewidth=0,fontsize=5,rot=0)# annotate with key dataax1.set_title('Performance')ax1.set_ylabel('Portfolio value($)')forxyin[portfolio_value[0][:2]]+[portfolio_value[-1][:2]]:ax1.annotate('$%s'%xy[1],xy=xy,textcoords='data')ax3.annotate('$%s'%xy[1],xy=xy,textcoords='data')# plot trade intent/trade actionax3.set_ylabel('Intent/Action')ax3.set_xlabel('Date')ax3.plot(trades)ax3.plot([x.timeforxinrequestsifx.side==Side.BUY],[x.priceforxinrequestsifx.side==Side.BUY],'2',color='y')ax3.plot([x.timeforxinrequestsifx.side==Side.SELL],[x.priceforxinrequestsifx.side==Side.SELL],'1',color='y')ax3.plot([x.timeforxinresponsesifx.side==Side.BUY],# FIXME[x.priceforxinresponsesifx.side==Side.BUY],'^',color='g')ax3.plot([x.timeforxinresponsesifx.side==Side.SELL],# FIXME[x.priceforxinresponsesifx.side==Side.SELL],'v',color='r')# set same limitsy_bot,y_top=ax1.get_ylim()x_bot,x_top=ax1.get_xlim()ax3.set_ylim(y_bot,y_top)ax1.set_xlim(x_bot,x_top)ax2.set_xlim(x_bot,x_top)ax3.set_xlim(x_bot,x_top)dif=(x_top-x_bot)*.01ax1.set_xlim(x_bot-dif,x_top+dif)ax2.set_xlim(x_bot-dif,x_top+dif)ax3.set_xlim(x_bot-dif,x_top+dif)plt.show()We can see that our algorithm also implementedslippageandtransactionCost, resulting in a worse execution price:defslippage(self,resp:TradeResponse)->TradeResponse:slippage=resp.price*.0001# .01% price impactifresp.side==Side.BUY:# price moves against (up)resp.slippage=slippageresp.price+=slippageelse:# price moves against (down)resp.slippage=-slippageresp.price-=slippagereturnrespdeftransactionCost(self,resp:TradeResponse)->TradeResponse:txncost=resp.price*resp.volume*.0025# gdax is 0.0025 max feeifresp.side==Side.BUY:# price moves against (up)resp.transaction_cost=txncostresp.price+=txncostelse:# price moves against (down)resp.transaction_cost=-txncostresp.price-=txncostreturnrespExtendingApart from writing new strategies, this library can be extended by adding new exchanges. These are pretty simple. For cryptocurrency exchanges, I rely heavily onccxt,asyncio, and websocket libraries.ExampleHere is the coinbase exchange. Most of the code is to manage different websocket subscription options, and to convert betweenaat,ccxtand exchange-specific formatting of things like symbols, order types, etc.classCoinbaseExchange(Exchange):@lru_cache(None)defsubscription(self):return[json.dumps({"type":"subscribe","product_id":x.value[0].value+'-'+x.value[1].value})forxinself.options().currency_pairs]@lru_cache(None)defheartbeat(self):returnjson.dumps({"type":"heartbeat","on":True})deftickToData(self,jsn:dict)->MarketData:'''convert a jsn tick off the websocket to a MarketData struct'''ifjsn.get('type')=='received':returns=jsn.get('type').upper()reason=jsn.get('reason','').upper()ifs=='MATCH'or(s=='DONE'andreason=='FILLED'):typ=TickType.TRADEelifsin('OPEN','DONE','CHANGE','HEARTBEAT'):ifreason=='CANCELED':typ=TickType.CANCELelifs=='DONE':typ=TickType.FILLelse:typ=TickType_from_string(s.upper())else:typ=TickType.ERRORorder_id=jsn.get('order_id',jsn.get('maker_order_id',''))time=parse_date(jsn.get('time'))ifjsn.get('time')elsedatetime.now()iftypin(TickType.CANCEL,TickType.OPEN):volume=float(jsn.get('remaining_size','nan'))else:volume=float(jsn.get('size','nan'))price=float(jsn.get('price','nan'))currency_pair=str_to_currency_pair_type(jsn.get('product_id'))iftyp!=TickType.ERRORelsePairType.NONEinstrument=Instrument(underlying=currency_pair)order_type=str_to_order_type(jsn.get('order_type',''))side=str_to_side(jsn.get('side',''))remaining_volume=float(jsn.get('remaining_size',0.0))sequence=int(jsn.get('sequence',-1))ret=MarketData(order_id=order_id,time=time,volume=volume,price=price,type=typ,instrument=instrument,remaining=remaining_volume,side=side,exchange=self.exchange(),order_type=order_type,sequence=sequence)returnret