Merge pull request #12 from mhallsmoore/unrealised_pnl
Added the ability for the backtester to use unrealised PnL from the P…
This commit is contained in:
@@ -39,6 +39,7 @@ def backtest(
|
|||||||
if event is not None:
|
if event is not None:
|
||||||
if event.type == 'TICK':
|
if event.type == 'TICK':
|
||||||
strategy.calculate_signals(event)
|
strategy.calculate_signals(event)
|
||||||
|
portfolio.update_portfolio(event)
|
||||||
elif event.type == 'SIGNAL':
|
elif event.type == 'SIGNAL':
|
||||||
portfolio.execute_signal(event)
|
portfolio.execute_signal(event)
|
||||||
elif event.type == 'ORDER':
|
elif event.type == 'ORDER':
|
||||||
@@ -70,7 +71,9 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create the portfolio object to track trades
|
# 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
|
# Create the simulated execution handler
|
||||||
execution = SimulatedExecution()
|
execution = SimulatedExecution()
|
||||||
|
|||||||
+29
-5
@@ -1,7 +1,13 @@
|
|||||||
import os, os.path
|
import os, os.path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import matplotlib
|
||||||
|
try:
|
||||||
|
matplotlib.use('TkAgg')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
import seaborn as sns
|
||||||
|
|
||||||
from qsforex.settings import OUTPUT_RESULTS_DIR
|
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
|
It requires OUTPUT_RESULTS_DIR to be set in the project
|
||||||
settings.
|
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_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
|
||||||
equity = pd.io.parsers.read_csv(
|
equity = pd.io.parsers.read_csv(
|
||||||
equity_file, header=True,
|
equity_file, parse_dates=True, header=0, index_col=0
|
||||||
names=["time", "balance"],
|
|
||||||
parse_dates=True, 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()
|
||||||
|
|||||||
@@ -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()
|
||||||
+58
-13
@@ -7,6 +7,7 @@ import os
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from qsforex.event.event import OrderEvent
|
from qsforex.event.event import OrderEvent
|
||||||
|
from qsforex.performance.performance import create_drawdowns
|
||||||
from qsforex.portfolio.position import Position
|
from qsforex.portfolio.position import Position
|
||||||
from qsforex.settings import OUTPUT_RESULTS_DIR
|
from qsforex.settings import OUTPUT_RESULTS_DIR
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ from qsforex.settings import OUTPUT_RESULTS_DIR
|
|||||||
class Portfolio(object):
|
class Portfolio(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, ticker, events, home_currency="GBP", leverage=20,
|
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.ticker = ticker
|
||||||
self.events = events
|
self.events = events
|
||||||
@@ -23,9 +25,10 @@ class Portfolio(object):
|
|||||||
self.equity = equity
|
self.equity = equity
|
||||||
self.balance = deepcopy(self.equity)
|
self.balance = deepcopy(self.equity)
|
||||||
self.risk_per_trade = risk_per_trade
|
self.risk_per_trade = risk_per_trade
|
||||||
|
self.backtest = backtest
|
||||||
self.trade_units = self.calc_risk_position_size()
|
self.trade_units = self.calc_risk_position_size()
|
||||||
self.positions = {}
|
self.positions = {}
|
||||||
self.equity = []
|
self.backtest_file = self.create_equity_file()
|
||||||
|
|
||||||
def calc_risk_position_size(self):
|
def calc_risk_position_size(self):
|
||||||
return self.equity * self.risk_per_trade
|
return self.equity * self.risk_per_trade
|
||||||
@@ -66,16 +69,61 @@ class Portfolio(object):
|
|||||||
del[self.positions[currency_pair]]
|
del[self.positions[currency_pair]]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def append_equity_row(self, time, balance):
|
def create_equity_file(self):
|
||||||
d = {"time": time, "balance": balance}
|
filename = "backtest.csv"
|
||||||
self.equity.append(d)
|
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):
|
def output_results(self):
|
||||||
filename = "equity.csv"
|
# Closes off the Backtest.csv file so it can be
|
||||||
out_file = os.path.join(OUTPUT_RESULTS_DIR, filename)
|
# read via Pandas without problems
|
||||||
df_equity = pd.DataFrame.from_records(self.equity, index='time')
|
self.backtest_file.close()
|
||||||
df_equity.to_csv(out_file)
|
|
||||||
print("Simulation complete and results exported to %s" % filename)
|
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):
|
def execute_signal(self, signal_event):
|
||||||
side = signal_event.side
|
side = signal_event.side
|
||||||
@@ -124,7 +172,4 @@ class Portfolio(object):
|
|||||||
|
|
||||||
order = OrderEvent(currency_pair, units, "market", side)
|
order = OrderEvent(currency_pair, units, "market", side)
|
||||||
self.events.put(order)
|
self.events.put(order)
|
||||||
|
|
||||||
print("Balance: %0.2f" % self.balance)
|
|
||||||
self.append_equity_row(time, self.balance)
|
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ class TickerMock(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.pairs = ["GBPUSD", "EURUSD"]
|
||||||
self.prices = {
|
self.prices = {
|
||||||
"GBPUSD": {"bid": Decimal("1.50328"), "ask": Decimal("1.50349")},
|
"GBPUSD": {"bid": Decimal("1.50328"), "ask": Decimal("1.50349")},
|
||||||
"USDGBP": {"bid": Decimal("0.66521"), "ask": Decimal("0.66512")},
|
"USDGBP": {"bid": Decimal("0.66521"), "ask": Decimal("0.66512")},
|
||||||
@@ -18,6 +19,7 @@ class TickerMock(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# =====================================
|
# =====================================
|
||||||
# GBP Home Currency with GBP/USD traded
|
# GBP Home Currency with GBP/USD traded
|
||||||
# =====================================
|
# =====================================
|
||||||
|
|||||||
+8
-8
@@ -1,15 +1,15 @@
|
|||||||
argparse==1.2.1
|
ipython==3.1.0
|
||||||
ipython==2.3.1
|
|
||||||
matplotlib==1.4.3
|
matplotlib==1.4.3
|
||||||
mock==1.0.1
|
mock==1.0.1
|
||||||
nose==1.3.6
|
nose==1.3.6
|
||||||
numpy==1.9.1
|
numpy==1.9.2
|
||||||
pandas==0.15.2
|
pandas==0.16.1
|
||||||
pyparsing==2.0.3
|
pyparsing==2.0.3
|
||||||
python-dateutil==2.4.0
|
python-dateutil==2.4.2
|
||||||
pytz==2014.10
|
pytz==2014.10
|
||||||
requests==2.5.1
|
requests==2.7.0
|
||||||
scikit-learn==0.15.2
|
scikit-learn==0.16.1
|
||||||
scipy==0.15.1
|
scipy==0.15.1
|
||||||
|
seaborn==0.5.1
|
||||||
six==1.9.0
|
six==1.9.0
|
||||||
wsgiref==0.1.2
|
urllib3==1.10.4
|
||||||
|
|||||||
+4
-1
@@ -31,6 +31,7 @@ def trade(events, strategy, portfolio, execution, heartbeat):
|
|||||||
if event is not None:
|
if event is not None:
|
||||||
if event.type == 'TICK':
|
if event.type == 'TICK':
|
||||||
strategy.calculate_signals(event)
|
strategy.calculate_signals(event)
|
||||||
|
portfolio.update_portfolio(event)
|
||||||
elif event.type == 'SIGNAL':
|
elif event.type == 'SIGNAL':
|
||||||
portfolio.execute_signal(event)
|
portfolio.execute_signal(event)
|
||||||
elif event.type == 'ORDER':
|
elif event.type == 'ORDER':
|
||||||
@@ -63,7 +64,9 @@ if __name__ == "__main__":
|
|||||||
# Create the portfolio object that will be used to
|
# Create the portfolio object that will be used to
|
||||||
# compare the OANDA positions with the local, to
|
# compare the OANDA positions with the local, to
|
||||||
# ensure backtesting integrity.
|
# ensure backtesting integrity.
|
||||||
portfolio = Portfolio(prices, events, equity=equity)
|
portfolio = Portfolio(
|
||||||
|
prices, events, equity=equity, backtest=False
|
||||||
|
)
|
||||||
|
|
||||||
# Create the execution handler making sure to
|
# Create the execution handler making sure to
|
||||||
# provide authentication commands
|
# provide authentication commands
|
||||||
|
|||||||
Reference in New Issue
Block a user