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.

This commit is contained in:
Michael Halls-Moore
2015-04-21 13:01:20 +01:00
parent e74777802b
commit e84512e1e7
11 changed files with 700 additions and 376 deletions
+20 -23
View File
@@ -7,19 +7,25 @@ from decimal import Decimal, getcontext
from qsforex.execution.execution import SimulatedExecution from qsforex.execution.execution import SimulatedExecution
from qsforex.portfolio.portfolio import Portfolio from qsforex.portfolio.portfolio import Portfolio
from qsforex import settings from qsforex import settings
from qsforex.strategy.strategy import TestStrategy from qsforex.strategy.strategy import TestStrategy, MovingAverageCrossStrategy
from qsforex.data.price import HistoricCSVPriceHandler 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 Carries out an infinite while loop that polls the
events queue and directs each event to either the events queue and directs each event to either the
strategy component of the execution handler. The strategy component of the execution handler. The
loop will then pause for "heartbeat" seconds and 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: try:
event = events.get(False) event = events.get(False)
except Queue.Empty: except Queue.Empty:
@@ -33,13 +39,12 @@ def trade(events, strategy, portfolio, execution, heartbeat):
elif event.type == 'ORDER': elif event.type == 'ORDER':
execution.execute_order(event) execution.execute_order(event)
time.sleep(heartbeat) time.sleep(heartbeat)
iters += 1
portfolio.output_results()
if __name__ == "__main__": if __name__ == "__main__":
# Set the number of decimal places to 2 heartbeat = 0.0
getcontext().prec = 2
heartbeat = 0.0 # Half a second between polling
events = Queue.Queue() events = Queue.Queue()
equity = settings.EQUITY equity = settings.EQUITY
@@ -51,27 +56,19 @@ if __name__ == "__main__":
sys.exit() sys.exit()
# Create the historic tick data streaming class # 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 # Create the strategy/signal generator, passing the
# instrument and the events queue # instrument and the events queue
strategy = TestStrategy(pairs[0], events) strategy = MovingAverageCrossStrategy(
pairs, events, 500, 2000
)
# Create the portfolio object to track trades # Create the portfolio object to track trades
portfolio = Portfolio(prices, events, equity=equity) portfolio = Portfolio(ticker, events, equity=equity)
# Create the simulated execution handler # Create the simulated execution handler
execution = SimulatedExecution() execution = SimulatedExecution()
# Create two separate threads: One for the trading loop # Carry out the backtest loop
# and another for the market price streaming class backtest(events, ticker, strategy, portfolio, execution, heartbeat)
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()
+24
View File
@@ -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()
+104 -11
View File
@@ -3,6 +3,8 @@ 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 time
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -59,28 +61,119 @@ class HistoricCSVPriceHandler(PriceHandler):
self.pairs = pairs self.pairs = pairs
self.events_queue = events_queue self.events_queue = events_queue
self.csv_dir = csv_dir self.csv_dir = csv_dir
self.cur_bid = None self.prices = self._set_up_prices_dict()
self.cur_ask = None 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): def _open_convert_csv_files(self):
""" """
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.
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]) for p in self.pairs:
self.pair = pd.io.parsers.read_csv( pair_path = os.path.join(self.csv_dir, '%s.csv' % p)
pair_path, header=True, index_col=0, parse_dates=True, self.pair_frames[p] = pd.io.parsers.read_csv(
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume") pair_path, header=True, index_col=0, parse_dates=True,
).iterrows() 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): def stream_to_queue(self):
self._open_convert_csv_files() self._open_convert_csv_files()
for index, row in self.pair: for index, row in self.all_pairs:
self.cur_bid = Decimal(str(row["Bid"])).quantize( self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN) 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) 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) self.events_queue.put(tev)
+2 -1
View File
@@ -12,11 +12,12 @@ class TickEvent(Event):
class SignalEvent(Event): class SignalEvent(Event):
def __init__(self, instrument, order_type, side): def __init__(self, instrument, order_type, side, time):
self.type = 'SIGNAL' self.type = 'SIGNAL'
self.instrument = instrument self.instrument = instrument
self.order_type = order_type self.order_type = order_type
self.side = side self.side = side
self.time = time # Time of the last tick that generated the signal
class OrderEvent(Event): class OrderEvent(Event):
+49 -64
View File
@@ -1,123 +1,107 @@
from copy import deepcopy from copy import deepcopy
from decimal import Decimal, getcontext, ROUND_HALF_DOWN from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import os
import pandas as pd
from qsforex.event.event import OrderEvent from qsforex.event.event import OrderEvent
from qsforex.portfolio.position import Position from qsforex.portfolio.position import Position
from qsforex.settings import OUTPUT_RESULTS_DIR
class Portfolio(object): class Portfolio(object):
def __init__( 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") equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02")
): ):
self.ticker = ticker self.ticker = ticker
self.events = events self.events = events
self.base = base self.home_currency = home_currency
self.leverage = leverage self.leverage = leverage
self.equity = equity self.equity = equity
self.balance = deepcopy(self.equity) self.balance = deepcopy(self.equity)
self.risk_per_trade = risk_per_trade self.risk_per_trade = risk_per_trade
self.trade_units = self.calc_risk_position_size() self.trade_units = self.calc_risk_position_size()
self.positions = {} self.positions = {}
self.equity = []
def calc_risk_position_size(self): def calc_risk_position_size(self):
return self.equity * self.risk_per_trade return self.equity * self.risk_per_trade
def add_new_position( def add_new_position(
self, position_type, market, units, self, position_type, currency_pair, units, ticker
exposure, bid, ask
): ):
ps = Position( ps = Position(
position_type, market, units, self.home_currency, position_type,
exposure, bid, ask currency_pair, units, ticker
) )
self.positions[market] = ps self.positions[currency_pair] = ps
def add_position_units( def add_position_units(self, currency_pair, units):
self, market, units, if currency_pair not in self.positions:
exposure, bid, ask
):
if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] ps = self.positions[currency_pair]
if ps.position_type == "long": ps.add_units(units)
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)
return True return True
def remove_position_units( def remove_position_units(self, currency_pair, units):
self, market, units, bid, ask if currency_pair not in self.positions:
):
if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] ps = self.positions[currency_pair]
if ps.position_type == "long": pnl = ps.remove_units(units)
remove_price = bid self.balance += pnl
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))
return True return True
def close_position( def close_position(self, currency_pair):
self, market, bid, ask if currency_pair not in self.positions:
):
if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] ps = self.positions[currency_pair]
ps.update_position_price(bid, ask, ps.exposure) pnl = ps.close_position()
if ps.position_type == "long": self.balance += pnl
remove_price = bid del[self.positions[currency_pair]]
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 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): def execute_signal(self, signal_event):
side = signal_event.side side = signal_event.side
market = signal_event.instrument currency_pair = signal_event.instrument
units = int(self.trade_units) units = int(self.trade_units)
exposure = Decimal(str(units)) time = signal_event.time
bid = Decimal(str(self.ticker.cur_bid))
ask = Decimal(str(self.ticker.cur_ask))
# If there is no position, create one # If there is no position, create one
if market not in self.positions: if currency_pair not in self.positions:
if side == "buy": if side == "buy":
position_type = "long" position_type = "long"
else: else:
position_type = "short" position_type = "short"
self.add_new_position( self.add_new_position(
position_type, market, units, position_type, currency_pair,
exposure, bid, ask units, self.ticker
) )
# If a position exists add or remove units # If a position exists add or remove units
else: else:
ps = self.positions[market] ps = self.positions[currency_pair]
if side == "buy" and ps.position_type == "long": 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": elif side == "sell" and ps.position_type == "long":
if units == ps.units: if units == ps.units:
self.close_position(market, bid, ask) self.close_position(currency_pair)
# TODO: Allow units to be added/removed # TODO: Allow units to be added/removed
elif units < ps.units: elif units < ps.units:
return return
@@ -126,7 +110,7 @@ class Portfolio(object):
elif side == "buy" and ps.position_type == "short": elif side == "buy" and ps.position_type == "short":
if units == ps.units: if units == ps.units:
self.close_position(market, bid, ask) self.close_position(currency_pair)
# TODO: Allow units to be added/removed # TODO: Allow units to be added/removed
elif units < ps.units: elif units < ps.units:
return return
@@ -134,9 +118,10 @@ class Portfolio(object):
return return
elif side == "sell" and ps.position_type == "short": 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) self.events.put(order)
print "Balance: %0.2f" % self.balance print "Balance: %0.2f" % self.balance
self.append_equity_row(time, self.balance)
+171 -210
View File
@@ -1,364 +1,325 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import unittest 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): class TestPortfolio(unittest.TestCase):
def setUp(self): def setUp(self):
base = "GBP" home_currency = "GBP"
leverage = 20 leverage = 20
equity = Decimal("100000.00") equity = Decimal("100000.00")
risk_per_trade = Decimal("0.02") risk_per_trade = Decimal("0.02")
ticker = {} ticker = TickerMock()
events = {} events = {}
self.port = Portfolio( self.port = Portfolio(
ticker, events, base=base, leverage=leverage, ticker, events, home_currency=home_currency,
equity=equity, risk_per_trade=risk_per_trade leverage=leverage, equity=equity,
risk_per_trade=risk_per_trade
) )
def test_add_position_long(self): def test_add_position_long(self):
position_type = "long" position_type = "long"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask 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.position_type, position_type)
self.assertEquals(ps.market, market) self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units) self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure) self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["ask"])
self.assertEquals(ps.avg_price, ask) self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["bid"])
self.assertEquals(ps.cur_price, bid)
def test_add_position_short(self): def test_add_position_short(self):
position_type = "short" position_type = "short"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask 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.position_type, position_type)
self.assertEquals(ps.market, market) self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units) self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure) self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["bid"])
self.assertEquals(ps.avg_price, bid) self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["ask"])
self.assertEquals(ps.cur_price, ask)
def test_add_position_units_long(self): def test_add_position_units_long(self):
position_type = "long" position_type = "long"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, alt_currency_pair, units
bid, ask
) )
self.assertFalse(apu) self.assertFalse(apu)
# Add a position and test for real position # Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
# Test for addition of units # Test for addition of units
bid = Decimal("1.51878") ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ask = Decimal("1.51928") 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( apu = self.port.add_position_units(
market, units, exposure, currency_pair, units
bid, ask
) )
self.assertTrue(apu) 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): def test_add_position_units_short(self):
position_type = "short" position_type = "short"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, alt_currency_pair, units
bid, ask
) )
self.assertFalse(apu) self.assertFalse(apu)
# Add a position and test for real position # Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
# Test for addition of units # Test for addition of units
bid = Decimal("1.51878") ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ask = Decimal("1.51928") 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( apu = self.port.add_position_units(
market, units, exposure, currency_pair, units
bid, ask
) )
self.assertTrue(apu) 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): def test_remove_position_units_long(self):
position_type = "long" position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units( apu = self.port.remove_position_units(
market, units, bid, ask alt_currency_pair, units
) )
self.assertFalse(apu) self.assertFalse(apu)
# Add a position and then add units to it # Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
bid = Decimal("1.51878") # Test for addition of units
ask = Decimal("1.51928") ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
add_units = 8000 ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
add_exposure = Decimal(str(add_units)) ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, add_units, add_exposure, currency_pair, add_units
bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.516122"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
# Test removal of (some) of the units # Test removal of (some) of the units
bid = Decimal("1.52017") ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ask = Decimal("1.52134") ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
remove_units = 3000 ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, bid, ask currency_pair, remove_units
) )
self.assertTrue(rpu) self.assertTrue(rpu)
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(self.port.balance, Decimal("100007.99"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100002.19"))
def test_remove_position_units_short(self): def test_remove_position_units_short(self):
position_type = "short" position_type = "short"
currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units( apu = self.port.remove_position_units(
market, units, bid, ask alt_currency_pair, units
) )
self.assertFalse(apu) self.assertFalse(apu)
# Add a position and then add units to it # Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
bid = Decimal("1.51878") # Test for addition of units
ask = Decimal("1.51928") ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
add_units = 8000 ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
add_exposure = Decimal(str(add_units)) ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, add_units, add_exposure, currency_pair, add_units
bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.51568"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
# Test removal of (some) of the units # Test removal of (some) of the units
bid = Decimal("1.52017") ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ask = Decimal("1.52134") ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
remove_units = 3000 ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, bid, ask currency_pair, remove_units
) )
self.assertTrue(rpu) self.assertTrue(rpu)
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(self.port.balance, Decimal("99988.84"))
self.assertEqual(ps.profit_base, Decimal("-5.48201"))
self.assertEqual(self.port.balance, Decimal("99994.52"))
def test_close_position_long(self): def test_close_position_long(self):
position_type = "long" position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
cp = self.port.close_position( apu = self.port.remove_position_units(
market, bid, ask alt_currency_pair, units
) )
self.assertFalse(cp) self.assertFalse(apu)
# Add a position and then close it # Add a position and then add units to it
# Will lose money on the spread
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
cp = self.port.close_position( # Test for addition of units
market, bid, ask ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
) ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
self.assertTrue(cp) ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
self.assertRaises(ps) # Key doesn't exist ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
self.assertEqual(self.port.balance, Decimal("99999.35"))
# Add 2000, add another 8000, remove 3000 and then add_units = Decimal("8000")
# 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( apu = self.port.add_position_units(
market, add_units, currency_pair, add_units
add_exposure, bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.516122"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
# Remove 3000 units # Test removal of (some) of the units
bid = Decimal("1.52017") ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ask = Decimal("1.52134") ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
remove_units = 3000 ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, bid, ask currency_pair, remove_units
) )
self.assertEqual(ps.units, 7000) self.assertTrue(rpu)
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(ps.profit_base, Decimal("2.19054")) self.assertEqual(self.port.balance, Decimal("100007.99"))
self.assertEqual(self.port.balance, Decimal("100001.54"))
# Close the position # Close the position
cp = self.port.close_position( cp = self.port.close_position(currency_pair)
market, bid, ask
)
self.assertTrue(cp) self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist 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): def test_close_position_short(self):
position_type = "short" position_type = "short"
currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.51770")
ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" alt_currency_pair = "USDCAD"
cp = self.port.close_position( apu = self.port.remove_position_units(
market, bid, ask alt_currency_pair, units
) )
self.assertFalse(cp) self.assertFalse(apu)
# Add a position and then close it # Add a position and then add units to it
# Will lose money on the spread
market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
position_type, market, units, position_type,
exposure, bid, ask currency_pair,
units, ticker
) )
ps = self.port.positions[market] ps = self.port.positions[currency_pair]
cp = self.port.close_position( # Test for addition of units
market, bid, ask ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
) ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
self.assertTrue(cp) ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
self.assertRaises(ps) # Key doesn't exist ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
self.assertEqual(self.port.balance, Decimal("99999.35"))
# Add 2000, add another 8000, remove 3000 and then add_units = Decimal("8000")
# 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( apu = self.port.add_position_units(
market, add_units, currency_pair, add_units
add_exposure, bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.avg_price, Decimal("1.51568"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
# Remove 3000 units # Test removal of (some) of the units
bid = Decimal("1.52017") ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ask = Decimal("1.52134") ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
remove_units = 3000 ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, bid, ask currency_pair, remove_units
) )
self.assertEqual(ps.units, 7000) self.assertTrue(rpu)
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(ps.profit_base, Decimal("-5.48201")) self.assertEqual(self.port.balance, Decimal("99988.84"))
self.assertEqual(self.port.balance, Decimal("99993.87"))
# Close the position # Close the position
cp = self.port.close_position( cp = self.port.close_position(currency_pair)
market, bid, ask
)
self.assertTrue(cp) self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+85 -29
View File
@@ -3,51 +3,107 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN
class Position(object): class Position(object):
def __init__( def __init__(
self, position_type, market, self, home_currency, position_type,
units, exposure, bid, ask currency_pair, units, ticker
): ):
self.home_currency = home_currency # Account denomination (e.g. GBP)
self.position_type = position_type # Long or short self.position_type = position_type # Long or short
self.market = market self.currency_pair = currency_pair # Intended traded currency pair
self.units = units 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": if self.position_type == "long":
self.avg_price = Decimal(str(ask)) self.avg_price = Decimal(str(ticker_cur["ask"]))
self.cur_price = Decimal(str(bid)) self.cur_price = Decimal(str(ticker_cur["bid"]))
else: else:
self.avg_price = Decimal(str(bid)) self.avg_price = Decimal(str(ticker_cur["bid"]))
self.cur_price = Decimal(str(ask)) self.cur_price = Decimal(str(ticker_cur["ask"]))
self.profit_base = self.calculate_profit_base(self.exposure)
self.profit_perc = self.calculate_profit_perc(self.exposure)
def calculate_pips(self): def calculate_pips(self):
getcontext.prec = 6
mult = Decimal("1") mult = Decimal("1")
if self.position_type == "long": if self.position_type == "long":
mult = Decimal("1") mult = Decimal("1")
elif self.position_type == "short": elif self.position_type == "short":
mult = Decimal("-1") 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 Decimal("0.00001"), ROUND_HALF_DOWN
) )
return pips
def calculate_profit_base(self, exposure): def calculate_profit_base(self):
pips = self.calculate_pips() pips = self.calculate_pips()
return (pips * exposure / self.cur_price).quantize( ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
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):
if self.position_type == "long": if self.position_type == "long":
self.cur_price = Decimal(str(bid)) qh_close = ticker_qh["bid"]
else: else:
self.cur_price = Decimal(str(ask)) qh_close = ticker_qh["ask"]
self.profit_base = self.calculate_profit_base(exposure) profit = pips * qh_close * self.units
self.profit_perc = self.calculate_profit_perc(exposure) 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))
+162 -28
View File
@@ -4,18 +4,39 @@ import unittest
from position import Position 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): 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): def setUp(self):
getcontext.prec = 2 getcontext.prec = 2
home_currency = "GBP"
position_type = "long" position_type = "long"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.50328")
ask = Decimal("1.50349")
self.position = Position( self.position = Position(
position_type, market, home_currency, position_type,
units, exposure, bid, ask currency_pair, units, ticker
) )
def test_calculate_init_pips(self): def test_calculate_init_pips(self):
@@ -23,11 +44,11 @@ class TestLongGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("-0.00021")) self.assertEqual(pos_pips, Decimal("-0.00021"))
def test_calculate_init_profit_base(self): 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")) self.assertEqual(profit_base, Decimal("-0.27939"))
def test_calculate_init_profit_perc(self): 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")) self.assertEqual(profit_perc, Decimal("-0.01397"))
def test_calculate_updated_values(self): 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 Check that after the bid/ask prices move, that the updated
pips, profit and percentage profit calculations are correct. pips, profit and percentage profit calculations are correct.
""" """
bid = Decimal("1.50486") prices = self.position.ticker.prices
ask = Decimal("1.50586") prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")}
self.position.update_position_price(bid, ask, self.position.exposure) prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")}
self.position.update_position_price()
# Check pips # Check pips
pos_pips = self.position.calculate_pips() pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("0.00137")) self.assertEqual(pos_pips, Decimal("0.00137"))
# Check profit base # Check profit base
profit_base = self.position.calculate_profit_base(self.position.exposure) profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("1.82077")) self.assertEqual(profit_base, Decimal("1.82076"))
# Check profit percentage # 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")) self.assertEqual(profit_perc, Decimal("0.09104"))
class TestShortGBPUSDPosition(unittest.TestCase): 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): def setUp(self):
getcontext.prec = 2 getcontext.prec = 2
home_currency = "GBP"
position_type = "short" position_type = "short"
market = "GBP/USD" currency_pair = "GBPUSD"
units = Decimal("2000") units = Decimal("2000")
exposure = Decimal("2000.00") ticker = TickerMock()
bid = Decimal("1.50328")
ask = Decimal("1.50349")
self.position = Position( self.position = Position(
position_type, market, home_currency, position_type,
units, exposure, bid, ask currency_pair, units, ticker
) )
def test_calculate_init_pips(self): def test_calculate_init_pips(self):
@@ -68,11 +94,11 @@ class TestShortGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("-0.00021")) self.assertEqual(pos_pips, Decimal("-0.00021"))
def test_calculate_init_profit_base(self): 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")) self.assertEqual(profit_base, Decimal("-0.27935"))
def test_calculate_init_profit_perc(self): 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")) self.assertEqual(profit_perc, Decimal("-0.01397"))
def test_calculate_updated_values(self): 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 Check that after the bid/ask prices move, that the updated
pips, profit and percentage profit calculations are correct. pips, profit and percentage profit calculations are correct.
""" """
bid = Decimal("1.50486") prices = self.position.ticker.prices
ask = Decimal("1.50586") prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")}
self.position.update_position_price(bid, ask, self.position.exposure) prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")}
self.position.update_position_price()
# Check pips # Check pips
pos_pips = self.position.calculate_pips() pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("-0.00258")) self.assertEqual(pos_pips, Decimal("-0.00258"))
# Check profit base # Check profit base
profit_base = self.position.calculate_profit_base(self.position.exposure) profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-3.42661")) self.assertEqual(profit_base, Decimal("-3.42660"))
# Check profit percentage # 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")) 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+4
View File
@@ -1,7 +1,11 @@
argparse==1.2.1 argparse==1.2.1
ipython==2.3.1 ipython==2.3.1
matplotlib==1.4.3
mock==1.0.1
nose==1.3.6
numpy==1.9.1 numpy==1.9.1
pandas==0.15.2 pandas==0.15.2
pyparsing==2.0.3
python-dateutil==2.4.0 python-dateutil==2.4.0
pytz==2014.10 pytz==2014.10
requests==2.5.1 requests==2.5.1
+1
View File
@@ -16,6 +16,7 @@ ENVIRONMENTS = {
} }
CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None) CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None)
OUTPUT_RESULTS_DIR = os.environ.get('QSFOREX_OUTPUT_RESULTS_DIR', None)
DOMAIN = "practice" DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN] STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
+72 -4
View File
@@ -2,8 +2,17 @@ from qsforex.event.event import SignalEvent
class TestStrategy(object): 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.events = events
self.ticks = 0 self.ticks = 0
self.invested = False self.invested = False
@@ -12,11 +21,70 @@ class TestStrategy(object):
if event.type == 'TICK': if event.type == 'TICK':
if self.ticks % 5 == 0: if self.ticks % 5 == 0:
if self.invested == False: if self.invested == False:
signal = SignalEvent(self.instrument, "market", "buy") signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
self.events.put(signal) self.events.put(signal)
self.invested = True self.invested = True
else: 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.events.put(signal)
self.invested = False self.invested = False
self.ticks += 1 self.ticks += 1