MtGox support depends on ws4py (https://github.com/Lawouach/WebSocket-for-Python) and tornado (http://www.tornadoweb.org/en/stable/) so be sure to have those installed before moving forward.
PyAlgoTrade allows you to backtest and paper trade (backtest using a realtime feed) Bitcoin trading strategies through Mt. Gox (https://mtgox.com/).
In this tutorial we’ll first backtest a trading strategy using historical data, and later on we’ll test it using a realtime feed. Before we move on, this tutorial assumes that you’re already familiar with the basic concepts presented in the Tutorial section.
The first thing that we’ll need to test our strategy is some data. Let’s start by downloading trades for March 2013 using the following command:
python -c "from pyalgotrade.mtgox import tools; tools.download_trades_by_month('USD', 2013, 3, 'trades-mtgox-usd-2013-03.csv')"
The output should look like this:
2013-08-12 22:34:22,260 mtgox [INFO] Downloading trades since 2013-03-01 00:00:00+00:00.
2013-08-12 22:34:25,728 mtgox [INFO] Got 1000 trades.
2013-08-12 22:34:25,739 mtgox [INFO] Downloading trades since 2013-03-01 05:06:22.262840+00:00.
2013-08-12 22:34:27,581 mtgox [INFO] Got 1000 trades.
2013-08-12 22:34:27,594 mtgox [INFO] Downloading trades since 2013-03-01 09:03:12.939311+00:00.
2013-08-12 22:34:29,307 mtgox [INFO] Got 1000 trades.
2013-08-12 22:34:29,319 mtgox [INFO] Downloading trades since 2013-03-01 11:35:16.695161+00:00.
2013-08-12 22:34:30,954 mtgox [INFO] Got 1000 trades.
2013-08-12 22:34:30,966 mtgox [INFO] Downloading trades since 2013-03-01 15:55:48.855317+00:00.
2013-08-12 22:34:32,679 mtgox [INFO] Got 1000 trades.
2013-08-12 22:34:32,691 mtgox [INFO] Downloading trades since 2013-03-01 18:19:12.283606+00:00.
.
.
and it will take some time since Mt. Gox API returns no more than 1000 trades on each request and there are about 324878 trades. The CSV file will have 4 columns:
- The trade identifier (which is in fact the trade timestamp in microseconds).
- The price.
- The amount of bitcoin traded.
- If the trade is the result of the execution of a bid or an ask.
For this tutorial we’ll use a Bitcoin Scalper strategy inspired in http://nobulart.com/bitcoin/blog/bitcoin-scalper-part-1/ . As explained in that webpage, the general idea is to place a bid order a fixed percentage below the current market price. Once a bid order is filled, it is transitioned to the held state. It will remain held until any one of the following three conditions are met:
- The commit price is met
- The stop loss is triggered
- The maximum hold period is exceeded
Depending on which condition was met, we’ll exit with a Market or a Limit order.
Save the following code as mtgox_scalper.py.
import datetime
from pyalgotrade import strategy
from pyalgotrade.technical import roc
from pyalgotrade.technical import stats
# This strategy is inspired on: http://nobulart.com/bitcoin/blog/bitcoin-scalper-part-1/
#
# Possible states and transitions:
# NoPos -> WaitEntry
# WaitEntry -> LongPos | NoPos
# LongPos -> WaitExitLimit | WaitExitMarket
# WaitExitLimit -> WaitExitMarket | NoPos
# WaitExitMarket -> NoPos
class Strategy(strategy.BaseStrategy):
def __init__(self, instrument, feed, brk):
strategy.BaseStrategy.__init__(self, feed, brk)
self.__verbosityLevel = 1
self.__instrument = instrument
self.__orderSize = 0.2
self.__targetPricePct = 0.015
self.__commitPricePct = self.__targetPricePct / 2
self.__stopLossPct = -0.02
# Time to wait for BUY order to get filled.
self.__maxWaitEntry = datetime.timedelta(minutes=3)
# Maximum holding period.
self.__maxHoldPeriod = datetime.timedelta(hours=1)
volatilityPeriod = 5 # How many returns to use to calculate volatility.
self.returnsVolatility = stats.StdDev(roc.RateOfChange(feed[self.__instrument].getCloseDataSeries(), 1), volatilityPeriod)
self.__switchNoPos()
def setVerbosityLevel(self, level):
self.__verbosityLevel = level
def __log(self, level, *elements):
if level >= self.__verbosityLevel:
print " ".join([str(element) for element in elements])
def __switchNoPos(self):
self.__stateFun = self.__onNoPos
self.__position = None
self.__commitPrice = None
self.__targetPrice = None
self.__deadline = None
def __exitWithMarketOrder(self, bars):
# Exit with a market order at the target price and switch to WaitExitMarket
self.__log(1, bars.getDateTime(), "SELL (Market order)")
self.__position.exit()
self.__stateFun = self.__onWaitExitMarket
# Calculate the bid price based on the current price and the volatility.
def __getBidPrice(self, currentPrice):
vol = self.returnsVolatility[-1]
if vol != None and vol > 0.006:
return currentPrice * 0.98
return None
def onEnterOk(self, position):
assert(self.__position == position)
assert(self.__stateFun == self.__onWaitEntry)
self.__log(1, position.getEntryOrder().getExecutionInfo().getDateTime(), "BUY filled at", position.getEntryOrder().getExecutionInfo().getPrice())
# Switch to LongPos
self.__deadline = position.getEntryOrder().getExecutionInfo().getDateTime() + self.__maxHoldPeriod
self.__stateFun = self.__onLongPos
def onEnterCanceled(self, position):
assert(self.__position == position)
assert(self.__stateFun == self.__onWaitEntry)
self.__log(1, "BUY canceled.")
# Switch to NoPos
self.__switchNoPos()
def onExitOk(self, position):
assert(self.__position == position)
assert(self.__stateFun in (self.__onWaitExitLimit, self.__onWaitExitMarket))
self.__log(1, position.getExitOrder().getExecutionInfo().getDateTime(), "SELL filled. %", position.getReturn())
# Switch to NoPos
self.__switchNoPos()
def onExitCanceled(self, position):
assert(self.__position == position)
assert(self.__stateFun in (self.__onWaitExitLimit, self.__onWaitExitMarket))
self.__log(1, "SELL canceled. Resubmitting as market order.")
# If the exit was canceled, re-submit it as a market order.
self.__position.exit()
self.__stateFun = self.__onWaitExitMarket
def __waitingPeriodExceeded(self, currentDateTime):
assert(self.__deadline != None)
return currentDateTime >= self.__deadline
def __stopLoss(self, currentPrice):
assert(self.__position != None)
return self.__position.getUnrealizedReturn(currentPrice) <= self.__stopLossPct
# NoPos: A position is not opened.
def __onNoPos(self, bars):
assert(self.__position == None)
assert(self.__commitPrice == None)
assert(self.__targetPrice == None)
currentPrice = bars[self.__instrument].getClose()
bidPrice = self.__getBidPrice(currentPrice)
if bidPrice != None:
self.__commitPrice = bidPrice * (1 + self.__commitPricePct)
self.__targetPrice = bidPrice * (1 + self.__targetPricePct)
# EnterLong and switch state to WaitEntry
self.__log(1, bars.getDateTime(), "BUY (ask: %s commit: %s target: %s)" % (bidPrice, self.__commitPrice, self.__targetPrice))
self.__position = self.enterLongLimit(self.__instrument, bidPrice, self.__orderSize, True)
self.__stateFun = self.__onWaitEntry
self.__deadline = bars.getDateTime() + self.__maxWaitEntry
# WaitEntry: Waiting for the entry order to get filled.
def __onWaitEntry(self, bars):
assert(self.__position != None)
assert(not self.__position.entryFilled())
if self.__waitingPeriodExceeded(bars.getDateTime()):
# Cancel the entry order. This should eventually take us back to NoPos.
self.__log(1, bars.getDateTime(), "Waiting period exceeded. Cancel entry")
self.__position.cancelEntry()
# LongPos: In a long position.
def __onLongPos(self, bars):
assert(self.__position != None)
assert(self.__commitPrice != None)
assert(self.__targetPrice != None)
currentPrice = bars[self.__instrument].getClose()
# If the holding perios is exceeded, we exit with a market order.
if self.__waitingPeriodExceeded(bars.getDateTime()):
self.__log(1, bars.getDateTime(), "Holding period exceeded.")
self.__exitWithMarketOrder(bars)
elif self.__stopLoss(currentPrice):
self.__log(1, bars.getDateTime(), "Stop loss.")
self.__exitWithMarketOrder(bars)
elif currentPrice >= self.__commitPrice:
# Exit with a limit order at the target price and switch to WaitExitLimit
self.__log(1, bars.getDateTime(), "SELL (%s)" % (self.__targetPrice))
self.__position.exit(self.__targetPrice)
self.__stateFun = self.__onWaitExitLimit
# WaitExitLimit: Waiting for the sell limit order to get filled.
def __onWaitExitLimit(self, bars):
assert(self.__position != None)
if self.__position.exitActive():
currentPrice = bars[self.__instrument].getClose()
if self.__stopLoss(currentPrice):
self.__log(1, bars.getDateTime(), "Stop loss. Canceling SELL (Limit order).")
self.__position.cancelExit()
else:
self.__exitWithMarketOrder()
# WaitExitMarket: Waiting for the sell market order to get filled.
def __onWaitExitMarket(self, bars):
assert(self.__position != None)
def onBars(self, bars):
self.__log(0, bars.getDateTime(), "Price:", bars[self.__instrument].getClose(), "Volume:", bars[self.__instrument].getVolume(), "Volatility:", self.returnsVolatility[-1])
self.__stateFun(bars)
and use the following code to run the mtgox_scalper.py strategy with the bars we just downloaded:
from pyalgotrade import plotter
from pyalgotrade.mtgox import barfeed
from pyalgotrade.mtgox import broker
import mtgox_scalper
def main(plot):
# Load the trades from the CSV file
print "Loading bars"
feed = barfeed.CSVTradeFeed()
feed.addBarsFromCSV("trades-mtgox-usd-2013-03.csv")
# Create a backtesting broker.
brk = broker.BacktestingBroker(200, feed)
# Run the strategy with the feed and the broker.
print "Running strategy"
strat = mtgox_scalper.Strategy("BTC", feed, brk)
if plot:
plt = plotter.StrategyPlotter(strat, plotBuySell=False)
plt.getOrCreateSubplot("volatility").addDataSeries("Volatility", strat.returnsVolatility)
strat.run()
print "Result: %.2f" % strat.getResult()
if plot:
plt.plot()
if __name__ == "__main__":
main(True)
If you run the script you should see something like this:
Loading bars
Running strategy
2013-03-01 01:03:01.723215+00:00 BUY (ask: 32.7275508 commit: 32.973007431 target: 33.218464062)
2013-03-01 01:07:07.577842+00:00 Waiting period exceeded. Cancel entry
BUY canceled.
2013-03-01 01:07:10.018437+00:00 BUY (ask: 32.4675176 commit: 32.711023982 target: 32.954530364)
2013-03-01 01:10:17.828689+00:00 Waiting period exceeded. Cancel entry
BUY canceled.
2013-03-01 03:54:44.713943+00:00 BUY (ask: 32.89958 commit: 33.14632685 target: 33.3930737)
2013-03-01 03:59:14.375401+00:00 Waiting period exceeded. Cancel entry
.
.
.
2013-03-31 19:18:18.430133+00:00 BUY (ask: 91.1302 commit: 91.8136765 target: 92.497153)
2013-03-31 19:21:29.664933+00:00 Waiting period exceeded. Cancel entry
BUY canceled.
2013-03-31 21:47:23.413314+00:00 BUY (ask: 91.203161 commit: 91.8871847075 target: 92.571208415)
2013-03-31 21:50:44.928156+00:00 Waiting period exceeded. Cancel entry
BUY canceled.
2013-03-31 23:08:43.620930+00:00 BUY (ask: 90.3560294 commit: 91.0336996205 target: 91.711369841)
2013-03-31 23:13:07.495571+00:00 Waiting period exceeded. Cancel entry
BUY canceled.
Result: 221.01
Note that while this strategy seems profitable for March 2013, it may not be the case for other periods. The main point of this tutorial is to show how to build and run a strategy.
Now let’s run the same strategy but instead of using historical data we’ll use live data coming directly from MtGox:
from pyalgotrade.mtgox import client
from pyalgotrade.mtgox import barfeed
from pyalgotrade.mtgox import broker
import mtgox_scalper
def main():
# Create a client responsible for all the interaction with MtGox
cl = client.Client("USD", None, None)
# Create a real-time feed that will build bars from live trades.
feed = barfeed.LiveTradeFeed(cl)
# Create a backtesting broker.
brk = broker.BacktestingBroker(200, feed)
# Run the strategy with the feed and the broker.
strat = mtgox_scalper.Strategy("BTC", feed, brk)
# It is VERY important to add the client to the event dispatch loop before running the strategy.
strat.getDispatcher().addSubject(cl)
# This is just to get each bar printed.
strat.setVerbosityLevel(0)
strat.run()
if __name__ == "__main__":
main()
If you run the script you should see something like this:
2013-08-19 00:32:34,573 mtgox [INFO] Initializing MtGox client.
2013-08-19 00:32:35,198 mtgox [INFO] Connection opened.
2013-08-19 00:32:35,199 mtgox [INFO] Initialization ok.
2013-08-19 00:32:57.063971 Price: 116.95505 Volume: 0.01079 Volatility: None
2013-08-19 00:33:35.472846 Price: 116.95504 Volume: 0.199 Volatility: None
2013-08-19 00:33:51.604106 Price: 116.95503 Volume: 0.01033 Volatility: None
2013-08-19 00:33:59.452987 Price: 116.46002 Volume: 0.01 Volatility: None
2013-08-19 00:34:02.809658 Price: 116.46003 Volume: 0.01 Volatility: None
2013-08-19 00:34:14.537918 Price: 116.46003 Volume: 0.01 Volatility: 0.00169298408448
2013-08-19 00:34:18.911074 Price: 116.46004 Volume: 0.01 Volatility: 0.00169300122141
2013-08-19 00:34:48.579430 Price: 116.95506 Volume: 0.0108 Volatility: 0.00268257581563
2013-08-19 00:34:49.821193 Price: 116.46 Volume: 0.01 Volatility: 0.00268271049486
.
.
.