diff --git a/backtest/backtest.py b/backtest/backtest.py index 4749ed1..20e7bb1 100644 --- a/backtest/backtest.py +++ b/backtest/backtest.py @@ -39,6 +39,7 @@ def backtest( if event is not None: if event.type == 'TICK': strategy.calculate_signals(event) + portfolio.update_portfolio(event) elif event.type == 'SIGNAL': portfolio.execute_signal(event) elif event.type == 'ORDER': @@ -70,7 +71,9 @@ if __name__ == "__main__": ) # Create the portfolio object to track trades - portfolio = Portfolio(ticker, events, equity=equity) + portfolio = Portfolio( + ticker, events, equity=equity, backtest=True + ) # Create the simulated execution handler execution = SimulatedExecution() diff --git a/backtest/output.py b/backtest/output.py index aa4bf1a..60d1823 100644 --- a/backtest/output.py +++ b/backtest/output.py @@ -1,7 +1,13 @@ import os, os.path import pandas as pd +import matplotlib +try: + matplotlib.use('TkAgg') +except: + pass import matplotlib.pyplot as plt +import seaborn as sns from qsforex.settings import OUTPUT_RESULTS_DIR @@ -14,11 +20,29 @@ if __name__ == "__main__": It requires OUTPUT_RESULTS_DIR to be set in the project settings. """ + sns.set_palette("deep", desat=.6) + sns.set_context(rc={"figure.figsize": (8, 4)}) + equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv") equity = pd.io.parsers.read_csv( - equity_file, header=True, - names=["time", "balance"], - parse_dates=True, index_col=0 + equity_file, parse_dates=True, header=0, index_col=0 ) - equity["balance"].plot() - plt.show() \ No newline at end of file + + # Plot three charts: Equity curve, period returns, drawdowns + fig = plt.figure() + fig.patch.set_facecolor('white') # Set the outer colour to white + + # Plot the equity curve + ax1 = fig.add_subplot(311, ylabel='Portfolio value') + equity["Equity"].plot(ax=ax1, color=sns.color_palette()[0]) + + # Plot the returns + ax2 = fig.add_subplot(312, ylabel='Period returns') + equity['Returns'].plot(ax=ax2, color=sns.color_palette()[1]) + + # Plot the returns + ax3 = fig.add_subplot(313, ylabel='Drawdowns') + equity['Drawdown'].plot(ax=ax3, color=sns.color_palette()[2]) + + # Plot the figure + plt.show() diff --git a/performance/__init__.py b/performance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/performance/performance.py b/performance/performance.py new file mode 100644 index 0000000..c61d154 --- /dev/null +++ b/performance/performance.py @@ -0,0 +1,32 @@ +import numpy as np +import pandas as pd + + +def create_drawdowns(pnl): + """ + Calculate the largest peak-to-trough drawdown of the PnL curve + as well as the duration of the drawdown. Requires that the + pnl_returns is a pandas Series. + + Parameters: + pnl - A pandas Series representing period percentage returns. + + Returns: + drawdown, duration - Highest peak-to-trough drawdown and duration. + """ + + # Calculate the cumulative returns curve + # and set up the High Water Mark + hwm = [0] + + # Create the drawdown and duration series + idx = pnl.index + drawdown = pd.Series(index = idx) + duration = pd.Series(index = idx) + + # Loop over the index range + for t in range(1, len(idx)): + hwm.append(max(hwm[t-1], pnl[t])) + drawdown[t]= (hwm[t]-pnl[t]) + duration[t]= (0 if drawdown[t] == 0 else duration[t-1]+1) + return drawdown, drawdown.max(), duration.max() diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index d24408c..7b48035 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -7,6 +7,7 @@ import os import pandas as pd from qsforex.event.event import OrderEvent +from qsforex.performance.performance import create_drawdowns from qsforex.portfolio.position import Position from qsforex.settings import OUTPUT_RESULTS_DIR @@ -14,7 +15,8 @@ from qsforex.settings import OUTPUT_RESULTS_DIR class Portfolio(object): def __init__( self, ticker, events, home_currency="GBP", leverage=20, - equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02") + equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02"), + backtest=True ): self.ticker = ticker self.events = events @@ -23,9 +25,10 @@ class Portfolio(object): self.equity = equity self.balance = deepcopy(self.equity) self.risk_per_trade = risk_per_trade + self.backtest = backtest self.trade_units = self.calc_risk_position_size() self.positions = {} - self.equity = [] + self.backtest_file = self.create_equity_file() def calc_risk_position_size(self): return self.equity * self.risk_per_trade @@ -66,16 +69,61 @@ class Portfolio(object): del[self.positions[currency_pair]] return True - def append_equity_row(self, time, balance): - d = {"time": time, "balance": balance} - self.equity.append(d) + def create_equity_file(self): + filename = "backtest.csv" + out_file = open(os.path.join(OUTPUT_RESULTS_DIR, filename), "w") + header = "Timestamp,Balance" + for pair in self.ticker.pairs: + header += ",%s" % pair + header += "\n" + out_file.write(header) + if self.backtest: + print(header[:-2]) + return out_file def output_results(self): - filename = "equity.csv" - out_file = os.path.join(OUTPUT_RESULTS_DIR, filename) - df_equity = pd.DataFrame.from_records(self.equity, index='time') - df_equity.to_csv(out_file) - print("Simulation complete and results exported to %s" % filename) + # Closes off the Backtest.csv file so it can be + # read via Pandas without problems + self.backtest_file.close() + + in_filename = "backtest.csv" + out_filename = "equity.csv" + in_file = os.path.join(OUTPUT_RESULTS_DIR, in_filename) + out_file = os.path.join(OUTPUT_RESULTS_DIR, out_filename) + + # Create equity curve dataframe + df = pd.read_csv(in_file, index_col=0) + df.dropna(inplace=True) + df["Total"] = df.sum(axis=1) + df["Returns"] = df["Total"].pct_change() + df["Equity"] = (1.0+df["Returns"]).cumprod() + + # Create drawdown statistics + drawdown, max_dd, dd_duration = create_drawdowns(df["Equity"]) + df["Drawdown"] = drawdown + df.to_csv(out_file, index=True) + + print("Simulation complete and results exported to %s" % out_filename) + + def update_portfolio(self, tick_event): + """ + This updates all positions ensuring an up to date + unrealised profit and loss (PnL). + """ + currency_pair = tick_event.instrument + if currency_pair in self.positions: + ps = self.positions[currency_pair] + ps.update_position_price() + out_line = "%s,%s" % (tick_event.time, self.balance) + for pair in self.ticker.pairs: + if pair in self.positions: + out_line += ",%s" % self.positions[currency_pair].profit_base + else: + out_line += ",0.00" + out_line += "\n" + if self.backtest: + print(out_line[:-2]) + self.backtest_file.write(out_line) def execute_signal(self, signal_event): side = signal_event.side @@ -124,7 +172,4 @@ class Portfolio(object): order = OrderEvent(currency_pair, units, "market", side) self.events.put(order) - - print("Balance: %0.2f" % self.balance) - self.append_equity_row(time, self.balance) \ No newline at end of file diff --git a/portfolio/position_test.py b/portfolio/position_test.py index 9486298..f428a9b 100644 --- a/portfolio/position_test.py +++ b/portfolio/position_test.py @@ -11,6 +11,7 @@ class TickerMock(object): """ def __init__(self): + self.pairs = ["GBPUSD", "EURUSD"] self.prices = { "GBPUSD": {"bid": Decimal("1.50328"), "ask": Decimal("1.50349")}, "USDGBP": {"bid": Decimal("0.66521"), "ask": Decimal("0.66512")}, @@ -18,6 +19,7 @@ class TickerMock(object): } + # ===================================== # GBP Home Currency with GBP/USD traded # ===================================== diff --git a/requirements.txt b/requirements.txt index 3a3a758..a06ff14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ -argparse==1.2.1 -ipython==2.3.1 +ipython==3.1.0 matplotlib==1.4.3 mock==1.0.1 nose==1.3.6 -numpy==1.9.1 -pandas==0.15.2 +numpy==1.9.2 +pandas==0.16.1 pyparsing==2.0.3 -python-dateutil==2.4.0 +python-dateutil==2.4.2 pytz==2014.10 -requests==2.5.1 -scikit-learn==0.15.2 +requests==2.7.0 +scikit-learn==0.16.1 scipy==0.15.1 +seaborn==0.5.1 six==1.9.0 -wsgiref==0.1.2 +urllib3==1.10.4 diff --git a/trading/trading.py b/trading/trading.py index f2d38e7..d40a0cd 100644 --- a/trading/trading.py +++ b/trading/trading.py @@ -31,6 +31,7 @@ def trade(events, strategy, portfolio, execution, heartbeat): if event is not None: if event.type == 'TICK': strategy.calculate_signals(event) + portfolio.update_portfolio(event) elif event.type == 'SIGNAL': portfolio.execute_signal(event) elif event.type == 'ORDER': @@ -63,7 +64,9 @@ if __name__ == "__main__": # Create the portfolio object that will be used to # compare the OANDA positions with the local, to # ensure backtesting integrity. - portfolio = Portfolio(prices, events, equity=equity) + portfolio = Portfolio( + prices, events, equity=equity, backtest=False + ) # Create the execution handler making sure to # provide authentication commands