Skip to content

Commit

Permalink
Added the ability for the backtester to use unrealised PnL from the P…
Browse files Browse the repository at this point in the history
…osition objects to calculate a tick-by-tick equity curve. Added a performance directory that calculates drawdown statistics. Modified the output.py script to use Seaborn and output the equity curve, returns and drawdown curve.
  • Loading branch information
mhallsmoore committed May 15, 2015
1 parent a03bc7a commit 4380200
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 28 deletions.
5 changes: 4 additions & 1 deletion backtest/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -70,7 +71,9 @@ def backtest(
)

# 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()
Expand Down
34 changes: 29 additions & 5 deletions backtest/output.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,11 +20,29 @@
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()

# 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()
Empty file added performance/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions performance/performance.py
Original file line number Diff line number Diff line change
@@ -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()
71 changes: 58 additions & 13 deletions portfolio/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
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


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
Expand All @@ -23,9 +25,10 @@ def __init__(
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
Expand Down Expand Up @@ -66,16 +69,61 @@ def close_position(self, currency_pair):
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
Expand Down Expand Up @@ -124,7 +172,4 @@ def execute_signal(self, signal_event):

order = OrderEvent(currency_pair, units, "market", side)
self.events.put(order)

print("Balance: %0.2f" % self.balance)
self.append_equity_row(time, self.balance)

2 changes: 2 additions & 0 deletions portfolio/position_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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")},
"EURUSD": {"bid": Decimal("1.07832"), "ask": Decimal("1.07847")}
}



# =====================================
# GBP Home Currency with GBP/USD traded
# =====================================
Expand Down
16 changes: 8 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion trading/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -63,7 +64,9 @@ def trade(events, strategy, portfolio, execution, heartbeat):
# 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
Expand Down

0 comments on commit 4380200

Please sign in to comment.