Skip to content

Commit

Permalink
Lots of changes. Modified the Position object to handle more of the a…
Browse files Browse the repository at this point in the history
…ctual position calculations instead of the Portfolio. Added more unit tests for both Position and Portfolio. Allowed Positions to trade in currencies other than GBPUSD and in base/quotes which aren't the home currency. Modified the backtester to be single-threaded and added a basic Moving Average Crossover strategy. Also added a basic equity curve output script.
  • Loading branch information
mhallsmoore committed Apr 21, 2015
1 parent e747778 commit e84512e
Show file tree
Hide file tree
Showing 11 changed files with 695 additions and 371 deletions.
43 changes: 20 additions & 23 deletions backtest/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@
from qsforex.execution.execution import SimulatedExecution
from qsforex.portfolio.portfolio import Portfolio
from qsforex import settings
from qsforex.strategy.strategy import TestStrategy
from qsforex.strategy.strategy import TestStrategy, MovingAverageCrossStrategy
from qsforex.data.price import HistoricCSVPriceHandler


def trade(events, strategy, portfolio, execution, heartbeat):
def backtest(
events, ticker, strategy, portfolio,
execution, heartbeat, max_iters=200000
):
"""
Carries out an infinite while loop that polls the
events queue and directs each event to either the
strategy component of the execution handler. The
loop will then pause for "heartbeat" seconds and
continue.
continue unti the maximum number of iterations is
exceeded.
"""
while True:
iters = 0
while True and iters < max_iters:
ticker.stream_next_tick()
try:
event = events.get(False)
except Queue.Empty:
Expand All @@ -33,13 +39,12 @@ def trade(events, strategy, portfolio, execution, heartbeat):
elif event.type == 'ORDER':
execution.execute_order(event)
time.sleep(heartbeat)
iters += 1
portfolio.output_results()


if __name__ == "__main__":
# Set the number of decimal places to 2
getcontext().prec = 2

heartbeat = 0.0 # Half a second between polling
heartbeat = 0.0
events = Queue.Queue()
equity = settings.EQUITY

Expand All @@ -51,27 +56,19 @@ def trade(events, strategy, portfolio, execution, heartbeat):
sys.exit()

# Create the historic tick data streaming class
prices = HistoricCSVPriceHandler(pairs, events, csv_dir)
ticker = HistoricCSVPriceHandler(pairs, events, csv_dir)

# Create the strategy/signal generator, passing the
# instrument and the events queue
strategy = TestStrategy(pairs[0], events)
strategy = MovingAverageCrossStrategy(
pairs, events, 500, 2000
)

# Create the portfolio object to track trades
portfolio = Portfolio(prices, events, equity=equity)
portfolio = Portfolio(ticker, events, equity=equity)

# Create the simulated execution handler
execution = SimulatedExecution()

# Create two separate threads: One for the trading loop
# and another for the market price streaming class
trade_thread = threading.Thread(
target=trade, args=(
events, strategy, portfolio, execution, heartbeat
)
)
price_thread = threading.Thread(target=prices.stream_to_queue, args=[])

# Start both threads
trade_thread.start()
price_thread.start()
# Carry out the backtest loop
backtest(events, ticker, strategy, portfolio, execution, heartbeat)
24 changes: 24 additions & 0 deletions backtest/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os, os.path

import pandas as pd
import matplotlib.pyplot as plt

from qsforex.settings import OUTPUT_RESULTS_DIR


if __name__ == "__main__":
"""
A simple script to plot the balance of the portfolio, or
"equity curve", as a function of time.
It requires OUTPUT_RESULTS_DIR to be set in the project
settings.
"""
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["balance"].plot()
plt.show()
115 changes: 104 additions & 11 deletions data/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import os
import os.path
import time

import numpy as np
import pandas as pd

Expand Down Expand Up @@ -59,28 +61,119 @@ def __init__(self, pairs, events_queue, csv_dir):
self.pairs = pairs
self.events_queue = events_queue
self.csv_dir = csv_dir
self.cur_bid = None
self.cur_ask = None
self.prices = self._set_up_prices_dict()
self.pair_frames = {}
self._open_convert_csv_files()

def _set_up_prices_dict(self):
"""
Due to the way that the Position object handles P&L
calculation, it is necessary to include values for not
only base/quote currencies but also their reciprocals.
This means that this class will contain keys for, e.g.
"GBPUSD" and "USDGBP".
At this stage they are calculated in an ad-hoc manner,
but a future TODO is to modify the following code to
be more robust and straightforward to follow.
"""
prices_dict = dict(
(k, v) for k,v in [
(p, {"bid": None, "ask": None, "time": None}) for p in self.pairs
]
)
inv_prices_dict = dict(
(k, v) for k,v in [
(
"%s%s" % (p[3:], p[:3]),
{"bid": None, "ask": None, "time": None}
) for p in self.pairs
]
)
prices_dict.update(inv_prices_dict)
return prices_dict

def _open_convert_csv_files(self):
"""
Opens the CSV files from the data directory, converting
them into pandas DataFrames within a pairs dictionary.
The function then concatenates all of the separate pairs
for a single day into a single data frame that is time
ordered, allowing tick data events to be added to the queue
in a chronological fashion.
"""
for p in self.pairs:
pair_path = os.path.join(self.csv_dir, '%s.csv' % p)
self.pair_frames[p] = pd.io.parsers.read_csv(
pair_path, header=True, index_col=0, parse_dates=True,
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
)
self.pair_frames[p]["Pair"] = p
self.all_pairs = pd.concat(self.pair_frames.values()).sort().iterrows()

def invert_prices(self, row):
"""
Simply inverts the prices for a particular currency pair.
This will turn the bid/ask of "GBPUSD" into bid/ask for
"USDGBP" and place them in the prices dictionary.
"""
pair_path = os.path.join(self.csv_dir, '%s.csv' % self.pairs[0])
self.pair = pd.io.parsers.read_csv(
pair_path, header=True, index_col=0, parse_dates=True,
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
).iterrows()
pair = row["Pair"]
bid = row["Bid"]
ask = row["Ask"]
inv_pair = "%s%s" % (pair[3:], pair[:3])
inv_bid = Decimal(str(1.0/bid)).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
inv_ask = Decimal(str(1.0/ask)).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
return inv_pair, inv_bid, inv_ask

def stream_next_tick(self):
"""
The Backtester has now moved over to a single-threaded
model in order to fully reproduce results on each run.
This means that the stream_to_queue method is unable to
be used and a replacement, called stream_next_tick, is
used instead.
This method is called by the backtesting function outside
of this class and places a single tick onto the queue, as
well as updating the current bid/ask and inverse bid/ask.
"""
try:
index, row = self.all_pairs.next()
except StopIteration:
return
else:
self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.prices[row["Pair"]]["time"] = index
inv_pair, inv_bid, inv_ask = self.invert_prices(row)
self.prices[inv_pair]["bid"] = inv_bid
self.prices[inv_pair]["ask"] = inv_ask
self.prices[inv_pair]["time"] = index
tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
self.events_queue.put(tev)

def stream_to_queue(self):
self._open_convert_csv_files()
for index, row in self.pair:
self.cur_bid = Decimal(str(row["Bid"])).quantize(
for index, row in self.all_pairs:
self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.cur_ask = Decimal(str(row["Ask"])).quantize(
self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
tev = TickEvent(self.pairs[0], index, row["Bid"], row["Ask"])
self.prices[row["Pair"]]["time"] = index
inv_pair, inv_bid, inv_ask = self.invert_prices(row)
self.prices[inv_pair]["bid"] = inv_bid
self.prices[inv_pair]["ask"] = inv_ask
self.prices[inv_pair]["time"] = index
tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
self.events_queue.put(tev)
3 changes: 2 additions & 1 deletion event/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ def __init__(self, instrument, time, bid, ask):


class SignalEvent(Event):
def __init__(self, instrument, order_type, side):
def __init__(self, instrument, order_type, side, time):
self.type = 'SIGNAL'
self.instrument = instrument
self.order_type = order_type
self.side = side
self.time = time # Time of the last tick that generated the signal


class OrderEvent(Event):
Expand Down
Loading

0 comments on commit e84512e

Please sign in to comment.