Decimalised the trading engine to be more realistic
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from copy import deepcopy
|
||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||
|
||||
from qsforex.event.event import OrderEvent
|
||||
from qsforex.portfolio.position import Position
|
||||
@@ -7,7 +8,7 @@ from qsforex.portfolio.position import Position
|
||||
class Portfolio(object):
|
||||
def __init__(
|
||||
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.events = events
|
||||
@@ -56,11 +57,11 @@ class Portfolio(object):
|
||||
else:
|
||||
ps = self.positions[market]
|
||||
ps.units -= units
|
||||
exposure = float(units)
|
||||
exposure = Decimal(units)
|
||||
ps.exposure -= exposure
|
||||
ps.update_position_price(remove_price)
|
||||
pnl = ps.calculate_pips() * exposure / remove_price
|
||||
self.balance += pnl
|
||||
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
|
||||
return True
|
||||
|
||||
def close_position(
|
||||
@@ -72,7 +73,7 @@ class Portfolio(object):
|
||||
ps = self.positions[market]
|
||||
ps.update_position_price(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]]
|
||||
return True
|
||||
|
||||
@@ -83,12 +84,12 @@ class Portfolio(object):
|
||||
|
||||
# Check side for correct bid/ask prices
|
||||
#if side == "buy":
|
||||
add_price = self.ticker.cur_ask
|
||||
remove_price = self.ticker.cur_bid
|
||||
add_price = Decimal(str(self.ticker.cur_ask))
|
||||
remove_price = Decimal(str(self.ticker.cur_bid))
|
||||
#else:
|
||||
#add_price = self.ticker.cur_bid
|
||||
#remove_price = self.ticker.cur_ask
|
||||
exposure = float(units)
|
||||
exposure = Decimal(str(units))
|
||||
|
||||
# If there is no position, create one
|
||||
if market not in self.positions:
|
||||
@@ -130,7 +131,7 @@ class Portfolio(object):
|
||||
new_side = "sell"
|
||||
else:
|
||||
new_side = "sell"
|
||||
new_exposure = float(units)
|
||||
new_exposure = Decimal(str(units))
|
||||
self.add_new_position(
|
||||
new_side, market, new_units,
|
||||
new_exposure, add_price, remove_price
|
||||
|
||||
+42
-41
@@ -1,6 +1,6 @@
|
||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||
import unittest
|
||||
|
||||
#from position import Position
|
||||
from portfolio import Portfolio
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ class TestPortfolio(unittest.TestCase):
|
||||
def setUp(self):
|
||||
base = "GBP"
|
||||
leverage = 20
|
||||
equity = 100000.0
|
||||
risk_per_trade = 0.02
|
||||
equity = Decimal("100000.00")
|
||||
risk_per_trade = Decimal("0.02")
|
||||
ticker = {}
|
||||
events = {}
|
||||
self.port = Portfolio(
|
||||
ticker, base=base, leverage=leverage,
|
||||
ticker, events, base=base, leverage=leverage,
|
||||
equity=equity, risk_per_trade=risk_per_trade
|
||||
)
|
||||
|
||||
@@ -20,9 +21,9 @@ class TestPortfolio(unittest.TestCase):
|
||||
side = "LONG"
|
||||
market = "GBP/USD"
|
||||
units = 2000
|
||||
exposure = float(units)
|
||||
add_price = 1.51819
|
||||
remove_price = 1.51770
|
||||
exposure = Decimal(str(units))
|
||||
add_price = Decimal("1.51819")
|
||||
remove_price = Decimal("1.51770")
|
||||
|
||||
self.port.add_new_position(
|
||||
side, market, units, exposure,
|
||||
@@ -41,9 +42,9 @@ class TestPortfolio(unittest.TestCase):
|
||||
side = "LONG"
|
||||
market = "GBP/USD"
|
||||
units = 2000
|
||||
exposure = float(units)
|
||||
add_price = 1.51819
|
||||
remove_price = 1.51770
|
||||
exposure = Decimal(str(units))
|
||||
add_price = Decimal("1.51819")
|
||||
remove_price = Decimal("1.51770")
|
||||
|
||||
# Test for no position
|
||||
market = "EUR/USD"
|
||||
@@ -62,21 +63,21 @@ class TestPortfolio(unittest.TestCase):
|
||||
ps = self.port.positions[market]
|
||||
|
||||
# Test for addition of units
|
||||
add_price = 1.51928
|
||||
remove_price = 1.51878
|
||||
add_price = Decimal("1.51928")
|
||||
remove_price = Decimal("1.51878")
|
||||
apu = self.port.add_position_units(
|
||||
market, units, exposure,
|
||||
add_price, remove_price
|
||||
)
|
||||
self.assertTrue(apu)
|
||||
self.assertAlmostEqual(ps.avg_price, 1.518735)
|
||||
self.assertEqual(ps.avg_price, Decimal("1.518735"))
|
||||
|
||||
def test_remove_position_units(self):
|
||||
side = "LONG"
|
||||
units = 2000
|
||||
exposure = float(units)
|
||||
add_price = 1.51819
|
||||
remove_price = 1.51770
|
||||
exposure = Decimal(str(units))
|
||||
add_price = Decimal("1.51819")
|
||||
remove_price = Decimal("1.51770")
|
||||
|
||||
# Test for no position
|
||||
market = "EUR/USD"
|
||||
@@ -92,37 +93,37 @@ class TestPortfolio(unittest.TestCase):
|
||||
add_price, remove_price
|
||||
)
|
||||
ps = self.port.positions[market]
|
||||
add_price = 1.51928
|
||||
remove_price = 1.51878
|
||||
add_price = Decimal("1.51928")
|
||||
remove_price = Decimal("1.51878")
|
||||
add_units = 8000
|
||||
add_exposure = float(add_units)
|
||||
add_exposure = Decimal(str(add_units))
|
||||
apu = self.port.add_position_units(
|
||||
market, add_units, add_exposure,
|
||||
add_price, remove_price
|
||||
)
|
||||
self.assertEqual(ps.units, 10000)
|
||||
self.assertEqual(ps.exposure, 10000.0)
|
||||
self.assertAlmostEqual(ps.avg_price, 1.519062)
|
||||
self.assertEqual(ps.exposure, Decimal("10000.00"))
|
||||
self.assertEqual(ps.avg_price, Decimal("1.519062"))
|
||||
|
||||
# Test removal of (some) of the units
|
||||
add_price = 1.52134
|
||||
remove_price = 1.52017
|
||||
add_price = Decimal("1.52134")
|
||||
remove_price = Decimal("1.52017")
|
||||
remove_units = 3000
|
||||
rpu = self.port.remove_position_units(
|
||||
market, remove_units, remove_price
|
||||
)
|
||||
self.assertTrue(rpu)
|
||||
self.assertEqual(ps.units, 7000)
|
||||
self.assertEqual(ps.exposure, 7000.0)
|
||||
self.assertAlmostEqual(ps.profit_base, 5.102060953709626)
|
||||
self.assertAlmostEqual(self.port.balance, 100002.18659755158)
|
||||
self.assertEqual(ps.exposure, Decimal("7000.00"))
|
||||
self.assertEqual(ps.profit_base, Decimal("5.11127"))
|
||||
self.assertEqual(self.port.balance, Decimal("100002.19"))
|
||||
|
||||
def test_close_position(self):
|
||||
side = "LONG"
|
||||
units = 2000
|
||||
exposure = float(units)
|
||||
add_price = 1.51819
|
||||
remove_price = 1.51770
|
||||
exposure = Decimal(str(units))
|
||||
add_price = Decimal("1.51819")
|
||||
remove_price = Decimal("1.51770")
|
||||
|
||||
# Test for no position
|
||||
market = "EUR/USD"
|
||||
@@ -144,7 +145,7 @@ class TestPortfolio(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(cp)
|
||||
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
|
||||
# close the position. Balance should be as expected
|
||||
@@ -154,33 +155,33 @@ class TestPortfolio(unittest.TestCase):
|
||||
add_price, remove_price
|
||||
)
|
||||
ps = self.port.positions[market]
|
||||
add_price = 1.51928
|
||||
remove_price = 1.51878
|
||||
add_price = Decimal("1.51928")
|
||||
remove_price = Decimal("1.51878")
|
||||
add_units = 8000
|
||||
add_exposure = float(add_units)
|
||||
add_exposure = Decimal(str(add_units))
|
||||
apu = self.port.add_position_units(
|
||||
market, add_units, add_exposure,
|
||||
add_price, remove_price
|
||||
)
|
||||
self.assertEqual(ps.units, 10000)
|
||||
self.assertEqual(ps.exposure, 10000.0)
|
||||
self.assertAlmostEqual(ps.avg_price, 1.519062)
|
||||
add_price = 1.52134
|
||||
remove_price = 1.52017
|
||||
self.assertEqual(ps.exposure, Decimal("10000.00"))
|
||||
self.assertEqual(ps.avg_price, Decimal("1.519062"))
|
||||
add_price = Decimal("1.52134")
|
||||
remove_price = Decimal("1.52017")
|
||||
remove_units = 3000
|
||||
rpu = self.port.remove_position_units(
|
||||
market, remove_units, remove_price
|
||||
)
|
||||
self.assertEqual(ps.units, 7000)
|
||||
self.assertEqual(ps.exposure, 7000.0)
|
||||
self.assertAlmostEqual(ps.profit_base, 5.102060953709626)
|
||||
self.assertAlmostEqual(self.port.balance, 100001.54088364237)
|
||||
self.assertEqual(ps.exposure, Decimal("7000.00"))
|
||||
self.assertEqual(ps.profit_base, Decimal("5.11127"))
|
||||
self.assertEqual(self.port.balance, Decimal("100001.54"))
|
||||
cp = self.port.close_position(
|
||||
market, remove_price
|
||||
)
|
||||
self.assertTrue(cp)
|
||||
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__":
|
||||
|
||||
+18
-8
@@ -1,3 +1,6 @@
|
||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||
|
||||
|
||||
class Position(object):
|
||||
def __init__(
|
||||
self, side, market, units,
|
||||
@@ -6,24 +9,31 @@ class Position(object):
|
||||
self.side = side
|
||||
self.market = market
|
||||
self.units = units
|
||||
self.exposure = exposure
|
||||
self.avg_price = avg_price
|
||||
self.cur_price = cur_price
|
||||
self.exposure = Decimal(str(exposure))
|
||||
self.avg_price = Decimal(str(avg_price))
|
||||
self.cur_price = Decimal(str(cur_price))
|
||||
self.profit_base = self.calculate_profit_base()
|
||||
self.profit_perc = self.calculate_profit_perc()
|
||||
|
||||
def calculate_pips(self):
|
||||
mult = 1.0
|
||||
getcontext.prec = 6
|
||||
mult = Decimal("1")
|
||||
if self.side == "SHORT":
|
||||
mult = -1.0
|
||||
return mult * (self.cur_price - self.avg_price)
|
||||
mult = Decimal("-1")
|
||||
return (mult * (self.cur_price - self.avg_price)).quantize(
|
||||
Decimal("0.00001"), ROUND_HALF_DOWN
|
||||
)
|
||||
|
||||
def calculate_profit_base(self):
|
||||
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):
|
||||
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):
|
||||
self.cur_price = cur_price
|
||||
|
||||
+17
-12
@@ -1,3 +1,4 @@
|
||||
from decimal import Decimal, getcontext
|
||||
import unittest
|
||||
|
||||
from position import Position
|
||||
@@ -5,12 +6,13 @@ from position import Position
|
||||
|
||||
class TestLongGBPUSDPosition(unittest.TestCase):
|
||||
def setUp(self):
|
||||
getcontext.prec = 2
|
||||
side = "LONG"
|
||||
market = "GBP/USD"
|
||||
units = 2000
|
||||
exposure = 2000.0
|
||||
avg_price = 1.51819
|
||||
cur_price = 1.51770
|
||||
exposure = Decimal("2000.00")
|
||||
avg_price = Decimal("1.51819")
|
||||
cur_price = Decimal("1.51770")
|
||||
self.position = Position(
|
||||
side, market, units, exposure,
|
||||
avg_price, cur_price
|
||||
@@ -18,25 +20,28 @@ class TestLongGBPUSDPosition(unittest.TestCase):
|
||||
|
||||
def test_calculate_pips(self):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
def setUp(self):
|
||||
getcontext.prec = 2
|
||||
side = "SHORT"
|
||||
market = "GBP/USD"
|
||||
units = 2000
|
||||
exposure = 2000.0
|
||||
avg_price = 1.51819
|
||||
cur_price = 1.51770
|
||||
exposure = Decimal("2000.00")
|
||||
avg_price = Decimal("1.51819")
|
||||
cur_price = Decimal("1.51770")
|
||||
self.position = Position(
|
||||
side, market, units, exposure,
|
||||
avg_price, cur_price
|
||||
@@ -44,15 +49,15 @@ class TestShortGBPUSDPosition(unittest.TestCase):
|
||||
|
||||
def test_calculate_pips(self):
|
||||
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):
|
||||
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):
|
||||
profit_perc = self.position.calculate_profit_perc()
|
||||
self.assertAlmostEqual(profit_perc, 0.032285695)
|
||||
self.assertEqual(profit_perc, Decimal("0.03229"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+12
-6
@@ -1,17 +1,23 @@
|
||||
from qsforex.event.event import SignalEvent
|
||||
|
||||
|
||||
class TestRandomStrategy(object):
|
||||
class TestStrategy(object):
|
||||
def __init__(self, instrument, events):
|
||||
self.instrument = instrument
|
||||
self.events = events
|
||||
self.ticks = 0
|
||||
self.invested = False
|
||||
|
||||
def calculate_signals(self, event):
|
||||
if event.type == 'TICK':
|
||||
self.ticks += 1
|
||||
if self.ticks % 2 == 0:
|
||||
signal = SignalEvent(self.instrument, "market", "buy")
|
||||
else:
|
||||
signal = SignalEvent(self.instrument, "market", "sell")
|
||||
self.events.put(signal)
|
||||
if self.ticks % 5 == 0:
|
||||
if self.invested == False:
|
||||
signal = SignalEvent(self.instrument, "market", "buy")
|
||||
self.events.put(signal)
|
||||
self.invested = True
|
||||
else:
|
||||
signal = SignalEvent(self.instrument, "market", "sell")
|
||||
self.events.put(signal)
|
||||
self.invested = False
|
||||
|
||||
+8
-3
@@ -2,11 +2,12 @@ import copy
|
||||
import Queue
|
||||
import threading
|
||||
import time
|
||||
from decimal import Decimal, getcontext
|
||||
|
||||
from qsforex.execution.execution import Execution
|
||||
from qsforex.portfolio.portfolio import Portfolio
|
||||
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
|
||||
|
||||
|
||||
@@ -35,8 +36,12 @@ def trade(events, strategy, portfolio, execution):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set the number of decimal places to 2
|
||||
getcontext().prec = 2
|
||||
|
||||
heartbeat = 0.5 # Half a second between polling
|
||||
events = Queue.Queue()
|
||||
equity = Decimal("99999.65")
|
||||
|
||||
# Trade "Cable"
|
||||
instrument = "GBP_USD"
|
||||
@@ -50,12 +55,12 @@ if __name__ == "__main__":
|
||||
|
||||
# Create the strategy/signal generator, passing the
|
||||
# instrument and the events queue
|
||||
strategy = TestRandomStrategy(instrument, events)
|
||||
strategy = TestStrategy(instrument, events)
|
||||
|
||||
# Create the portfolio object that will be used to
|
||||
# compare the OANDA positions with the local, to
|
||||
# ensure backtesting integrity.
|
||||
portfolio = Portfolio(prices, events, equity=98499.05)
|
||||
portfolio = Portfolio(prices, events, equity=equity)
|
||||
|
||||
# Create the execution handler making sure to
|
||||
# provide authentication commands
|
||||
|
||||
Reference in New Issue
Block a user