Written by Khang Nguyen Vo, khangvo88@gmail.com, for the RobustTechHouse blog. Khang is a graduate from the Masters of Quantitative and Computational Finance Program, John Von Neumann Institute 2014. He is passionate about research in machine learning, predictive modeling and backtesting of trading strategies.

INTRODUCTION

Bitcoin (or BTC) was invented by Japanese Satoshi Nakamoto and considered the first decentralized digital currency or crypto-currency. In this article, we experiment with a simple momentum based trading strategy for Bitcoin using PyAlgoTrade which is a Python Backtesting library. The Moving Average Crossover trading strategy we start with is defined as:

  • Enter position:

    • Long when MA10 > MA20
    • Short when MA10 < MA20
  • Exit position:
    • reverse trend
    • Take profit when we gain $20
    • Cut loss when we lose $10

MA10 refers to 10 day moving average price and MA20 refers 20 day moving average price.

DATA

The bitcoin data can be obtained from Bitcoin charts. The raw data of this source is at minute based sampling frequency and we group the data to 15-minutes prices as follows:

BITCOIN TRADING STRATEGY BACKTEST WITH PYALGOTRADE

PyAlgoTrade, as mentioned in previous blog, is an event-driven library. So we must override the basic events onEnterOk and onExitOk, which are raised when orders submitted before are successfully filled.

import Momentum.MyBaseStrategy as bstr  #extend from pyalgotrade.BacktestStrategy
from pyalgotrade.technical import ma
from pyalgotrade.barfeed import csvfeed
from pyalgotrade.bar import Frequency
from pyalgotrade import plotter
import numpy as np
import datetime

class MyStrategy(bstr.MyBTStrategy):
    def __init__(self, feed, cash,sma):
        self.__instrument = 'Bitcoin'
        bstr.MyBTStrategy.__init__(self,feed,cash, instrument=self.__instrument)
        self.MAXPROFIT = 20; self.STOPLOSS = 10
        self.getBroker().setAllowNegativeCash(True)                        

        # using for trading signal
        self.__position = None
        self.__price = feed[self.__instrument].getCloseDataSeries()
        self.__sma10 = ma.SMA(self.__price,sma[0],maxLen=100000)
        self.__sma20 = ma.SMA(self.__price,sma[1],maxLen=100000)
        self.__lastPrice = 0 #last price. Use for take profit and cutloss
        self.__signal = 0 #1: buying, -1: selling, 0: no change
        self.__last_exit_time = None

    def onEnterOk(self, position):
        execInfo = position.getEntryOrder().getExecutionInfo()
        self.info("%s %d at VND %s" %(self.alert_message,execInfo.getQuantity(),
                                      ut.accountingFormat(execInfo.getPrice())))
        self.__lastPrice = execInfo.getPrice()
        self.record_detail_transaction(position)

    def onEnterCanceled(self, position):
        self.__position = None        

    def onExitOk(self, position):
        execInfo = position.getExitOrder().getExecutionInfo()
        self.info("%s %d at %s\n================================="
                  %(self.alert_message,
                    execInfo.getQuantity(),'{:11,.2f}'.format(execInfo.getPrice())))
        self.__position = None
        self.record_detail_transaction(position, False) # log detail for later analysis

    # run before onEnterOk and onExitOk
    def onOrderUpdated(self,order):
        pass

The main process of trading algorithm is in onBars, which is raised every time there is new record of time series. PyAlgoTrade feed the data series and put it in bars, on each time given. This mandatory method is implemented as follows:

. . .
    # main event to update trading strategy
    def onBars(self, bars):
        self.portfolio_values.append(self.getBroker().getEquity())
        if self.__sma20[-1] is None:
            return
        bar = bars[self.__instrument]                

        if self.__sma10[-1] > self.__sma20[-1]:   self.__signal = 1 # buying signal
        elif self.__sma10[-1] < self.__sma20[-1]: self.__signal =-1 # selling signal
        shares = 1

        if(self.__position) is None and self.__sma20[-2] is not None:
            # go into long position
            if self.__sma10[-1] > self.__sma20[-1] and self.__sma10[-2] <= self.__sma20[-2]:
                self.info("short SMA > long SMA. RAISE BUY SIGNAL")
                #shares = int(self.getBroker().getCash() * 0.9 / bar.getClose())
                self.__position = self.enterLong(self.__instrument,shares,False)
                self.alert_message='Long position'
                self.buy_signals.append(self.getCurrentDateTime())
            #short position
            elif self.__sma10[-1] < self.__sma20[-1] and self.__sma10[-2] >= self.__sma20[-2]:
                self.info("short SMA < long SMA. RAISE SELL SIGNAL")
                self.__position = self.enterShort(self.__instrument,shares,False)
                self.alert_message='Short position'
                self.sell_signals.append(self.getCurrentDateTime())
        elif self.__lastPrice is not None and self.getBroker().getPositions() != {}:
            pos = self.getBroker().getPositions()[self.__instrument]
            # take profit when we obtain >= $20
            if( np.sign(pos)*(bar.getClose() - self.__lastPrice) >= self.MAXPROFIT):
                self.alert_message = 'TAKE PROFIT'
                self.__position.exitMarket()
                self.__lastPrice = None
            # cut loss when we lose more than $10
            elif (np.sign(pos)*(self.__lastPrice - bar.getClose())) >= self.STOPLOSS:
                self.alert_message = 'STOP LOSS'
                self.__position.exitMarket()
                self.__lastPrice = None
            elif pos*self.__signal < 0:
                self.alert_message = "Reverse signal. TAKE PROFIT"
                self.__position.exitMarket()
                self.__lastPrice = None
                if self.__signal < 0:
                    self.sell_signals.append(self.getCurrentDateTime())
                else:
                    self.buy_signals.append(self.getCurrentDateTime())
                self.__last_exit_time = self.getCurrentDateTime()

Then the main script as follows:

    filename = '../btcUSD15m_2.csv'
    # TODO: change the date range
    firstDate = datetime.datetime(2014,1,1,0,0,0,0,pytz.utc)
    endDate = datetime.datetime(2014,3,31,0,0,0,0,pytz.utc)

    feed = csvfeed.GenericBarFeed(15*Frequency.MINUTE,pytz.utc,maxLen=100000)
    feed.setBarFilter(csvfeed.DateRangeFilter(firstDate,endDate))
    feed.addBarsFromCSV('Bitcoin', filename)    

    cash = 10 ** 3 # 1,000 USD
    myStrategy = MyStrategy(feed,cash,[12,30]) #short and long moving average
    plt = plotter.StrategyPlotter(myStrategy, True, False, True)
    myStrategy.run()

    myStrategy.printTradingPerformance()

The trading transaction detail of this strategy from Jan 2014 to Mar 2014 are as follows:

In this short time window, the Sharpe Ratio is indeed poor and only -1.9. Moreover, there are a total of 200 trades executed in 3 months, and most are unprofitable trades (132/200 trades = 66%). Therefore, we need to reduce the number of unprofitable trades.

TWEAKING BITCOIN TRADING STRATEGY BACKTEST

The problem might be that we are using a very short-length moving average window to calculate the change of trends, so the strategy is very sensitive to changes. Now we try a longer moving average window with MA(80,200) crossover

    myStrategy = MyStrategy(feed,cash,[80,200]) #short and long moving average
    plt = plotter.StrategyPlotter(myStrategy, True, False, True)
    myStrategy.run()

The result of this trading strategy as follows for the same period.

The summary result when running this strategy between 2013-2015

CHARTS IN 2013

CHARTS IN 2014

CHARTS IN 2015

We see that the trading performance is better now. The Sharpe ratio is larger than 0.5, and in 2014, the cumulative returns is as big as 33%. The length of Moving Average could be further optimized (data-mined!).

NOTE ON TRANSACTION COSTS

In real trading, it is mandatory to add commission rates or transaction costs. Usually, the transaction cost can be computed as the difference between ASK price and BID price (BID-ASK SPREAD) if market orders are used to buy or sell. In our data set, the average “bid ask spread” is about 0.11, so we set the cost of each transaction to BTC 0.11.

from pyalgotrade.broker import backtesting
feed = createFeed(firstDate, endDate)
strat3 = MyStrategy(feed,cash,[80,200]) #short and long moving average
strat3.getBroker().setCommission(backtesting.FixedPerTrade(0.11)) #t-cost per trade = $0.11
strat3.run()
strat3.printTradingPerformance()

Overall, the strategy is still profitable, though we have to be mindful that because BitCoin history is very short, so the statistical significance of the strategy is inconclusive. Note that we assume there are no broker transaction fees. In reality, usually this fee cost 0.7$ per trade.

BitCoin Trading Strategies BackTest With PyAlgoTrade的更多相关文章

  1. Basics of Algorithmic Trading: Concepts and Examples

    https://www.investopedia.com/articles/active-trading/101014/basics-algorithmic-trading-concepts-and- ...

  2. Algorithmic Trading[z]

    Algorithmic Trading has been a hot topic for equity/derivative trading over a decade. Many ibanks an ...

  3. Python金融行业必备工具

    有些国外的平台.社区.博客如果连接无法打开,那说明可能需要"科学"上网 量化交易平台 国内在线量化平台: BigQuant - 你的人工智能量化平台 - 可以无门槛地使用机器学习. ...

  4. [转]Introduction to Learning to Trade with Reinforcement Learning

    Introduction to Learning to Trade with Reinforcement Learning http://www.wildml.com/2018/02/introduc ...

  5. Introduction to Learning to Trade with Reinforcement Learning

    http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/ The academic ...

  6. OnePy--构建属于自己的量化回测框架

    本文主要记录我构建量化回测系统的学习历程. 被遗弃的项目:Chandlercjy/OnePy_Old 新更新中的项目:Chandlercjy/OnePy 目录 1. 那究竟应该学习哪种编程语言比较好呢 ...

  7. Should You Build Your Own Backtester?

    By Michael Halls-Moore on August 2nd, 2016 This post relates to a talk I gave in April at QuantCon 2 ...

  8. An Introduction to Stock Market Data Analysis with R (Part 1)

    Around September of 2016 I wrote two articles on using Python for accessing, visualizing, and evalua ...

  9. 数据集划分——train set, validate set and test set

    先扯点闲篇儿,直取干货者,可以点击这里. 我曾误打误撞的搞过一年多的量化交易,期间尝试过做价格和涨跌的预测,当时全凭一腔热血,拿到行情数据就迫不及待地开始测试各种算法. 最基本的算法是技术指标类型的, ...

随机推荐

  1. Windows Phone 推送通知的第四类推送

    在 MSDN 文档有关 Windows Phone 推送通知 有关推送的内容包含 Tile.Toast.Raw 这三种通知.这三种通知 的方式类似,运用的场合不同,这里不再赘述,它们的运行原理类似: ...

  2. oracle 快速批量插入复杂数据的内容

    最近迷上一种批量插入的方法,一句sql解决,将需要插入的数据用with as 的方式查出来,不管多么复杂的sql,都可以用临时表的方式查出来,然后直接插入,这样代码更加清晰 流程也简单 insert ...

  3. jquery 给表格tbody t 加事件

    jquery给所有td加事件 $('#erji_list_table').on('click','td', function(){ $('#yuan_content').slideToggle(&qu ...

  4. 为什么 Windows API 使用 stdcall 调用约定?

    作者:知乎用户链接:https://www.zhihu.com/question/31453641/answer/52001143来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...

  5. Mysql定时备份数据脚本

    项目集群搭建完成,数据库虽有做主从同步,但考虑到数据安全性,为了满足这个需求那么要每天对数据备份处理, 但每天手动进行备份处理太过于被动,而且白天用户访问,会有数据变化以及在备份时会影响服务器正常运行 ...

  6. [dts]TI-am437x dts

    imx6 可以参考http://blog.csdn.net/shengzhadon/article/details/49908439 参照文件: Documentation/devicetree/bi ...

  7. Python高级编程之生成器(Generator)与coroutine(三):coroutine与pipeline(管道)和Dataflow(数据流_

    原创作品,转载请注明出处:点我 在前两篇文章中,我们介绍了什么是Generator和coroutine,在这一篇文章中,我们会介绍coroutine在模拟pipeline(管道 )和控制Dataflo ...

  8. c经典算法

    1. 河内之塔 说明 河内之塔(Towers of Hanoi)是法国人M.Claus(Lucas)于1883年从泰国带至法国的,河内为越战时 北越的首都,即现在的胡志明市:1883年法国数学家 Ed ...

  9. 采样方法(Sampling Methods)

    1.基本采样算法(Basic Sampling Algorithms) 1.1.标准概率分布(Standard distributions) 1.2.拒绝采样(Rejection sampling) ...

  10. CONTEST23 2014xmoi邀请赛(被pj虐哭)

    http://www.luogu.org/contest/show?tid=23 表示被普及的虐瞎了T_T_T_T_T_T_T_TT_T_T_T_T_T_T_T_T 33名100多分滚粗 各种贪心神题 ...