From e84512e1e7bc5f96fd4366b10ba87f9c0e6e045d Mon Sep 17 00:00:00 2001 From: Michael Halls-Moore Date: Tue, 21 Apr 2015 13:01:20 +0100 Subject: [PATCH] Lots of changes. Modified the Position object to handle more of the actual 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. --- backtest/backtest.py | 43 ++-- backtest/output.py | 24 +++ data/price.py | 115 +++++++++-- event/event.py | 3 +- portfolio/portfolio.py | 117 +++++------ portfolio/portfolio_test.py | 385 ++++++++++++++++-------------------- portfolio/position.py | 116 ++++++++--- portfolio/position_test.py | 190 +++++++++++++++--- requirements.txt | 6 +- settings.py | 1 + strategy/strategy.py | 76 ++++++- 11 files changed, 700 insertions(+), 376 deletions(-) create mode 100644 backtest/output.py diff --git a/backtest/backtest.py b/backtest/backtest.py index 8d0630d..10e0a29 100644 --- a/backtest/backtest.py +++ b/backtest/backtest.py @@ -7,19 +7,25 @@ from decimal import Decimal, getcontext 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: @@ -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 @@ -51,27 +56,19 @@ if __name__ == "__main__": 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() \ No newline at end of file + # Carry out the backtest loop + backtest(events, ticker, strategy, portfolio, execution, heartbeat) diff --git a/backtest/output.py b/backtest/output.py new file mode 100644 index 0000000..aa4bf1a --- /dev/null +++ b/backtest/output.py @@ -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() \ No newline at end of file diff --git a/data/price.py b/data/price.py index d433be9..1be953e 100644 --- a/data/price.py +++ b/data/price.py @@ -3,6 +3,8 @@ import datetime from decimal import Decimal, getcontext, ROUND_HALF_DOWN import os import os.path +import time + import numpy as np import pandas as pd @@ -59,28 +61,119 @@ class HistoricCSVPriceHandler(PriceHandler): 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. """ - 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() + 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 = 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) diff --git a/event/event.py b/event/event.py index c2ec0fd..0552a1c 100644 --- a/event/event.py +++ b/event/event.py @@ -12,11 +12,12 @@ class TickEvent(Event): 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): diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index 5467cf2..d004efe 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -1,123 +1,107 @@ from copy import deepcopy from decimal import Decimal, getcontext, ROUND_HALF_DOWN +import os + +import pandas as pd from qsforex.event.event import OrderEvent from qsforex.portfolio.position import Position +from qsforex.settings import OUTPUT_RESULTS_DIR class Portfolio(object): def __init__( - self, ticker, events, base="GBP", leverage=20, + self, ticker, events, home_currency="GBP", leverage=20, equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02") ): self.ticker = ticker self.events = events - self.base = base + self.home_currency = home_currency self.leverage = leverage self.equity = equity self.balance = deepcopy(self.equity) self.risk_per_trade = risk_per_trade self.trade_units = self.calc_risk_position_size() self.positions = {} + self.equity = [] def calc_risk_position_size(self): return self.equity * self.risk_per_trade def add_new_position( - self, position_type, market, units, - exposure, bid, ask + self, position_type, currency_pair, units, ticker ): ps = Position( - position_type, market, units, - exposure, bid, ask + self.home_currency, position_type, + currency_pair, units, ticker ) - self.positions[market] = ps + self.positions[currency_pair] = ps - def add_position_units( - self, market, units, - exposure, bid, ask - ): - if market not in self.positions: + def add_position_units(self, currency_pair, units): + if currency_pair not in self.positions: return False else: - ps = self.positions[market] - if ps.position_type == "long": - add_price = ask - else: - add_price = bid - new_total_units = ps.units + units - new_total_cost = ps.avg_price*ps.units + add_price*units - ps.exposure += exposure - ps.avg_price = new_total_cost/new_total_units - ps.units = new_total_units - ps.update_position_price(bid, ask, exposure) + ps = self.positions[currency_pair] + ps.add_units(units) return True - def remove_position_units( - self, market, units, bid, ask - ): - if market not in self.positions: + def remove_position_units(self, currency_pair, units): + if currency_pair not in self.positions: return False else: - ps = self.positions[market] - if ps.position_type == "long": - remove_price = bid - else: - remove_price = ask - ps.units -= units - exposure = Decimal(str(units)) - ps.exposure -= exposure - ps.update_position_price(bid, ask, exposure) - pnl = ps.calculate_pips() * exposure / remove_price - self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) + ps = self.positions[currency_pair] + pnl = ps.remove_units(units) + self.balance += pnl return True - def close_position( - self, market, bid, ask - ): - if market not in self.positions: + def close_position(self, currency_pair): + if currency_pair not in self.positions: return False else: - ps = self.positions[market] - ps.update_position_price(bid, ask, ps.exposure) - if ps.position_type == "long": - remove_price = bid - else: - remove_price = ask - pnl = ps.calculate_pips() * ps.exposure / remove_price - self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) - del[self.positions[market]] + ps = self.positions[currency_pair] + pnl = ps.close_position() + self.balance += pnl + del[self.positions[currency_pair]] return True + def append_equity_row(self, time, balance): + d = {"time": time, "balance": balance} + self.equity.append(d) + + 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 + def execute_signal(self, signal_event): side = signal_event.side - market = signal_event.instrument + currency_pair = signal_event.instrument units = int(self.trade_units) - exposure = Decimal(str(units)) - bid = Decimal(str(self.ticker.cur_bid)) - ask = Decimal(str(self.ticker.cur_ask)) - + time = signal_event.time + # If there is no position, create one - if market not in self.positions: + if currency_pair not in self.positions: if side == "buy": position_type = "long" else: position_type = "short" self.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, currency_pair, + units, self.ticker ) # If a position exists add or remove units else: - ps = self.positions[market] + ps = self.positions[currency_pair] if side == "buy" and ps.position_type == "long": - add_position_units(market, units, exposure, bid, ask) + add_position_units(currency_pair, units) elif side == "sell" and ps.position_type == "long": if units == ps.units: - self.close_position(market, bid, ask) + self.close_position(currency_pair) # TODO: Allow units to be added/removed elif units < ps.units: return @@ -126,7 +110,7 @@ class Portfolio(object): elif side == "buy" and ps.position_type == "short": if units == ps.units: - self.close_position(market, bid, ask) + self.close_position(currency_pair) # TODO: Allow units to be added/removed elif units < ps.units: return @@ -134,9 +118,10 @@ class Portfolio(object): return elif side == "sell" and ps.position_type == "short": - add_position_units(market, units, exposure, bid, ask) + add_position_units(currency_pair, units) - order = OrderEvent(market, units, "market", side) + order = OrderEvent(currency_pair, units, "market", side) self.events.put(order) - print "Balance: %0.2f" % self.balance \ No newline at end of file + print "Balance: %0.2f" % self.balance + self.append_equity_row(time, self.balance) \ No newline at end of file diff --git a/portfolio/portfolio_test.py b/portfolio/portfolio_test.py index f5a0b92..6c8d939 100644 --- a/portfolio/portfolio_test.py +++ b/portfolio/portfolio_test.py @@ -1,364 +1,325 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN import unittest -from portfolio import Portfolio +from qsforex.portfolio.portfolio import Portfolio +from qsforex.portfolio.position_test import TickerMock +from qsforex.portfolio.position import Position class TestPortfolio(unittest.TestCase): def setUp(self): - base = "GBP" + home_currency = "GBP" leverage = 20 equity = Decimal("100000.00") risk_per_trade = Decimal("0.02") - ticker = {} + ticker = TickerMock() events = {} self.port = Portfolio( - ticker, events, base=base, leverage=leverage, - equity=equity, risk_per_trade=risk_per_trade + ticker, events, home_currency=home_currency, + leverage=leverage, equity=equity, + risk_per_trade=risk_per_trade ) def test_add_position_long(self): position_type = "long" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] + ps = self.port.positions[currency_pair] self.assertEquals(ps.position_type, position_type) - self.assertEquals(ps.market, market) + self.assertEquals(ps.currency_pair, currency_pair) self.assertEquals(ps.units, units) - self.assertEquals(ps.exposure, exposure) - self.assertEquals(ps.avg_price, ask) - self.assertEquals(ps.cur_price, bid) + self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["ask"]) + self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["bid"]) def test_add_position_short(self): position_type = "short" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") - + ticker = TickerMock() self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] + ps = self.port.positions[currency_pair] self.assertEquals(ps.position_type, position_type) - self.assertEquals(ps.market, market) + self.assertEquals(ps.currency_pair, currency_pair) self.assertEquals(ps.units, units) - self.assertEquals(ps.exposure, exposure) - self.assertEquals(ps.avg_price, bid) - self.assertEquals(ps.cur_price, ask) + self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["bid"]) + self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["ask"]) def test_add_position_units_long(self): position_type = "long" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" + alt_currency_pair = "USDCAD" apu = self.port.add_position_units( - market, units, exposure, - bid, ask + alt_currency_pair, units ) self.assertFalse(apu) # Add a position and test for real position - market = "GBP/USD" self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] + ps = self.port.positions[currency_pair] # Test for addition of units - bid = Decimal("1.51878") - ask = Decimal("1.51928") + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") apu = self.port.add_position_units( - market, units, exposure, - bid, ask + currency_pair, units ) self.assertTrue(apu) - self.assertEqual(ps.avg_price, Decimal("1.518735")) + self.assertEqual(ps.avg_price, Decimal("1.511385")) def test_add_position_units_short(self): position_type = "short" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" + alt_currency_pair = "USDCAD" apu = self.port.add_position_units( - market, units, exposure, - bid, ask + alt_currency_pair, units ) self.assertFalse(apu) # Add a position and test for real position - market = "GBP/USD" self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] + ps = self.port.positions[currency_pair] # Test for addition of units - bid = Decimal("1.51878") - ask = Decimal("1.51928") + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") apu = self.port.add_position_units( - market, units, exposure, - bid, ask + currency_pair, units ) self.assertTrue(apu) - self.assertEqual(ps.avg_price, Decimal("1.51824")) + self.assertEqual(ps.avg_price, Decimal("1.51103")) def test_remove_position_units_long(self): position_type = "long" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" + alt_currency_pair = "USDCAD" apu = self.port.remove_position_units( - market, units, bid, ask + alt_currency_pair, units ) self.assertFalse(apu) # Add a position and then add units to it - market = "GBP/USD" self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] - bid = Decimal("1.51878") - ask = Decimal("1.51928") - add_units = 8000 - add_exposure = Decimal(str(add_units)) + ps = self.port.positions[currency_pair] + # Test for addition of units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") + + add_units = Decimal("8000") apu = self.port.add_position_units( - market, add_units, add_exposure, - bid, ask + currency_pair, add_units ) self.assertEqual(ps.units, 10000) - self.assertEqual(ps.exposure, Decimal("10000.00")) - self.assertEqual(ps.avg_price, Decimal("1.519062")) + self.assertEqual(ps.avg_price, Decimal("1.516122")) # Test removal of (some) of the units - bid = Decimal("1.52017") - ask = Decimal("1.52134") - remove_units = 3000 + ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65782") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65732") + + remove_units = Decimal("3000") rpu = self.port.remove_position_units( - market, remove_units, bid, ask + currency_pair, remove_units ) self.assertTrue(rpu) - self.assertEqual(ps.units, 7000) - self.assertEqual(ps.exposure, Decimal("7000.00")) - self.assertEqual(ps.profit_base, Decimal("2.19054")) - self.assertEqual(self.port.balance, Decimal("100002.19")) + self.assertEqual(ps.units, Decimal("7000")) + self.assertEqual(self.port.balance, Decimal("100007.99")) def test_remove_position_units_short(self): position_type = "short" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" + alt_currency_pair = "USDCAD" apu = self.port.remove_position_units( - market, units, bid, ask + alt_currency_pair, units ) self.assertFalse(apu) # Add a position and then add units to it - market = "GBP/USD" self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] - bid = Decimal("1.51878") - ask = Decimal("1.51928") - add_units = 8000 - add_exposure = Decimal(str(add_units)) + ps = self.port.positions[currency_pair] + # Test for addition of units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") + + add_units = Decimal("8000") apu = self.port.add_position_units( - market, add_units, add_exposure, - bid, ask + currency_pair, add_units ) self.assertEqual(ps.units, 10000) - self.assertEqual(ps.exposure, Decimal("10000.00")) - self.assertEqual(ps.avg_price, Decimal("1.518564")) + self.assertEqual(ps.avg_price, Decimal("1.51568")) # Test removal of (some) of the units - bid = Decimal("1.52017") - ask = Decimal("1.52134") - remove_units = 3000 + ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65782") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65732") + + remove_units = Decimal("3000") rpu = self.port.remove_position_units( - market, remove_units, bid, ask + currency_pair, remove_units ) self.assertTrue(rpu) - self.assertEqual(ps.units, 7000) - self.assertEqual(ps.exposure, Decimal("7000.00")) - self.assertEqual(ps.profit_base, Decimal("-5.48201")) - self.assertEqual(self.port.balance, Decimal("99994.52")) + self.assertEqual(ps.units, Decimal("7000")) + self.assertEqual(self.port.balance, Decimal("99988.84")) def test_close_position_long(self): position_type = "long" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" - cp = self.port.close_position( - market, bid, ask + alt_currency_pair = "USDCAD" + apu = self.port.remove_position_units( + alt_currency_pair, units ) - self.assertFalse(cp) + self.assertFalse(apu) - # Add a position and then close it - # Will lose money on the spread - market = "GBP/USD" + # Add a position and then add units to it self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] - cp = self.port.close_position( - market, bid, ask - ) - self.assertTrue(cp) - self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("99999.35")) - - # Add 2000, add another 8000, remove 3000 and then - # close the position. Balance should be as expected - # for a multi-leg transaction. - self.port.add_new_position( - position_type, market, units, - exposure, bid, ask - ) - ps = self.port.positions[market] - - # Add 8000 units - bid = Decimal("1.51878") - ask = Decimal("1.51928") - add_units = 8000 - add_exposure = Decimal(str(add_units)) + ps = self.port.positions[currency_pair] + # Test for addition of units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") + + add_units = Decimal("8000") apu = self.port.add_position_units( - market, add_units, - add_exposure, bid, ask + currency_pair, add_units ) self.assertEqual(ps.units, 10000) - self.assertEqual(ps.exposure, Decimal("10000.00")) - self.assertEqual(ps.avg_price, Decimal("1.519062")) + self.assertEqual(ps.avg_price, Decimal("1.516122")) - # Remove 3000 units - bid = Decimal("1.52017") - ask = Decimal("1.52134") - remove_units = 3000 + # Test removal of (some) of the units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65782") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65732") + + remove_units = Decimal("3000") rpu = self.port.remove_position_units( - market, remove_units, bid, ask + currency_pair, remove_units ) - self.assertEqual(ps.units, 7000) - self.assertEqual(ps.exposure, Decimal("7000.00")) - self.assertEqual(ps.profit_base, Decimal("2.19054")) - self.assertEqual(self.port.balance, Decimal("100001.54")) + self.assertTrue(rpu) + self.assertEqual(ps.units, Decimal("7000")) + self.assertEqual(self.port.balance, Decimal("100007.99")) # Close the position - cp = self.port.close_position( - market, bid, ask - ) + cp = self.port.close_position(currency_pair) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("100006.65")) + self.assertEqual(self.port.balance, Decimal("100026.64")) def test_close_position_short(self): position_type = "short" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.51770") - ask = Decimal("1.51819") + ticker = TickerMock() # Test for no position - market = "EUR/USD" - cp = self.port.close_position( - market, bid, ask + alt_currency_pair = "USDCAD" + apu = self.port.remove_position_units( + alt_currency_pair, units ) - self.assertFalse(cp) + self.assertFalse(apu) - # Add a position and then close it - # Will lose money on the spread - market = "GBP/USD" + # Add a position and then add units to it self.port.add_new_position( - position_type, market, units, - exposure, bid, ask + position_type, + currency_pair, + units, ticker ) - ps = self.port.positions[market] - cp = self.port.close_position( - market, bid, ask - ) - self.assertTrue(cp) - self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("99999.35")) - - # Add 2000, add another 8000, remove 3000 and then - # close the position. Balance should be as expected - # for a multi-leg transaction. - self.port.add_new_position( - position_type, market, units, - exposure, bid, ask - ) - ps = self.port.positions[market] - - # Add 8000 units - bid = Decimal("1.51878") - ask = Decimal("1.51928") - add_units = 8000 - add_exposure = Decimal(str(add_units)) + ps = self.port.positions[currency_pair] + # Test for addition of units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") + + add_units = Decimal("8000") apu = self.port.add_position_units( - market, add_units, - add_exposure, bid, ask + currency_pair, add_units ) self.assertEqual(ps.units, 10000) - self.assertEqual(ps.exposure, Decimal("10000.00")) - self.assertEqual(ps.avg_price, Decimal("1.518564")) + self.assertEqual(ps.avg_price, Decimal("1.51568")) - # Remove 3000 units - bid = Decimal("1.52017") - ask = Decimal("1.52134") - remove_units = 3000 + # Test removal of (some) of the units + ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017") + ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134") + ticker.prices["USDGBP"]["bid"] = Decimal("0.65782") + ticker.prices["USDGBP"]["ask"] = Decimal("0.65732") + + remove_units = Decimal("3000") rpu = self.port.remove_position_units( - market, remove_units, bid, ask + currency_pair, remove_units ) - self.assertEqual(ps.units, 7000) - self.assertEqual(ps.exposure, Decimal("7000.00")) - self.assertEqual(ps.profit_base, Decimal("-5.48201")) - self.assertEqual(self.port.balance, Decimal("99993.87")) + self.assertTrue(rpu) + self.assertEqual(ps.units, Decimal("7000")) + self.assertEqual(self.port.balance, Decimal("99988.84")) # Close the position - cp = self.port.close_position( - market, bid, ask - ) + cp = self.port.close_position(currency_pair) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("99981.08")) + self.assertEqual(self.port.balance, Decimal("99962.80")) + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/portfolio/position.py b/portfolio/position.py index bdc4021..1577e70 100644 --- a/portfolio/position.py +++ b/portfolio/position.py @@ -3,51 +3,107 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN class Position(object): def __init__( - self, position_type, market, - units, exposure, bid, ask + self, home_currency, position_type, + currency_pair, units, ticker ): + self.home_currency = home_currency # Account denomination (e.g. GBP) self.position_type = position_type # Long or short - self.market = market + self.currency_pair = currency_pair # Intended traded currency pair self.units = units - self.exposure = Decimal(str(exposure)) + self.ticker = ticker + self.set_up_currencies() + self.profit_base = self.calculate_profit_base() + self.profit_perc = self.calculate_profit_perc() - # Long or short + def set_up_currencies(self): + self.base_currency = self.currency_pair[:3] # For EUR/USD, this is EUR + self.quote_currency = self.currency_pair[3:] # For EUR/USD, this is USD + # For EUR/USD, with account denominated in GBP, this is USD/GBP + self.quote_home_currency_pair = "%s%s" % (self.quote_currency, self.home_currency) + + ticker_cur = self.ticker.prices[self.currency_pair] if self.position_type == "long": - self.avg_price = Decimal(str(ask)) - self.cur_price = Decimal(str(bid)) + self.avg_price = Decimal(str(ticker_cur["ask"])) + self.cur_price = Decimal(str(ticker_cur["bid"])) else: - self.avg_price = Decimal(str(bid)) - self.cur_price = Decimal(str(ask)) - - self.profit_base = self.calculate_profit_base(self.exposure) - self.profit_perc = self.calculate_profit_perc(self.exposure) + self.avg_price = Decimal(str(ticker_cur["bid"])) + self.cur_price = Decimal(str(ticker_cur["ask"])) def calculate_pips(self): - getcontext.prec = 6 mult = Decimal("1") if self.position_type == "long": mult = Decimal("1") elif self.position_type == "short": mult = Decimal("-1") - return (mult * (self.cur_price - self.avg_price)).quantize( + pips = (mult * (self.cur_price - self.avg_price)).quantize( Decimal("0.00001"), ROUND_HALF_DOWN ) + return pips - def calculate_profit_base(self, exposure): - pips = self.calculate_pips() - return (pips * exposure / self.cur_price).quantize( - Decimal("0.00001"), ROUND_HALF_DOWN - ) - - def calculate_profit_perc(self, exposure): - return (self.profit_base / exposure * Decimal("100.00")).quantize( - Decimal("0.00001"), ROUND_HALF_DOWN - ) - - def update_position_price(self, bid, ask, exposure): + def calculate_profit_base(self): + pips = self.calculate_pips() + ticker_qh = self.ticker.prices[self.quote_home_currency_pair] if self.position_type == "long": - self.cur_price = Decimal(str(bid)) + qh_close = ticker_qh["bid"] else: - self.cur_price = Decimal(str(ask)) - self.profit_base = self.calculate_profit_base(exposure) - self.profit_perc = self.calculate_profit_perc(exposure) + qh_close = ticker_qh["ask"] + profit = pips * qh_close * self.units + return profit.quantize( + Decimal("0.00001"), ROUND_HALF_DOWN + ) + + def calculate_profit_perc(self): + return (self.profit_base / self.units * Decimal("100.00")).quantize( + Decimal("0.00001"), ROUND_HALF_DOWN + ) + + def update_position_price(self): + ticker_cur = self.ticker.prices[self.currency_pair] + if self.position_type == "long": + self.cur_price = Decimal(str(ticker_cur["bid"])) + else: + self.cur_price = Decimal(str(ticker_cur["ask"])) + self.profit_base = self.calculate_profit_base() + self.profit_perc = self.calculate_profit_perc() + + def add_units(self, units): + cp = self.ticker.prices[self.currency_pair] + if self.position_type == "long": + add_price = cp["ask"] + else: + add_price = cp["bid"] + new_total_units = self.units + units + new_total_cost = self.avg_price*self.units + add_price*units + self.avg_price = new_total_cost/new_total_units + self.units = new_total_units + self.update_position_price() + + def remove_units(self, units): + dec_units = Decimal(str(units)) + ticker_cp = self.ticker.prices[self.currency_pair] + ticker_qh = self.ticker.prices[self.quote_home_currency_pair] + if self.position_type == "long": + remove_price = ticker_cp["ask"] + qh_close = ticker_qh["bid"] + else: + remove_price = ticker_cp["bid"] + qh_close = ticker_qh["ask"] + self.units -= dec_units + self.update_position_price() + # Calculate PnL + pnl = self.calculate_pips() * qh_close * dec_units + return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) + + def close_position(self): + ticker_cp = self.ticker.prices[self.currency_pair] + ticker_qh = self.ticker.prices[self.quote_home_currency_pair] + if self.position_type == "long": + remove_price = ticker_cp["ask"] + qh_close = ticker_qh["bid"] + else: + remove_price = ticker_cp["bid"] + qh_close = ticker_qh["ask"] + self.update_position_price() + # Calculate PnL + pnl = self.calculate_pips() * qh_close * self.units + return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) diff --git a/portfolio/position_test.py b/portfolio/position_test.py index 1c930aa..a0f1c62 100644 --- a/portfolio/position_test.py +++ b/portfolio/position_test.py @@ -4,18 +4,39 @@ import unittest from position import Position +class TickerMock(object): + """ + A mock object that allows a representation of the + ticker/pricing handler. + """ + + def __init__(self): + 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 +# ===================================== + class TestLongGBPUSDPosition(unittest.TestCase): + """ + Unit tests that cover going long GBP/USD with an account + denominated currency of GBP, using 2,000 units of GBP/USD. + """ def setUp(self): getcontext.prec = 2 + home_currency = "GBP" position_type = "long" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.50328") - ask = Decimal("1.50349") + ticker = TickerMock() self.position = Position( - position_type, market, - units, exposure, bid, ask + home_currency, position_type, + currency_pair, units, ticker ) def test_calculate_init_pips(self): @@ -23,11 +44,11 @@ class TestLongGBPUSDPosition(unittest.TestCase): self.assertEqual(pos_pips, Decimal("-0.00021")) def test_calculate_init_profit_base(self): - profit_base = self.position.calculate_profit_base(self.position.exposure) + profit_base = self.position.calculate_profit_base() self.assertEqual(profit_base, Decimal("-0.27939")) def test_calculate_init_profit_perc(self): - profit_perc = self.position.calculate_profit_perc(self.position.exposure) + profit_perc = self.position.calculate_profit_perc() self.assertEqual(profit_perc, Decimal("-0.01397")) def test_calculate_updated_values(self): @@ -35,32 +56,37 @@ class TestLongGBPUSDPosition(unittest.TestCase): Check that after the bid/ask prices move, that the updated pips, profit and percentage profit calculations are correct. """ - bid = Decimal("1.50486") - ask = Decimal("1.50586") - self.position.update_position_price(bid, ask, self.position.exposure) + prices = self.position.ticker.prices + prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + self.position.update_position_price() + # Check pips pos_pips = self.position.calculate_pips() self.assertEqual(pos_pips, Decimal("0.00137")) # Check profit base - profit_base = self.position.calculate_profit_base(self.position.exposure) - self.assertEqual(profit_base, Decimal("1.82077")) + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("1.82076")) # Check profit percentage - profit_perc = self.position.calculate_profit_perc(self.position.exposure) + profit_perc = self.position.calculate_profit_perc() self.assertEqual(profit_perc, Decimal("0.09104")) class TestShortGBPUSDPosition(unittest.TestCase): + """ + Unit tests that cover going short GBP/USD with an account + denominated currency of GBP, using 2,000 units of GBP/USD. + """ def setUp(self): getcontext.prec = 2 + home_currency = "GBP" position_type = "short" - market = "GBP/USD" + currency_pair = "GBPUSD" units = Decimal("2000") - exposure = Decimal("2000.00") - bid = Decimal("1.50328") - ask = Decimal("1.50349") + ticker = TickerMock() self.position = Position( - position_type, market, - units, exposure, bid, ask + home_currency, position_type, + currency_pair, units, ticker ) def test_calculate_init_pips(self): @@ -68,11 +94,11 @@ class TestShortGBPUSDPosition(unittest.TestCase): self.assertEqual(pos_pips, Decimal("-0.00021")) def test_calculate_init_profit_base(self): - profit_base = self.position.calculate_profit_base(self.position.exposure) + profit_base = self.position.calculate_profit_base() self.assertEqual(profit_base, Decimal("-0.27935")) def test_calculate_init_profit_perc(self): - profit_perc = self.position.calculate_profit_perc(self.position.exposure) + profit_perc = self.position.calculate_profit_perc() self.assertEqual(profit_perc, Decimal("-0.01397")) def test_calculate_updated_values(self): @@ -80,19 +106,127 @@ class TestShortGBPUSDPosition(unittest.TestCase): Check that after the bid/ask prices move, that the updated pips, profit and percentage profit calculations are correct. """ - bid = Decimal("1.50486") - ask = Decimal("1.50586") - self.position.update_position_price(bid, ask, self.position.exposure) + prices = self.position.ticker.prices + prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + self.position.update_position_price() + # Check pips pos_pips = self.position.calculate_pips() self.assertEqual(pos_pips, Decimal("-0.00258")) # Check profit base - profit_base = self.position.calculate_profit_base(self.position.exposure) - self.assertEqual(profit_base, Decimal("-3.42661")) + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("-3.42660")) # Check profit percentage - profit_perc = self.position.calculate_profit_perc(self.position.exposure) + profit_perc = self.position.calculate_profit_perc() self.assertEqual(profit_perc, Decimal("-0.17133")) +# ===================================== +# GBP Home Currency with EUR/USD traded +# ===================================== + +class TestLongEURUSDPosition(unittest.TestCase): + """ + Unit tests that cover going long EUR/USD with an account + denominated currency of GBP, using 2,000 units of EUR/USD. + """ + def setUp(self): + getcontext.prec = 2 + home_currency = "GBP" + position_type = "long" + currency_pair = "EURUSD" + units = Decimal("2000") + ticker = TickerMock() + self.position = Position( + home_currency, position_type, + currency_pair, units, ticker + ) + + def test_calculate_init_pips(self): + pos_pips = self.position.calculate_pips() + self.assertEqual(pos_pips, Decimal("-0.00015")) + + def test_calculate_init_profit_base(self): + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("-0.19956")) + + def test_calculate_init_profit_perc(self): + profit_perc = self.position.calculate_profit_perc() + self.assertEqual(profit_perc, Decimal("-0.00998")) + + def test_calculate_updated_values(self): + """ + Check that after the bid/ask prices move, that the updated + pips, profit and percentage profit calculations are correct. + """ + prices = self.position.ticker.prices + prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["EURUSD"] = {"bid": Decimal("1.07811"), "ask": Decimal("1.07827")} + self.position.update_position_price() + + # Check pips + pos_pips = self.position.calculate_pips() + self.assertEqual(pos_pips, Decimal("-0.00036")) + # Check profit base + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("-0.47845")) + # Check profit percentage + profit_perc = self.position.calculate_profit_perc() + self.assertEqual(profit_perc, Decimal("-0.02392")) + + +class TestLongEURUSDPosition(unittest.TestCase): + """ + Unit tests that cover going short EUR/USD with an account + denominated currency of GBP, using 2,000 units of EUR/USD. + """ + def setUp(self): + getcontext.prec = 2 + home_currency = "GBP" + position_type = "short" + currency_pair = "EURUSD" + units = Decimal("2000") + ticker = TickerMock() + self.position = Position( + home_currency, position_type, + currency_pair, units, ticker + ) + + def test_calculate_init_pips(self): + pos_pips = self.position.calculate_pips() + self.assertEqual(pos_pips, Decimal("-0.00015")) + + def test_calculate_init_profit_base(self): + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("-0.19954")) + + def test_calculate_init_profit_perc(self): + profit_perc = self.position.calculate_profit_perc() + self.assertEqual(profit_perc, Decimal("-0.00998")) + + def test_calculate_updated_values(self): + """ + Check that after the bid/ask prices move, that the updated + pips, profit and percentage profit calculations are correct. + """ + prices = self.position.ticker.prices + prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["EURUSD"] = {"bid": Decimal("1.07811"), "ask": Decimal("1.07827")} + self.position.update_position_price() + + # Check pips + pos_pips = self.position.calculate_pips() + self.assertEqual(pos_pips, Decimal("0.00005")) + # Check profit base + profit_base = self.position.calculate_profit_base() + self.assertEqual(profit_base, Decimal("0.06641")) + # Check profit percentage + profit_perc = self.position.calculate_profit_perc() + self.assertEqual(profit_perc, Decimal("0.00332")) + + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3c78ae1..3a3a758 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,15 @@ argparse==1.2.1 ipython==2.3.1 +matplotlib==1.4.3 +mock==1.0.1 +nose==1.3.6 numpy==1.9.1 pandas==0.15.2 +pyparsing==2.0.3 python-dateutil==2.4.0 pytz==2014.10 requests==2.5.1 scikit-learn==0.15.2 scipy==0.15.1 six==1.9.0 -wsgiref==0.1.2 \ No newline at end of file +wsgiref==0.1.2 diff --git a/settings.py b/settings.py index bc3df2a..bfd0e78 100644 --- a/settings.py +++ b/settings.py @@ -16,6 +16,7 @@ ENVIRONMENTS = { } CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None) +OUTPUT_RESULTS_DIR = os.environ.get('QSFOREX_OUTPUT_RESULTS_DIR', None) DOMAIN = "practice" STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN] diff --git a/strategy/strategy.py b/strategy/strategy.py index 7ebce96..5fb8157 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -2,8 +2,17 @@ from qsforex.event.event import SignalEvent class TestStrategy(object): - def __init__(self, instrument, events): - self.instrument = instrument + """ + A testing strategy that alternates between buying and selling + a currency pair on every 5th tick. This has the effect of + continuously "crossing the spread" and so will be loss-making + strategy. + + It is used to test that the backtester/live trading system is + behaving as expected. + """ + def __init__(self, pairs, events): + self.pairs = pairs self.events = events self.ticks = 0 self.invested = False @@ -12,11 +21,70 @@ class TestStrategy(object): if event.type == 'TICK': if self.ticks % 5 == 0: if self.invested == False: - signal = SignalEvent(self.instrument, "market", "buy") + signal = SignalEvent(self.pairs[0], "market", "buy", event.time) self.events.put(signal) self.invested = True else: - signal = SignalEvent(self.instrument, "market", "sell") + signal = SignalEvent(self.pairs[0], "market", "sell", event.time) self.events.put(signal) self.invested = False self.ticks += 1 + + +class MovingAverageCrossStrategy(object): + """ + A basic Moving Average Crossover strategy that generates + two simple moving averages (SMA), with default windows + of 500 ticks for the short SMA and 2,000 ticks for the + long SMA. + + The strategy is "long only" in the sense it will only + open a long position once the short SMA exceeds the long + SMA. It will close the position (by taking a corresponding + sell order) when the long SMA recrosses the short SMA. + + The strategy uses a rolling SMA calculation in order to + increase efficiency by eliminating the need to call two + full moving average calculations on each tick. + """ + def __init__( + self, pairs, events, + short_window=500, long_window=2000 + ): + self.pairs = pairs + self.events = events + self.ticks = 0 + self.invested = False + + self.short_window = short_window + self.long_window = long_window + self.short_sma = None + self.long_sma = None + + def calc_rolling_sma(self, sma_m_1, window, price): + return ((sma_m_1 * (window - 1)) + price) / window + + def calculate_signals(self, event): + if event.type == 'TICK': + price = event.bid + if self.ticks == 0: + self.short_sma = price + self.long_sma = price + else: + self.short_sma = self.calc_rolling_sma( + self.short_sma, self.short_window, price + ) + self.long_sma = self.calc_rolling_sma( + self.long_sma, self.long_window, price + ) + # Only start the strategy when we have created an accurate short window + if self.ticks > self.short_window: + if self.short_sma > self.long_sma and not self.invested: + signal = SignalEvent(self.pairs[0], "market", "buy", event.time) + self.events.put(signal) + self.invested = True + if self.short_sma < self.long_sma and self.invested: + signal = SignalEvent(self.pairs[0], "market", "sell", event.time) + self.events.put(signal) + self.invested = False + self.ticks += 1 \ No newline at end of file