From e74777802b54d9d65e3733d730941dde709cf549 Mon Sep 17 00:00:00 2001 From: Michael Halls-Moore Date: Fri, 17 Apr 2015 12:34:31 +0100 Subject: [PATCH] Modified the Position handling to use long/short instead of buy/sell for side/position_type. Also modified the unit tests for both Portfolio and Position to reflect these changes. Added a basic historical backtesting capability via backtest.py and using CSV tick data for currency pairs. --- backtest/__init__.py | 0 backtest/backtest.py | 77 +++++++++++ data/__init__.py | 0 data/price.py | 86 ++++++++++++ execution/execution.py | 31 ++++- portfolio/portfolio.py | 113 ++++++++-------- portfolio/portfolio_test.py | 257 +++++++++++++++++++++++------------- portfolio/position.py | 29 ++-- portfolio/position_test.py | 84 ++++++++---- settings.py | 8 +- strategy/strategy.py | 4 +- trading/trading.py | 22 +-- 12 files changed, 519 insertions(+), 192 deletions(-) create mode 100644 backtest/__init__.py create mode 100644 backtest/backtest.py create mode 100644 data/__init__.py create mode 100644 data/price.py diff --git a/backtest/__init__.py b/backtest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backtest/backtest.py b/backtest/backtest.py new file mode 100644 index 0000000..8d0630d --- /dev/null +++ b/backtest/backtest.py @@ -0,0 +1,77 @@ +import copy +import Queue +import threading +import time +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.data.price import HistoricCSVPriceHandler + + +def trade(events, strategy, portfolio, execution, heartbeat): + """ + 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. + """ + while True: + try: + event = events.get(False) + except Queue.Empty: + pass + else: + if event is not None: + if event.type == 'TICK': + strategy.calculate_signals(event) + elif event.type == 'SIGNAL': + portfolio.execute_signal(event) + elif event.type == 'ORDER': + execution.execute_order(event) + time.sleep(heartbeat) + + +if __name__ == "__main__": + # Set the number of decimal places to 2 + getcontext().prec = 2 + + heartbeat = 0.0 # Half a second between polling + events = Queue.Queue() + equity = settings.EQUITY + + # Load the historic CSV tick data files + pairs = ["GBPUSD"] + csv_dir = settings.CSV_DATA_DIR + if csv_dir is None: + print "No historic data directory provided - backtest terminating." + sys.exit() + + # Create the historic tick data streaming class + prices = HistoricCSVPriceHandler(pairs, events, csv_dir) + + # Create the strategy/signal generator, passing the + # instrument and the events queue + strategy = TestStrategy(pairs[0], events) + + # Create the portfolio object to track trades + portfolio = Portfolio(prices, 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 diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/price.py b/data/price.py new file mode 100644 index 0000000..d433be9 --- /dev/null +++ b/data/price.py @@ -0,0 +1,86 @@ +from abc import ABCMeta, abstractmethod +import datetime +from decimal import Decimal, getcontext, ROUND_HALF_DOWN +import os +import os.path +import numpy as np +import pandas as pd + +from qsforex.event.event import TickEvent + + +class PriceHandler(object): + """ + PriceHandler is an abstract base class providing an interface for + all subsequent (inherited) data handlers (both live and historic). + + The goal of a (derived) PriceHandler object is to output a set of + bid/ask/timestamp "ticks" for each currency pair and place them into + an event queue. + + This will replicate how a live strategy would function as current + tick data would be streamed via a brokerage. Thus a historic and live + system will be treated identically by the rest of the QSForex + backtesting suite. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def stream_to_queue(self): + """ + Streams a sequence of tick data events (timestamp, bid, ask) + tuples to the events queue. + """ + raise NotImplementedError("Should implement stream_to_queue()") + + +class HistoricCSVPriceHandler(PriceHandler): + """ + HistoricCSVPriceHandler is designed to read CSV files of + tick data for each requested currency pair and stream those + to the provided events queue. + """ + + def __init__(self, pairs, events_queue, csv_dir): + """ + Initialises the historic data handler by requesting + the location of the CSV files and a list of symbols. + + It will be assumed that all files are of the form + 'pair.csv', where "pair" is the currency pair. For + GBP/USD the filename is GBPUSD.csv. + + Parameters: + pairs - The list of currency pairs to obtain. + events_queue - The events queue to send the ticks to. + csv_dir - Absolute directory path to the CSV files. + """ + self.pairs = pairs + self.events_queue = events_queue + self.csv_dir = csv_dir + self.cur_bid = None + self.cur_ask = None + + def _open_convert_csv_files(self): + """ + Opens the CSV files from the data directory, converting + them into pandas DataFrames within a pairs 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() + + def stream_to_queue(self): + self._open_convert_csv_files() + for index, row in self.pair: + self.cur_bid = Decimal(str(row["Bid"])).quantize( + Decimal("0.00001", ROUND_HALF_DOWN) + ) + self.cur_ask = Decimal(str(row["Ask"])).quantize( + Decimal("0.00001", ROUND_HALF_DOWN) + ) + tev = TickEvent(self.pairs[0], index, row["Bid"], row["Ask"]) + self.events_queue.put(tev) diff --git a/execution/execution.py b/execution/execution.py index b571c08..15dbe4b 100644 --- a/execution/execution.py +++ b/execution/execution.py @@ -1,8 +1,37 @@ +from abc import ABCMeta, abstractmethod import httplib import urllib -class Execution(object): +class ExecutionHandler(object): + """ + Provides an abstract base class to handle all execution in the + backtesting and live trading system. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def execute_order(self): + """ + Send the order to the brokerage. + """ + raise NotImplementedError("Should implement execute_order()") + + +class SimulatedExecution(object): + """ + Provides a simulated execution handling environment. This class + actually does nothing - it simply receives an order to execute. + + Instead, the Portfolio object actually provides fill handling. + This will be modified in later versions. + """ + def execute_order(self, event): + pass + + +class OANDAExecutionHandler(ExecutionHandler): def __init__(self, domain, access_token, account_id): self.domain = domain self.access_token = access_token diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index 4f12a3d..5467cf2 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -24,116 +24,119 @@ class Portfolio(object): return self.equity * self.risk_per_trade def add_new_position( - self, side, market, units, exposure, - add_price, remove_price + self, position_type, market, units, + exposure, bid, ask ): ps = Position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) self.positions[market] = ps def add_position_units( - self, market, units, exposure, - add_price, remove_price + self, market, units, + exposure, bid, ask ): if market 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(remove_price, exposure) + ps.update_position_price(bid, ask, exposure) return True def remove_position_units( - self, market, units, remove_price + self, market, units, bid, ask ): if market 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(remove_price, 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)) return True def close_position( - self, market, remove_price + self, market, bid, ask ): if market not in self.positions: return False else: ps = self.positions[market] - ps.update_position_price(remove_price, ps.exposure) + 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]] return True - def execute_signal(self, signal_event): + def execute_signal(self, signal_event): side = signal_event.side market = signal_event.instrument units = int(self.trade_units) - - # Check side for correct bid/ask prices - if side == "buy": - add_price = Decimal(str(self.ticker.cur_ask)) - remove_price = Decimal(str(self.ticker.cur_bid)) - else: - add_price = Decimal(str(self.ticker.cur_bid)) - remove_price = Decimal(str(self.ticker.cur_ask)) exposure = Decimal(str(units)) + bid = Decimal(str(self.ticker.cur_bid)) + ask = Decimal(str(self.ticker.cur_ask)) # If there is no position, create one if market not in self.positions: + if side == "buy": + position_type = "long" + else: + position_type = "short" self.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) - order = OrderEvent(market, units, "market", side) - self.events.put(order) + # If a position exists add or remove units else: ps = self.positions[market] - # Check if the sides equal - if side == ps.side: - # Add to the position - add_position_units( - market, units, exposure, - add_price, remove_price - ) - else: - # Check if the units close out the position + + if side == "buy" and ps.position_type == "long": + add_position_units(market, units, exposure, bid, ask) + + elif side == "sell" and ps.position_type == "long": if units == ps.units: - # Close the position - self.close_position(market, remove_price) - order = OrderEvent(market, units, "market", side) - self.events.put(order) + self.close_position(market, bid, ask) + # TODO: Allow units to be added/removed elif units < ps.units: - # Remove from the position - self.remove_position_units( - market, units, remove_price - ) - else: # units > ps.units - # Close the position and add a new one with - # additional units of opposite side - new_units = units - ps.units - self.close_position(market, remove_price) + return + elif units > ps.units: + return + + elif side == "buy" and ps.position_type == "short": + if units == ps.units: + self.close_position(market, bid, ask) + # TODO: Allow units to be added/removed + elif units < ps.units: + return + elif units > ps.units: + return - if side == "buy": - new_side = "sell" - else: - new_side = "buy" - new_exposure = Decimal(str(units)) - self.add_new_position( - new_side, market, new_units, - new_exposure, add_price, remove_price - ) + elif side == "sell" and ps.position_type == "short": + add_position_units(market, units, exposure, bid, ask) + + order = OrderEvent(market, units, "market", side) + self.events.put(order) + print "Balance: %0.2f" % self.balance \ No newline at end of file diff --git a/portfolio/portfolio_test.py b/portfolio/portfolio_test.py index 0e7fec9..f5a0b92 100644 --- a/portfolio/portfolio_test.py +++ b/portfolio/portfolio_test.py @@ -18,200 +18,199 @@ class TestPortfolio(unittest.TestCase): ) def test_add_position_long(self): - side = "LONG" + position_type = "long" market = "GBP/USD" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51819") - remove_price = Decimal("1.51770") - + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] - self.assertEquals(ps.side, side) + self.assertEquals(ps.position_type, position_type) self.assertEquals(ps.market, market) self.assertEquals(ps.units, units) self.assertEquals(ps.exposure, exposure) - self.assertEquals(ps.avg_price, add_price) - self.assertEquals(ps.cur_price, remove_price) + self.assertEquals(ps.avg_price, ask) + self.assertEquals(ps.cur_price, bid) def test_add_position_short(self): - side = "SHORT" + position_type = "short" market = "GBP/USD" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51770") - remove_price = Decimal("1.51819") + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] - self.assertEquals(ps.side, side) + self.assertEquals(ps.position_type, position_type) self.assertEquals(ps.market, market) self.assertEquals(ps.units, units) self.assertEquals(ps.exposure, exposure) - self.assertEquals(ps.avg_price, add_price) - self.assertEquals(ps.cur_price, remove_price) + self.assertEquals(ps.avg_price, bid) + self.assertEquals(ps.cur_price, ask) def test_add_position_units_long(self): - side = "LONG" + position_type = "long" market = "GBP/USD" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51819") - remove_price = Decimal("1.51770") + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") # Test for no position market = "EUR/USD" apu = self.port.add_position_units( market, units, exposure, - add_price, remove_price + bid, ask ) self.assertFalse(apu) # Add a position and test for real position market = "GBP/USD" self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] # Test for addition of units - add_price = Decimal("1.51928") - remove_price = Decimal("1.51878") + bid = Decimal("1.51878") + ask = Decimal("1.51928") apu = self.port.add_position_units( market, units, exposure, - add_price, remove_price + bid, ask ) self.assertTrue(apu) self.assertEqual(ps.avg_price, Decimal("1.518735")) def test_add_position_units_short(self): - side = "SHORT" + position_type = "short" market = "GBP/USD" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51770") - remove_price = Decimal("1.51819") + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") # Test for no position market = "EUR/USD" apu = self.port.add_position_units( market, units, exposure, - add_price, remove_price + bid, ask ) self.assertFalse(apu) # Add a position and test for real position market = "GBP/USD" self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] # Test for addition of units - add_price = Decimal("1.51878") - remove_price = Decimal("1.51928") + bid = Decimal("1.51878") + ask = Decimal("1.51928") apu = self.port.add_position_units( market, units, exposure, - add_price, remove_price + bid, ask ) self.assertTrue(apu) self.assertEqual(ps.avg_price, Decimal("1.51824")) def test_remove_position_units_long(self): - side = "LONG" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51819") - remove_price = Decimal("1.51770") + position_type = "long" + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") # Test for no position market = "EUR/USD" apu = self.port.remove_position_units( - market, units, remove_price + market, units, bid, ask ) self.assertFalse(apu) # Add a position and then add units to it market = "GBP/USD" self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] - add_price = Decimal("1.51928") - remove_price = Decimal("1.51878") + bid = Decimal("1.51878") + ask = Decimal("1.51928") add_units = 8000 add_exposure = Decimal(str(add_units)) apu = self.port.add_position_units( market, add_units, add_exposure, - add_price, remove_price + bid, ask ) self.assertEqual(ps.units, 10000) self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.519062")) # Test removal of (some) of the units - add_price = Decimal("1.52134") - remove_price = Decimal("1.52017") + bid = Decimal("1.52017") + ask = Decimal("1.52134") remove_units = 3000 rpu = self.port.remove_position_units( - market, remove_units, remove_price + market, remove_units, bid, ask ) 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")) - + def test_remove_position_units_short(self): - side = "SHORT" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51770") - remove_price = Decimal("1.51819") + position_type = "short" + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") # Test for no position market = "EUR/USD" apu = self.port.remove_position_units( - market, units, remove_price + market, units, bid, ask ) self.assertFalse(apu) # Add a position and then add units to it market = "GBP/USD" self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] - add_price = Decimal("1.51878") - remove_price = Decimal("1.51928") + bid = Decimal("1.51878") + ask = Decimal("1.51928") add_units = 8000 add_exposure = Decimal(str(add_units)) apu = self.port.add_position_units( market, add_units, add_exposure, - add_price, remove_price + bid, ask ) self.assertEqual(ps.units, 10000) self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.518564")) # Test removal of (some) of the units - add_price = Decimal("1.52017") - remove_price = Decimal("1.52134") + bid = Decimal("1.52017") + ask = Decimal("1.52134") remove_units = 3000 rpu = self.port.remove_position_units( - market, remove_units, remove_price + market, remove_units, bid, ask ) self.assertTrue(rpu) self.assertEqual(ps.units, 7000) @@ -220,16 +219,16 @@ class TestPortfolio(unittest.TestCase): self.assertEqual(self.port.balance, Decimal("99994.52")) def test_close_position_long(self): - side = "LONG" - units = 2000 - exposure = Decimal(str(units)) - add_price = Decimal("1.51819") - remove_price = Decimal("1.51770") + position_type = "long" + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") # Test for no position market = "EUR/USD" cp = self.port.close_position( - market, remove_price + market, bid, ask ) self.assertFalse(cp) @@ -237,12 +236,12 @@ class TestPortfolio(unittest.TestCase): # Will lose money on the spread market = "GBP/USD" self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] cp = self.port.close_position( - market, remove_price + market, bid, ask ) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist @@ -252,38 +251,114 @@ class TestPortfolio(unittest.TestCase): # close the position. Balance should be as expected # for a multi-leg transaction. self.port.add_new_position( - side, market, units, exposure, - add_price, remove_price + position_type, market, units, + exposure, bid, ask ) ps = self.port.positions[market] - add_price = Decimal("1.51928") - remove_price = Decimal("1.51878") + + # Add 8000 units + bid = Decimal("1.51878") + ask = Decimal("1.51928") add_units = 8000 add_exposure = Decimal(str(add_units)) apu = self.port.add_position_units( - market, add_units, add_exposure, - add_price, remove_price + market, add_units, + add_exposure, bid, ask ) self.assertEqual(ps.units, 10000) self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.519062")) - add_price = Decimal("1.52134") - remove_price = Decimal("1.52017") + + # Remove 3000 units + bid = Decimal("1.52017") + ask = Decimal("1.52134") remove_units = 3000 rpu = self.port.remove_position_units( - market, remove_units, remove_price + market, remove_units, bid, ask ) 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")) + + # Close the position cp = self.port.close_position( - market, remove_price + market, bid, ask ) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist self.assertEqual(self.port.balance, Decimal("100006.65")) + def test_close_position_short(self): + position_type = "short" + units = Decimal("2000") + exposure = Decimal("2000.00") + bid = Decimal("1.51770") + ask = Decimal("1.51819") + + # Test for no position + market = "EUR/USD" + cp = self.port.close_position( + market, bid, ask + ) + self.assertFalse(cp) + + # Add a position and then close it + # Will lose money on the spread + market = "GBP/USD" + self.port.add_new_position( + position_type, market, units, + exposure, bid, ask + ) + 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)) + apu = self.port.add_position_units( + market, add_units, + add_exposure, bid, ask + ) + self.assertEqual(ps.units, 10000) + self.assertEqual(ps.exposure, Decimal("10000.00")) + self.assertEqual(ps.avg_price, Decimal("1.518564")) + + # Remove 3000 units + bid = Decimal("1.52017") + ask = Decimal("1.52134") + remove_units = 3000 + rpu = self.port.remove_position_units( + market, remove_units, bid, ask + ) + 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")) + + # Close the position + cp = self.port.close_position( + market, bid, ask + ) + self.assertTrue(cp) + self.assertRaises(ps) # Key doesn't exist + self.assertEqual(self.port.balance, Decimal("99981.08")) if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/portfolio/position.py b/portfolio/position.py index 6d14e4e..bdc4021 100644 --- a/portfolio/position.py +++ b/portfolio/position.py @@ -3,22 +3,31 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN class Position(object): def __init__( - self, side, market, units, - exposure, avg_price, cur_price + self, position_type, market, + units, exposure, bid, ask ): - self.side = side + self.position_type = position_type # Long or short self.market = market self.units = units self.exposure = Decimal(str(exposure)) - self.avg_price = Decimal(str(avg_price)) - self.cur_price = Decimal(str(cur_price)) + + # Long or short + if self.position_type == "long": + self.avg_price = Decimal(str(ask)) + self.cur_price = Decimal(str(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) def calculate_pips(self): getcontext.prec = 6 mult = Decimal("1") - if self.side == "SHORT": + 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( Decimal("0.00001"), ROUND_HALF_DOWN @@ -35,8 +44,10 @@ class Position(object): Decimal("0.00001"), ROUND_HALF_DOWN ) - def update_position_price(self, cur_price, exposure): - self.cur_price = cur_price + def update_position_price(self, bid, ask, exposure): + if self.position_type == "long": + self.cur_price = Decimal(str(bid)) + else: + self.cur_price = Decimal(str(ask)) self.profit_base = self.calculate_profit_base(exposure) self.profit_perc = self.calculate_profit_perc(exposure) - diff --git a/portfolio/position_test.py b/portfolio/position_test.py index 1040510..1c930aa 100644 --- a/portfolio/position_test.py +++ b/portfolio/position_test.py @@ -7,55 +7,91 @@ from position import Position class TestLongGBPUSDPosition(unittest.TestCase): def setUp(self): getcontext.prec = 2 - side = "LONG" + position_type = "long" market = "GBP/USD" - units = Decimal(str(2000)) + units = Decimal("2000") exposure = Decimal("2000.00") - avg_price = Decimal("1.51819") - cur_price = Decimal("1.51770") + bid = Decimal("1.50328") + ask = Decimal("1.50349") self.position = Position( - side, market, units, exposure, - avg_price, cur_price + position_type, market, + units, exposure, bid, ask ) - def test_calculate_pips(self): + def test_calculate_init_pips(self): pos_pips = self.position.calculate_pips() - self.assertEqual(pos_pips, Decimal("-0.00049")) + self.assertEqual(pos_pips, Decimal("-0.00021")) - def test_calculate_profit_base(self): + def test_calculate_init_profit_base(self): profit_base = self.position.calculate_profit_base(self.position.exposure) - self.assertEqual(profit_base, Decimal("-0.64571")) + self.assertEqual(profit_base, Decimal("-0.27939")) - def test_calculate_profit_perc(self): + def test_calculate_init_profit_perc(self): profit_perc = self.position.calculate_profit_perc(self.position.exposure) - self.assertEqual(profit_perc, Decimal("-0.03229")) + self.assertEqual(profit_perc, Decimal("-0.01397")) + + 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. + """ + bid = Decimal("1.50486") + ask = Decimal("1.50586") + self.position.update_position_price(bid, ask, self.position.exposure) + # 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")) + # Check profit percentage + profit_perc = self.position.calculate_profit_perc(self.position.exposure) + self.assertEqual(profit_perc, Decimal("0.09104")) class TestShortGBPUSDPosition(unittest.TestCase): def setUp(self): getcontext.prec = 2 - side = "SHORT" + position_type = "short" market = "GBP/USD" - units = 2000 + units = Decimal("2000") exposure = Decimal("2000.00") - avg_price = Decimal("1.51819") - cur_price = Decimal("1.51770") + bid = Decimal("1.50328") + ask = Decimal("1.50349") self.position = Position( - side, market, units, exposure, - avg_price, cur_price + position_type, market, + units, exposure, bid, ask ) - def test_calculate_pips(self): + def test_calculate_init_pips(self): pos_pips = self.position.calculate_pips() - self.assertEqual(pos_pips, Decimal("0.00049")) + self.assertEqual(pos_pips, Decimal("-0.00021")) - def test_calculate_profit_base(self): + def test_calculate_init_profit_base(self): profit_base = self.position.calculate_profit_base(self.position.exposure) - self.assertEqual(profit_base, Decimal("0.64571")) + self.assertEqual(profit_base, Decimal("-0.27935")) - def test_calculate_profit_perc(self): + def test_calculate_init_profit_perc(self): profit_perc = self.position.calculate_profit_perc(self.position.exposure) - self.assertEqual(profit_perc, Decimal("0.03229")) + self.assertEqual(profit_perc, Decimal("-0.01397")) + + 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. + """ + bid = Decimal("1.50486") + ask = Decimal("1.50586") + self.position.update_position_price(bid, ask, self.position.exposure) + # 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")) + # Check profit percentage + profit_perc = self.position.calculate_profit_perc(self.position.exposure) + self.assertEqual(profit_perc, Decimal("-0.17133")) if __name__ == "__main__": diff --git a/settings.py b/settings.py index c7be22d..bc3df2a 100644 --- a/settings.py +++ b/settings.py @@ -1,3 +1,4 @@ +from decimal import Decimal import os @@ -14,8 +15,13 @@ ENVIRONMENTS = { } } +CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None) + DOMAIN = "practice" STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN] API_DOMAIN = ENVIRONMENTS["api"][DOMAIN] ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None) -ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None) \ No newline at end of file +ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None) + +BASE_CURRENCY = "GBP" +EQUITY = Decimal("100000.00") diff --git a/strategy/strategy.py b/strategy/strategy.py index 0db1426..7ebce96 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -10,7 +10,7 @@ class TestStrategy(object): def calculate_signals(self, event): if event.type == 'TICK': - if self.ticks % 200 == 0: + if self.ticks % 5 == 0: if self.invested == False: signal = SignalEvent(self.instrument, "market", "buy") self.events.put(signal) @@ -19,4 +19,4 @@ class TestStrategy(object): signal = SignalEvent(self.instrument, "market", "sell") self.events.put(signal) self.invested = False - self.ticks += 1 \ No newline at end of file + self.ticks += 1 diff --git a/trading/trading.py b/trading/trading.py index 01e95d4..f38d4f6 100644 --- a/trading/trading.py +++ b/trading/trading.py @@ -4,14 +4,14 @@ import threading import time from decimal import Decimal, getcontext -from qsforex.execution.execution import Execution +from qsforex.execution.execution import OANDAExecutionHandler from qsforex.portfolio.portfolio import Portfolio -from qsforex.settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID +from qsforex import settings from qsforex.strategy.strategy import TestStrategy from qsforex.streaming.streaming import StreamingForexPrices -def trade(events, strategy, portfolio, execution): +def trade(events, strategy, portfolio, execution, heartbeat): """ Carries out an infinite while loop that polls the events queue and directs each event to either the @@ -39,9 +39,9 @@ if __name__ == "__main__": # Set the number of decimal places to 2 getcontext().prec = 2 - heartbeat = 0.5 # Half a second between polling + heartbeat = 0.0 # Half a second between polling events = Queue.Queue() - equity = Decimal("99949.82") + equity = settings.EQUITY # Trade "Cable" instrument = "GBP_USD" @@ -49,8 +49,8 @@ if __name__ == "__main__": # Create the OANDA market price streaming class # making sure to provide authentication commands prices = StreamingForexPrices( - STREAM_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID, - instrument, events + settings.STREAM_DOMAIN, settings.ACCESS_TOKEN, + settings.ACCOUNT_ID, instrument, events ) # Create the strategy/signal generator, passing the @@ -64,13 +64,17 @@ if __name__ == "__main__": # Create the execution handler making sure to # provide authentication commands - execution = Execution(API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID) + execution = OANDAExecutionHandler( + settings.API_DOMAIN, + settings.ACCESS_TOKEN, + settings.ACCOUNT_ID + ) # 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 + events, strategy, portfolio, execution, heartbeat ) ) price_thread = threading.Thread(target=prices.stream_to_queue, args=[])