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.

This commit is contained in:
Michael Halls-Moore
2015-04-17 12:34:31 +01:00
parent d9a7444fc2
commit e74777802b
12 changed files with 519 additions and 192 deletions
View File
+77
View File
@@ -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()
View File
+86
View File
@@ -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)
+30 -1
View File
@@ -1,8 +1,37 @@
from abc import ABCMeta, abstractmethod
import httplib import httplib
import urllib 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): def __init__(self, domain, access_token, account_id):
self.domain = domain self.domain = domain
self.access_token = access_token self.access_token = access_token
+58 -55
View File
@@ -24,116 +24,119 @@ class Portfolio(object):
return self.equity * self.risk_per_trade return self.equity * self.risk_per_trade
def add_new_position( def add_new_position(
self, side, market, units, exposure, self, position_type, market, units,
add_price, remove_price exposure, bid, ask
): ):
ps = Position( ps = Position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
self.positions[market] = ps self.positions[market] = ps
def add_position_units( def add_position_units(
self, market, units, exposure, self, market, units,
add_price, remove_price exposure, bid, ask
): ):
if market not in self.positions: if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] ps = self.positions[market]
if ps.position_type == "long":
add_price = ask
else:
add_price = bid
new_total_units = ps.units + units new_total_units = ps.units + units
new_total_cost = ps.avg_price*ps.units + add_price*units new_total_cost = ps.avg_price*ps.units + add_price*units
ps.exposure += exposure ps.exposure += exposure
ps.avg_price = new_total_cost/new_total_units ps.avg_price = new_total_cost/new_total_units
ps.units = new_total_units ps.units = new_total_units
ps.update_position_price(remove_price, exposure) ps.update_position_price(bid, ask, exposure)
return True return True
def remove_position_units( def remove_position_units(
self, market, units, remove_price self, market, units, bid, ask
): ):
if market not in self.positions: if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] ps = self.positions[market]
if ps.position_type == "long":
remove_price = bid
else:
remove_price = ask
ps.units -= units ps.units -= units
exposure = Decimal(str(units)) exposure = Decimal(str(units))
ps.exposure -= exposure ps.exposure -= exposure
ps.update_position_price(remove_price, exposure) ps.update_position_price(bid, ask, exposure)
pnl = ps.calculate_pips() * exposure / remove_price pnl = ps.calculate_pips() * exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
return True return True
def close_position( def close_position(
self, market, remove_price self, market, bid, ask
): ):
if market not in self.positions: if market not in self.positions:
return False return False
else: else:
ps = self.positions[market] 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 pnl = ps.calculate_pips() * ps.exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN)) self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
del[self.positions[market]] del[self.positions[market]]
return True return True
def execute_signal(self, signal_event): def execute_signal(self, signal_event):
side = signal_event.side side = signal_event.side
market = signal_event.instrument market = signal_event.instrument
units = int(self.trade_units) 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)) 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 there is no position, create one
if market not in self.positions: if market not in self.positions:
if side == "buy":
position_type = "long"
else:
position_type = "short"
self.add_new_position( self.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
order = OrderEvent(market, units, "market", side)
self.events.put(order)
# 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[market]
# Check if the sides equal
if side == ps.side: if side == "buy" and ps.position_type == "long":
# Add to the position add_position_units(market, units, exposure, bid, ask)
add_position_units(
market, units, exposure, elif side == "sell" and ps.position_type == "long":
add_price, remove_price
)
else:
# Check if the units close out the position
if units == ps.units: if units == ps.units:
# Close the position self.close_position(market, bid, ask)
self.close_position(market, remove_price) # TODO: Allow units to be added/removed
order = OrderEvent(market, units, "market", side)
self.events.put(order)
elif units < ps.units: elif units < ps.units:
# Remove from the position return
self.remove_position_units( elif units > ps.units:
market, units, remove_price return
)
else: # units > ps.units elif side == "buy" and ps.position_type == "short":
# Close the position and add a new one with if units == ps.units:
# additional units of opposite side self.close_position(market, bid, ask)
new_units = units - ps.units # TODO: Allow units to be added/removed
self.close_position(market, remove_price) elif units < ps.units:
return
elif units > ps.units:
return
if side == "buy": elif side == "sell" and ps.position_type == "short":
new_side = "sell" add_position_units(market, units, exposure, bid, ask)
else:
new_side = "buy" order = OrderEvent(market, units, "market", side)
new_exposure = Decimal(str(units)) self.events.put(order)
self.add_new_position(
new_side, market, new_units,
new_exposure, add_price, remove_price
)
print "Balance: %0.2f" % self.balance print "Balance: %0.2f" % self.balance
+166 -91
View File
@@ -18,200 +18,199 @@ class TestPortfolio(unittest.TestCase):
) )
def test_add_position_long(self): def test_add_position_long(self):
side = "LONG" position_type = "long"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51819") bid = Decimal("1.51770")
remove_price = Decimal("1.51770") ask = Decimal("1.51819")
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] 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.market, market)
self.assertEquals(ps.units, units) self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure) self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, add_price) self.assertEquals(ps.avg_price, ask)
self.assertEquals(ps.cur_price, remove_price) self.assertEquals(ps.cur_price, bid)
def test_add_position_short(self): def test_add_position_short(self):
side = "SHORT" position_type = "short"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51770") bid = Decimal("1.51770")
remove_price = Decimal("1.51819") ask = Decimal("1.51819")
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] 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.market, market)
self.assertEquals(ps.units, units) self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure) self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, add_price) self.assertEquals(ps.avg_price, bid)
self.assertEquals(ps.cur_price, remove_price) self.assertEquals(ps.cur_price, ask)
def test_add_position_units_long(self): def test_add_position_units_long(self):
side = "LONG" position_type = "long"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51819") bid = Decimal("1.51770")
remove_price = Decimal("1.51770") ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, market, units, exposure,
add_price, remove_price 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" market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] ps = self.port.positions[market]
# Test for addition of units # Test for addition of units
add_price = Decimal("1.51928") bid = Decimal("1.51878")
remove_price = Decimal("1.51878") ask = Decimal("1.51928")
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, market, units, exposure,
add_price, remove_price bid, ask
) )
self.assertTrue(apu) self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.518735")) self.assertEqual(ps.avg_price, Decimal("1.518735"))
def test_add_position_units_short(self): def test_add_position_units_short(self):
side = "SHORT" position_type = "short"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51770") bid = Decimal("1.51770")
remove_price = Decimal("1.51819") ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, market, units, exposure,
add_price, remove_price 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" market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] ps = self.port.positions[market]
# Test for addition of units # Test for addition of units
add_price = Decimal("1.51878") bid = Decimal("1.51878")
remove_price = Decimal("1.51928") ask = Decimal("1.51928")
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, market, units, exposure,
add_price, remove_price bid, ask
) )
self.assertTrue(apu) self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.51824")) self.assertEqual(ps.avg_price, Decimal("1.51824"))
def test_remove_position_units_long(self): def test_remove_position_units_long(self):
side = "LONG" position_type = "long"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51819") bid = Decimal("1.51770")
remove_price = Decimal("1.51770") ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
apu = self.port.remove_position_units( apu = self.port.remove_position_units(
market, units, remove_price market, units, bid, ask
) )
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" market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] ps = self.port.positions[market]
add_price = Decimal("1.51928") bid = Decimal("1.51878")
remove_price = Decimal("1.51878") ask = Decimal("1.51928")
add_units = 8000 add_units = 8000
add_exposure = Decimal(str(add_units)) add_exposure = Decimal(str(add_units))
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, add_units, add_exposure, market, add_units, add_exposure,
add_price, remove_price bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062")) self.assertEqual(ps.avg_price, Decimal("1.519062"))
# Test removal of (some) of the units # Test removal of (some) of the units
add_price = Decimal("1.52134") bid = Decimal("1.52017")
remove_price = Decimal("1.52017") ask = Decimal("1.52134")
remove_units = 3000 remove_units = 3000
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, remove_price market, remove_units, bid, ask
) )
self.assertTrue(rpu) self.assertTrue(rpu)
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054")) self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100002.19")) self.assertEqual(self.port.balance, Decimal("100002.19"))
def test_remove_position_units_short(self): def test_remove_position_units_short(self):
side = "SHORT" position_type = "short"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51770") bid = Decimal("1.51770")
remove_price = Decimal("1.51819") ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
apu = self.port.remove_position_units( apu = self.port.remove_position_units(
market, units, remove_price market, units, bid, ask
) )
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" market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] ps = self.port.positions[market]
add_price = Decimal("1.51878") bid = Decimal("1.51878")
remove_price = Decimal("1.51928") ask = Decimal("1.51928")
add_units = 8000 add_units = 8000
add_exposure = Decimal(str(add_units)) add_exposure = Decimal(str(add_units))
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, add_units, add_exposure, market, add_units, add_exposure,
add_price, remove_price bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564")) self.assertEqual(ps.avg_price, Decimal("1.518564"))
# Test removal of (some) of the units # Test removal of (some) of the units
add_price = Decimal("1.52017") bid = Decimal("1.52017")
remove_price = Decimal("1.52134") ask = Decimal("1.52134")
remove_units = 3000 remove_units = 3000
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, remove_price market, remove_units, bid, ask
) )
self.assertTrue(rpu) self.assertTrue(rpu)
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, 7000)
@@ -220,16 +219,16 @@ class TestPortfolio(unittest.TestCase):
self.assertEqual(self.port.balance, Decimal("99994.52")) self.assertEqual(self.port.balance, Decimal("99994.52"))
def test_close_position_long(self): def test_close_position_long(self):
side = "LONG" position_type = "long"
units = 2000 units = Decimal("2000")
exposure = Decimal(str(units)) exposure = Decimal("2000.00")
add_price = Decimal("1.51819") bid = Decimal("1.51770")
remove_price = Decimal("1.51770") ask = Decimal("1.51819")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
cp = self.port.close_position( cp = self.port.close_position(
market, remove_price market, bid, ask
) )
self.assertFalse(cp) self.assertFalse(cp)
@@ -237,12 +236,12 @@ class TestPortfolio(unittest.TestCase):
# Will lose money on the spread # Will lose money on the spread
market = "GBP/USD" market = "GBP/USD"
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] ps = self.port.positions[market]
cp = self.port.close_position( cp = self.port.close_position(
market, remove_price market, bid, ask
) )
self.assertTrue(cp) self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist self.assertRaises(ps) # Key doesn't exist
@@ -252,38 +251,114 @@ class TestPortfolio(unittest.TestCase):
# close the position. Balance should be as expected # close the position. Balance should be as expected
# for a multi-leg transaction. # for a multi-leg transaction.
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, position_type, market, units,
add_price, remove_price exposure, bid, ask
) )
ps = self.port.positions[market] 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_units = 8000
add_exposure = Decimal(str(add_units)) add_exposure = Decimal(str(add_units))
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, add_units, add_exposure, market, add_units,
add_price, remove_price add_exposure, bid, ask
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00")) self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062")) 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 remove_units = 3000
rpu = self.port.remove_position_units( rpu = self.port.remove_position_units(
market, remove_units, remove_price market, remove_units, bid, ask
) )
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00")) self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054")) self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100001.54")) self.assertEqual(self.port.balance, Decimal("100001.54"))
# Close the position
cp = self.port.close_position( cp = self.port.close_position(
market, remove_price 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("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__": if __name__ == "__main__":
unittest.main() unittest.main()
+20 -9
View File
@@ -3,22 +3,31 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN
class Position(object): class Position(object):
def __init__( def __init__(
self, side, market, units, self, position_type, market,
exposure, avg_price, cur_price units, exposure, bid, ask
): ):
self.side = side self.position_type = position_type # Long or short
self.market = market self.market = market
self.units = units self.units = units
self.exposure = Decimal(str(exposure)) 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_base = self.calculate_profit_base(self.exposure)
self.profit_perc = self.calculate_profit_perc(self.exposure) self.profit_perc = self.calculate_profit_perc(self.exposure)
def calculate_pips(self): def calculate_pips(self):
getcontext.prec = 6 getcontext.prec = 6
mult = Decimal("1") mult = Decimal("1")
if self.side == "SHORT": if self.position_type == "long":
mult = Decimal("1")
elif self.position_type == "short":
mult = Decimal("-1") mult = Decimal("-1")
return (mult * (self.cur_price - self.avg_price)).quantize( return (mult * (self.cur_price - self.avg_price)).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN Decimal("0.00001"), ROUND_HALF_DOWN
@@ -35,8 +44,10 @@ class Position(object):
Decimal("0.00001"), ROUND_HALF_DOWN Decimal("0.00001"), ROUND_HALF_DOWN
) )
def update_position_price(self, cur_price, exposure): def update_position_price(self, bid, ask, exposure):
self.cur_price = cur_price 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_base = self.calculate_profit_base(exposure)
self.profit_perc = self.calculate_profit_perc(exposure) self.profit_perc = self.calculate_profit_perc(exposure)
+60 -24
View File
@@ -7,55 +7,91 @@ from position import Position
class TestLongGBPUSDPosition(unittest.TestCase): class TestLongGBPUSDPosition(unittest.TestCase):
def setUp(self): def setUp(self):
getcontext.prec = 2 getcontext.prec = 2
side = "LONG" position_type = "long"
market = "GBP/USD" market = "GBP/USD"
units = Decimal(str(2000)) units = Decimal("2000")
exposure = Decimal("2000.00") exposure = Decimal("2000.00")
avg_price = Decimal("1.51819") bid = Decimal("1.50328")
cur_price = Decimal("1.51770") ask = Decimal("1.50349")
self.position = Position( self.position = Position(
side, market, units, exposure, position_type, market,
avg_price, cur_price units, exposure, bid, ask
) )
def test_calculate_pips(self): def test_calculate_init_pips(self):
pos_pips = self.position.calculate_pips() 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) 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) 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): class TestShortGBPUSDPosition(unittest.TestCase):
def setUp(self): def setUp(self):
getcontext.prec = 2 getcontext.prec = 2
side = "SHORT" position_type = "short"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = Decimal("2000")
exposure = Decimal("2000.00") exposure = Decimal("2000.00")
avg_price = Decimal("1.51819") bid = Decimal("1.50328")
cur_price = Decimal("1.51770") ask = Decimal("1.50349")
self.position = Position( self.position = Position(
side, market, units, exposure, position_type, market,
avg_price, cur_price units, exposure, bid, ask
) )
def test_calculate_pips(self): def test_calculate_init_pips(self):
pos_pips = self.position.calculate_pips() 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) 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) 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__": if __name__ == "__main__":
+7 -1
View File
@@ -1,3 +1,4 @@
from decimal import Decimal
import os import os
@@ -14,8 +15,13 @@ ENVIRONMENTS = {
} }
} }
CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None)
DOMAIN = "practice" DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN] STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
API_DOMAIN = ENVIRONMENTS["api"][DOMAIN] API_DOMAIN = ENVIRONMENTS["api"][DOMAIN]
ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None) ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None)
ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None) ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None)
BASE_CURRENCY = "GBP"
EQUITY = Decimal("100000.00")
+2 -2
View File
@@ -10,7 +10,7 @@ class TestStrategy(object):
def calculate_signals(self, event): def calculate_signals(self, event):
if event.type == 'TICK': if event.type == 'TICK':
if self.ticks % 200 == 0: if self.ticks % 5 == 0:
if self.invested == False: if self.invested == False:
signal = SignalEvent(self.instrument, "market", "buy") signal = SignalEvent(self.instrument, "market", "buy")
self.events.put(signal) self.events.put(signal)
@@ -19,4 +19,4 @@ class TestStrategy(object):
signal = SignalEvent(self.instrument, "market", "sell") signal = SignalEvent(self.instrument, "market", "sell")
self.events.put(signal) self.events.put(signal)
self.invested = False self.invested = False
self.ticks += 1 self.ticks += 1
+13 -9
View File
@@ -4,14 +4,14 @@ import threading
import time import time
from decimal import Decimal, getcontext 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.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.strategy.strategy import TestStrategy
from qsforex.streaming.streaming import StreamingForexPrices 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 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
@@ -39,9 +39,9 @@ if __name__ == "__main__":
# Set the number of decimal places to 2 # Set the number of decimal places to 2
getcontext().prec = 2 getcontext().prec = 2
heartbeat = 0.5 # Half a second between polling heartbeat = 0.0 # Half a second between polling
events = Queue.Queue() events = Queue.Queue()
equity = Decimal("99949.82") equity = settings.EQUITY
# Trade "Cable" # Trade "Cable"
instrument = "GBP_USD" instrument = "GBP_USD"
@@ -49,8 +49,8 @@ if __name__ == "__main__":
# Create the OANDA market price streaming class # Create the OANDA market price streaming class
# making sure to provide authentication commands # making sure to provide authentication commands
prices = StreamingForexPrices( prices = StreamingForexPrices(
STREAM_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID, settings.STREAM_DOMAIN, settings.ACCESS_TOKEN,
instrument, events settings.ACCOUNT_ID, instrument, events
) )
# Create the strategy/signal generator, passing the # Create the strategy/signal generator, passing the
@@ -64,13 +64,17 @@ if __name__ == "__main__":
# Create the execution handler making sure to # Create the execution handler making sure to
# provide authentication commands # 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 # Create two separate threads: One for the trading loop
# and another for the market price streaming class # and another for the market price streaming class
trade_thread = threading.Thread( trade_thread = threading.Thread(
target=trade, args=( target=trade, args=(
events, strategy, portfolio, execution events, strategy, portfolio, execution, heartbeat
) )
) )
price_thread = threading.Thread(target=prices.stream_to_queue, args=[]) price_thread = threading.Thread(target=prices.stream_to_queue, args=[])