Multi-day backtesting now supported.
This commit is contained in:
@@ -18,7 +18,7 @@ from qsforex.data.price import HistoricCSVPriceHandler
|
|||||||
|
|
||||||
def backtest(
|
def backtest(
|
||||||
events, ticker, strategy, portfolio,
|
events, ticker, strategy, portfolio,
|
||||||
execution, heartbeat, max_iters=200000
|
execution, heartbeat, max_iters=5000000
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Carries out an infinite while loop that polls the
|
Carries out an infinite while loop that polls the
|
||||||
@@ -29,12 +29,11 @@ def backtest(
|
|||||||
exceeded.
|
exceeded.
|
||||||
"""
|
"""
|
||||||
iters = 0
|
iters = 0
|
||||||
while True and iters < max_iters:
|
while iters < max_iters and ticker.continue_backtest:
|
||||||
ticker.stream_next_tick()
|
|
||||||
try:
|
try:
|
||||||
event = events.get(False)
|
event = events.get(False)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
ticker.stream_next_tick()
|
||||||
else:
|
else:
|
||||||
if event is not None:
|
if event is not None:
|
||||||
if event.type == 'TICK':
|
if event.type == 'TICK':
|
||||||
@@ -54,7 +53,7 @@ if __name__ == "__main__":
|
|||||||
events = queue.Queue()
|
events = queue.Queue()
|
||||||
equity = settings.EQUITY
|
equity = settings.EQUITY
|
||||||
|
|
||||||
# Load the historic CSV tick data files
|
# Load the historic CSV tick data filesw
|
||||||
pairs = ["GBPUSD"]
|
pairs = ["GBPUSD"]
|
||||||
csv_dir = settings.CSV_DATA_DIR
|
csv_dir = settings.CSV_DATA_DIR
|
||||||
if csv_dir is None:
|
if csv_dir is None:
|
||||||
|
|||||||
+71
-28
@@ -1,12 +1,16 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
from qsforex import settings
|
||||||
from qsforex.event.event import TickEvent
|
from qsforex.event.event import TickEvent
|
||||||
|
|
||||||
|
|
||||||
@@ -96,9 +100,32 @@ class HistoricCSVPriceHandler(PriceHandler):
|
|||||||
self.csv_dir = csv_dir
|
self.csv_dir = csv_dir
|
||||||
self.prices = self._set_up_prices_dict()
|
self.prices = self._set_up_prices_dict()
|
||||||
self.pair_frames = {}
|
self.pair_frames = {}
|
||||||
self._open_convert_csv_files()
|
self.file_dates = self._list_all_file_dates()
|
||||||
|
self.continue_backtest = True
|
||||||
|
self.cur_date_idx = 0
|
||||||
|
self.cur_date_pairs = self._open_convert_csv_files_for_day(
|
||||||
|
self.file_dates[self.cur_date_idx]
|
||||||
|
)
|
||||||
|
|
||||||
def _open_convert_csv_files(self):
|
def _list_all_csv_files(self):
|
||||||
|
files = os.listdir(settings.CSV_DATA_DIR)
|
||||||
|
pattern = re.compile("[A-Z]{6}_\d{8}.csv")
|
||||||
|
matching_files = [f for f in files if pattern.search(f)]
|
||||||
|
matching_files.sort()
|
||||||
|
return matching_files
|
||||||
|
|
||||||
|
def _list_all_file_dates(self):
|
||||||
|
"""
|
||||||
|
Removes the pair, underscore and '.csv' from the
|
||||||
|
dates and eliminates duplicates. Returns a list
|
||||||
|
of date strings of the form "YYYYMMDD".
|
||||||
|
"""
|
||||||
|
csv_files = self._list_all_csv_files()
|
||||||
|
de_dup_csv = list(set([d[7:-4] for d in csv_files]))
|
||||||
|
de_dup_csv.sort()
|
||||||
|
return de_dup_csv
|
||||||
|
|
||||||
|
def _open_convert_csv_files_for_day(self, date_str):
|
||||||
"""
|
"""
|
||||||
Opens the CSV files from the data directory, converting
|
Opens the CSV files from the data directory, converting
|
||||||
them into pandas DataFrames within a pairs dictionary.
|
them into pandas DataFrames within a pairs dictionary.
|
||||||
@@ -109,13 +136,24 @@ class HistoricCSVPriceHandler(PriceHandler):
|
|||||||
in a chronological fashion.
|
in a chronological fashion.
|
||||||
"""
|
"""
|
||||||
for p in self.pairs:
|
for p in self.pairs:
|
||||||
pair_path = os.path.join(self.csv_dir, '%s.csv' % p)
|
pair_path = os.path.join(self.csv_dir, '%s_%s.csv' % (p, date_str))
|
||||||
self.pair_frames[p] = pd.io.parsers.read_csv(
|
self.pair_frames[p] = pd.io.parsers.read_csv(
|
||||||
pair_path, header=True, index_col=0, parse_dates=True,
|
pair_path, header=True, index_col=0,
|
||||||
|
parse_dates=True, dayfirst=True,
|
||||||
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
|
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
|
||||||
)
|
)
|
||||||
self.pair_frames[p]["Pair"] = p
|
self.pair_frames[p]["Pair"] = p
|
||||||
self.all_pairs = pd.concat(self.pair_frames.values()).sort().iterrows()
|
return pd.concat(self.pair_frames.values()).sort().iterrows()
|
||||||
|
|
||||||
|
def _update_csv_for_day(self):
|
||||||
|
try:
|
||||||
|
dt = self.file_dates[self.cur_date_idx+1]
|
||||||
|
except IndexError: # End of file dates
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.cur_date_pairs = self._open_convert_csv_files_for_day(dt)
|
||||||
|
self.cur_date_idx += 1
|
||||||
|
return True
|
||||||
|
|
||||||
def stream_next_tick(self):
|
def stream_next_tick(self):
|
||||||
"""
|
"""
|
||||||
@@ -130,30 +168,35 @@ class HistoricCSVPriceHandler(PriceHandler):
|
|||||||
well as updating the current bid/ask and inverse bid/ask.
|
well as updating the current bid/ask and inverse bid/ask.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
index, row = next(self.all_pairs)
|
index, row = next(self.cur_date_pairs)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return
|
# End of the current days data
|
||||||
else:
|
if self._update_csv_for_day():
|
||||||
getcontext().rounding = ROUND_HALF_DOWN
|
index, row = next(self.cur_date_pairs)
|
||||||
pair = row["Pair"]
|
else: # End of the data
|
||||||
bid = Decimal(str(row["Bid"])).quantize(
|
self.continue_backtest = False
|
||||||
Decimal("0.00001")
|
return
|
||||||
)
|
|
||||||
ask = Decimal(str(row["Ask"])).quantize(
|
getcontext().rounding = ROUND_HALF_DOWN
|
||||||
Decimal("0.00001")
|
pair = row["Pair"]
|
||||||
)
|
bid = Decimal(str(row["Bid"])).quantize(
|
||||||
|
Decimal("0.00001")
|
||||||
|
)
|
||||||
|
ask = Decimal(str(row["Ask"])).quantize(
|
||||||
|
Decimal("0.00001")
|
||||||
|
)
|
||||||
|
|
||||||
# Create decimalised prices for traded pair
|
# Create decimalised prices for traded pair
|
||||||
self.prices[pair]["bid"] = bid
|
self.prices[pair]["bid"] = bid
|
||||||
self.prices[pair]["ask"] = ask
|
self.prices[pair]["ask"] = ask
|
||||||
self.prices[pair]["time"] = index
|
self.prices[pair]["time"] = index
|
||||||
|
|
||||||
# Create decimalised prices for inverted pair
|
# Create decimalised prices for inverted pair
|
||||||
inv_pair, inv_bid, inv_ask = self.invert_prices(pair, bid, ask)
|
inv_pair, inv_bid, inv_ask = self.invert_prices(pair, bid, ask)
|
||||||
self.prices[inv_pair]["bid"] = inv_bid
|
self.prices[inv_pair]["bid"] = inv_bid
|
||||||
self.prices[inv_pair]["ask"] = inv_ask
|
self.prices[inv_pair]["ask"] = inv_ask
|
||||||
self.prices[inv_pair]["time"] = index
|
self.prices[inv_pair]["time"] = index
|
||||||
|
|
||||||
# Create the tick event for the queue
|
# Create the tick event for the queue
|
||||||
tev = TickEvent(pair, index, bid, ask)
|
tev = TickEvent(pair, index, bid, ask)
|
||||||
self.events_queue.put(tev)
|
self.events_queue.put(tev)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def create_drawdowns(pnl):
|
|||||||
|
|
||||||
# Loop over the index range
|
# Loop over the index range
|
||||||
for t in range(1, len(idx)):
|
for t in range(1, len(idx)):
|
||||||
hwm.append(max(hwm[t-1], pnl[t]))
|
hwm.append(max(hwm[t-1], pnl.ix[t]))
|
||||||
drawdown[t]= (hwm[t]-pnl[t])
|
drawdown.ix[t]= (hwm[t]-pnl.ix[t])
|
||||||
duration[t]= (0 if drawdown[t] == 0 else duration[t-1]+1)
|
duration.ix[t]= (0 if drawdown.ix[t] == 0 else duration.ix[t-1]+1)
|
||||||
return drawdown, drawdown.max(), duration.max()
|
return drawdown, drawdown.max(), duration.max()
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
This is a small helper script written to help debug issues
|
||||||
|
with performance calculation, that avoids having to re-run
|
||||||
|
the full backtest.
|
||||||
|
|
||||||
|
In this case it simply works off the "backtest.csv" file that
|
||||||
|
is produced from a backtest.py run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from qsforex.performance.performance import create_drawdowns
|
||||||
|
from qsforex.settings import OUTPUT_RESULTS_DIR
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user