Decimalised the trading engine to be more realistic

This commit is contained in:
Michael Halls-Moore
2015-03-06 09:57:15 +00:00
parent 30dbcc7bfa
commit 6c77cc1deb
6 changed files with 106 additions and 78 deletions
+9 -8
View File
@@ -1,4 +1,5 @@
from copy import deepcopy from copy import deepcopy
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
from qsforex.event.event import OrderEvent from qsforex.event.event import OrderEvent
from qsforex.portfolio.position import Position from qsforex.portfolio.position import Position
@@ -7,7 +8,7 @@ from qsforex.portfolio.position import Position
class Portfolio(object): class Portfolio(object):
def __init__( def __init__(
self, ticker, events, base="GBP", leverage=20, self, ticker, events, base="GBP", leverage=20,
equity=100000.0, risk_per_trade=0.02 equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02")
): ):
self.ticker = ticker self.ticker = ticker
self.events = events self.events = events
@@ -56,11 +57,11 @@ class Portfolio(object):
else: else:
ps = self.positions[market] ps = self.positions[market]
ps.units -= units ps.units -= units
exposure = float(units) exposure = Decimal(units)
ps.exposure -= exposure ps.exposure -= exposure
ps.update_position_price(remove_price) ps.update_position_price(remove_price)
pnl = ps.calculate_pips() * exposure / remove_price pnl = ps.calculate_pips() * exposure / remove_price
self.balance += pnl self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
return True return True
def close_position( def close_position(
@@ -72,7 +73,7 @@ class Portfolio(object):
ps = self.positions[market] ps = self.positions[market]
ps.update_position_price(remove_price) ps.update_position_price(remove_price)
pnl = ps.calculate_pips() * ps.exposure / remove_price pnl = ps.calculate_pips() * ps.exposure / remove_price
self.balance += pnl self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
del[self.positions[market]] del[self.positions[market]]
return True return True
@@ -83,12 +84,12 @@ class Portfolio(object):
# Check side for correct bid/ask prices # Check side for correct bid/ask prices
#if side == "buy": #if side == "buy":
add_price = self.ticker.cur_ask add_price = Decimal(str(self.ticker.cur_ask))
remove_price = self.ticker.cur_bid remove_price = Decimal(str(self.ticker.cur_bid))
#else: #else:
#add_price = self.ticker.cur_bid #add_price = self.ticker.cur_bid
#remove_price = self.ticker.cur_ask #remove_price = self.ticker.cur_ask
exposure = float(units) exposure = Decimal(str(units))
# 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:
@@ -130,7 +131,7 @@ class Portfolio(object):
new_side = "sell" new_side = "sell"
else: else:
new_side = "sell" new_side = "sell"
new_exposure = float(units) new_exposure = Decimal(str(units))
self.add_new_position( self.add_new_position(
new_side, market, new_units, new_side, market, new_units,
new_exposure, add_price, remove_price new_exposure, add_price, remove_price
+42 -41
View File
@@ -1,6 +1,6 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import unittest import unittest
#from position import Position
from portfolio import Portfolio from portfolio import Portfolio
@@ -8,11 +8,12 @@ class TestPortfolio(unittest.TestCase):
def setUp(self): def setUp(self):
base = "GBP" base = "GBP"
leverage = 20 leverage = 20
equity = 100000.0 equity = Decimal("100000.00")
risk_per_trade = 0.02 risk_per_trade = Decimal("0.02")
ticker = {} ticker = {}
events = {}
self.port = Portfolio( self.port = Portfolio(
ticker, base=base, leverage=leverage, ticker, events, base=base, leverage=leverage,
equity=equity, risk_per_trade=risk_per_trade equity=equity, risk_per_trade=risk_per_trade
) )
@@ -20,9 +21,9 @@ class TestPortfolio(unittest.TestCase):
side = "LONG" side = "LONG"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = 2000
exposure = float(units) exposure = Decimal(str(units))
add_price = 1.51819 add_price = Decimal("1.51819")
remove_price = 1.51770 remove_price = Decimal("1.51770")
self.port.add_new_position( self.port.add_new_position(
side, market, units, exposure, side, market, units, exposure,
@@ -41,9 +42,9 @@ class TestPortfolio(unittest.TestCase):
side = "LONG" side = "LONG"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = 2000
exposure = float(units) exposure = Decimal(str(units))
add_price = 1.51819 add_price = Decimal("1.51819")
remove_price = 1.51770 remove_price = Decimal("1.51770")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
@@ -62,21 +63,21 @@ class TestPortfolio(unittest.TestCase):
ps = self.port.positions[market] ps = self.port.positions[market]
# Test for addition of units # Test for addition of units
add_price = 1.51928 add_price = Decimal("1.51928")
remove_price = 1.51878 remove_price = Decimal("1.51878")
apu = self.port.add_position_units( apu = self.port.add_position_units(
market, units, exposure, market, units, exposure,
add_price, remove_price add_price, remove_price
) )
self.assertTrue(apu) self.assertTrue(apu)
self.assertAlmostEqual(ps.avg_price, 1.518735) self.assertEqual(ps.avg_price, Decimal("1.518735"))
def test_remove_position_units(self): def test_remove_position_units(self):
side = "LONG" side = "LONG"
units = 2000 units = 2000
exposure = float(units) exposure = Decimal(str(units))
add_price = 1.51819 add_price = Decimal("1.51819")
remove_price = 1.51770 remove_price = Decimal("1.51770")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
@@ -92,37 +93,37 @@ class TestPortfolio(unittest.TestCase):
add_price, remove_price add_price, remove_price
) )
ps = self.port.positions[market] ps = self.port.positions[market]
add_price = 1.51928 add_price = Decimal("1.51928")
remove_price = 1.51878 remove_price = Decimal("1.51878")
add_units = 8000 add_units = 8000
add_exposure = float(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 add_price, remove_price
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, 10000.0) self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertAlmostEqual(ps.avg_price, 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 = 1.52134 add_price = Decimal("1.52134")
remove_price = 1.52017 remove_price = Decimal("1.52017")
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, remove_price
) )
self.assertTrue(rpu) self.assertTrue(rpu)
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, 7000.0) self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertAlmostEqual(ps.profit_base, 5.102060953709626) self.assertEqual(ps.profit_base, Decimal("5.11127"))
self.assertAlmostEqual(self.port.balance, 100002.18659755158) self.assertEqual(self.port.balance, Decimal("100002.19"))
def test_close_position(self): def test_close_position(self):
side = "LONG" side = "LONG"
units = 2000 units = 2000
exposure = float(units) exposure = Decimal(str(units))
add_price = 1.51819 add_price = Decimal("1.51819")
remove_price = 1.51770 remove_price = Decimal("1.51770")
# Test for no position # Test for no position
market = "EUR/USD" market = "EUR/USD"
@@ -144,7 +145,7 @@ class TestPortfolio(unittest.TestCase):
) )
self.assertTrue(cp) self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist self.assertRaises(ps) # Key doesn't exist
self.assertAlmostEqual(self.port.balance, 99999.35428609079) self.assertEqual(self.port.balance, Decimal("99999.35"))
# Add 2000, add another 8000, remove 3000 and then # Add 2000, add another 8000, remove 3000 and then
# close the position. Balance should be as expected # close the position. Balance should be as expected
@@ -154,33 +155,33 @@ class TestPortfolio(unittest.TestCase):
add_price, remove_price add_price, remove_price
) )
ps = self.port.positions[market] ps = self.port.positions[market]
add_price = 1.51928 add_price = Decimal("1.51928")
remove_price = 1.51878 remove_price = Decimal("1.51878")
add_units = 8000 add_units = 8000
add_exposure = float(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 add_price, remove_price
) )
self.assertEqual(ps.units, 10000) self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, 10000.0) self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertAlmostEqual(ps.avg_price, 1.519062) self.assertEqual(ps.avg_price, Decimal("1.519062"))
add_price = 1.52134 add_price = Decimal("1.52134")
remove_price = 1.52017 remove_price = Decimal("1.52017")
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, remove_price
) )
self.assertEqual(ps.units, 7000) self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, 7000.0) self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertAlmostEqual(ps.profit_base, 5.102060953709626) self.assertEqual(ps.profit_base, Decimal("5.11127"))
self.assertAlmostEqual(self.port.balance, 100001.54088364237) self.assertEqual(self.port.balance, Decimal("100001.54"))
cp = self.port.close_position( cp = self.port.close_position(
market, remove_price market, remove_price
) )
self.assertTrue(cp) self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist self.assertRaises(ps) # Key doesn't exist
self.assertAlmostEqual(self.port.balance, 100006.64294459608) self.assertEqual(self.port.balance, Decimal("100006.65"))
if __name__ == "__main__": if __name__ == "__main__":
+18 -8
View File
@@ -1,3 +1,6 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
class Position(object): class Position(object):
def __init__( def __init__(
self, side, market, units, self, side, market, units,
@@ -6,24 +9,31 @@ class Position(object):
self.side = side self.side = side
self.market = market self.market = market
self.units = units self.units = units
self.exposure = exposure self.exposure = Decimal(str(exposure))
self.avg_price = avg_price self.avg_price = Decimal(str(avg_price))
self.cur_price = cur_price self.cur_price = Decimal(str(cur_price))
self.profit_base = self.calculate_profit_base() self.profit_base = self.calculate_profit_base()
self.profit_perc = self.calculate_profit_perc() self.profit_perc = self.calculate_profit_perc()
def calculate_pips(self): def calculate_pips(self):
mult = 1.0 getcontext.prec = 6
mult = Decimal("1")
if self.side == "SHORT": if self.side == "SHORT":
mult = -1.0 mult = Decimal("-1")
return mult * (self.cur_price - self.avg_price) return (mult * (self.cur_price - self.avg_price)).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def calculate_profit_base(self): def calculate_profit_base(self):
pips = self.calculate_pips() pips = self.calculate_pips()
return pips * self.exposure / self.cur_price return (pips * self.exposure / self.cur_price).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def calculate_profit_perc(self): def calculate_profit_perc(self):
return self.profit_base / self.exposure * 100.0 return (self.profit_base / self.exposure * Decimal("100.00")).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def update_position_price(self, cur_price): def update_position_price(self, cur_price):
self.cur_price = cur_price self.cur_price = cur_price
+17 -12
View File
@@ -1,3 +1,4 @@
from decimal import Decimal, getcontext
import unittest import unittest
from position import Position from position import Position
@@ -5,12 +6,13 @@ from position import Position
class TestLongGBPUSDPosition(unittest.TestCase): class TestLongGBPUSDPosition(unittest.TestCase):
def setUp(self): def setUp(self):
getcontext.prec = 2
side = "LONG" side = "LONG"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = 2000
exposure = 2000.0 exposure = Decimal("2000.00")
avg_price = 1.51819 avg_price = Decimal("1.51819")
cur_price = 1.51770 cur_price = Decimal("1.51770")
self.position = Position( self.position = Position(
side, market, units, exposure, side, market, units, exposure,
avg_price, cur_price avg_price, cur_price
@@ -18,25 +20,28 @@ class TestLongGBPUSDPosition(unittest.TestCase):
def test_calculate_pips(self): def test_calculate_pips(self):
pos_pips = self.position.calculate_pips() pos_pips = self.position.calculate_pips()
self.assertAlmostEqual(pos_pips, -0.00049) self.assertEqual(pos_pips, Decimal("-0.00049"))
def test_calculate_profit_base(self): def test_calculate_profit_base(self):
profit_base = self.position.calculate_profit_base() profit_base = self.position.calculate_profit_base()
self.assertAlmostEqual(profit_base, -0.6457139) #self.assertEqual(profit_base, Decimal("-0.6457139"))
self.assertEqual(profit_base, Decimal("-0.64571"))
def test_calculate_profit_perc(self): def test_calculate_profit_perc(self):
profit_perc = self.position.calculate_profit_perc() profit_perc = self.position.calculate_profit_perc()
self.assertAlmostEqual(profit_perc, -0.032285695) #self.assertEqual(profit_perc, Decimal("-0.032285695"))
self.assertEqual(profit_perc, Decimal("-0.03229"))
class TestShortGBPUSDPosition(unittest.TestCase): class TestShortGBPUSDPosition(unittest.TestCase):
def setUp(self): def setUp(self):
getcontext.prec = 2
side = "SHORT" side = "SHORT"
market = "GBP/USD" market = "GBP/USD"
units = 2000 units = 2000
exposure = 2000.0 exposure = Decimal("2000.00")
avg_price = 1.51819 avg_price = Decimal("1.51819")
cur_price = 1.51770 cur_price = Decimal("1.51770")
self.position = Position( self.position = Position(
side, market, units, exposure, side, market, units, exposure,
avg_price, cur_price avg_price, cur_price
@@ -44,15 +49,15 @@ class TestShortGBPUSDPosition(unittest.TestCase):
def test_calculate_pips(self): def test_calculate_pips(self):
pos_pips = self.position.calculate_pips() pos_pips = self.position.calculate_pips()
self.assertAlmostEqual(pos_pips, 0.00049) self.assertEqual(pos_pips, Decimal("0.00049"))
def test_calculate_profit_base(self): def test_calculate_profit_base(self):
profit_base = self.position.calculate_profit_base() profit_base = self.position.calculate_profit_base()
self.assertAlmostEqual(profit_base, 0.6457139) self.assertEqual(profit_base, Decimal("0.64571"))
def test_calculate_profit_perc(self): def test_calculate_profit_perc(self):
profit_perc = self.position.calculate_profit_perc() profit_perc = self.position.calculate_profit_perc()
self.assertAlmostEqual(profit_perc, 0.032285695) self.assertEqual(profit_perc, Decimal("0.03229"))
if __name__ == "__main__": if __name__ == "__main__":
+12 -6
View File
@@ -1,17 +1,23 @@
from qsforex.event.event import SignalEvent from qsforex.event.event import SignalEvent
class TestRandomStrategy(object): class TestStrategy(object):
def __init__(self, instrument, events): def __init__(self, instrument, events):
self.instrument = instrument self.instrument = instrument
self.events = events self.events = events
self.ticks = 0 self.ticks = 0
self.invested = False
def calculate_signals(self, event): def calculate_signals(self, event):
if event.type == 'TICK': if event.type == 'TICK':
self.ticks += 1 self.ticks += 1
if self.ticks % 2 == 0: if self.ticks % 5 == 0:
signal = SignalEvent(self.instrument, "market", "buy") if self.invested == False:
else: signal = SignalEvent(self.instrument, "market", "buy")
signal = SignalEvent(self.instrument, "market", "sell") self.events.put(signal)
self.events.put(signal) self.invested = True
else:
signal = SignalEvent(self.instrument, "market", "sell")
self.events.put(signal)
self.invested = False
+8 -3
View File
@@ -2,11 +2,12 @@ import copy
import Queue import Queue
import threading import threading
import time import time
from decimal import Decimal, getcontext
from qsforex.execution.execution import Execution from qsforex.execution.execution import Execution
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.settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID
from qsforex.strategy.strategy import TestRandomStrategy from qsforex.strategy.strategy import TestStrategy
from qsforex.streaming.streaming import StreamingForexPrices from qsforex.streaming.streaming import StreamingForexPrices
@@ -35,8 +36,12 @@ def trade(events, strategy, portfolio, execution):
if __name__ == "__main__": if __name__ == "__main__":
# Set the number of decimal places to 2
getcontext().prec = 2
heartbeat = 0.5 # Half a second between polling heartbeat = 0.5 # Half a second between polling
events = Queue.Queue() events = Queue.Queue()
equity = Decimal("99999.65")
# Trade "Cable" # Trade "Cable"
instrument = "GBP_USD" instrument = "GBP_USD"
@@ -50,12 +55,12 @@ if __name__ == "__main__":
# 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 = TestRandomStrategy(instrument, events) strategy = TestStrategy(instrument, events)
# Create the portfolio object that will be used to # Create the portfolio object that will be used to
# compare the OANDA positions with the local, to # compare the OANDA positions with the local, to
# ensure backtesting integrity. # ensure backtesting integrity.
portfolio = Portfolio(prices, events, equity=98499.05) portfolio = Portfolio(prices, events, equity=equity)
# Create the execution handler making sure to # Create the execution handler making sure to
# provide authentication commands # provide authentication commands