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
+173 -212
View File
@@ -1,364 +1,325 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
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):
def setUp(self):
base = "GBP"
home_currency = "GBP"
leverage = 20
equity = Decimal("100000.00")
risk_per_trade = Decimal("0.02")
ticker = {}
ticker = TickerMock()
events = {}
self.port = Portfolio(
ticker, events, base=base, leverage=leverage,
equity=equity, risk_per_trade=risk_per_trade
ticker, events, home_currency=home_currency,
leverage=leverage, equity=equity,
risk_per_trade=risk_per_trade
)
def test_add_position_long(self):
position_type = "long"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
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.market, market)
self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, ask)
self.assertEquals(ps.cur_price, bid)
self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["ask"])
self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["bid"])
def test_add_position_short(self):
position_type = "short"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
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.market, market)
self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, bid)
self.assertEquals(ps.cur_price, ask)
self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["bid"])
self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["ask"])
def test_add_position_units_long(self):
position_type = "long"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
# Test for addition of units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, units, exposure,
bid, ask
currency_pair, units
)
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):
position_type = "short"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
# Test for addition of units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, units, exposure,
bid, ask
currency_pair, units
)
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):
position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
market, units, bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units(
market, add_units, add_exposure,
bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
self.assertEqual(ps.avg_price, Decimal("1.516122"))
# Test removal of (some) of the units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertTrue(rpu)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100002.19"))
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("100007.99"))
def test_remove_position_units_short(self):
position_type = "short"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
market, units, bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units(
market, add_units, add_exposure,
bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
self.assertEqual(ps.avg_price, Decimal("1.51568"))
# Test removal of (some) of the units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertTrue(rpu)
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("99994.52"))
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("99988.84"))
def test_close_position_long(self):
position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
cp = self.port.close_position(
market, bid, ask
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
alt_currency_pair, units
)
self.assertFalse(cp)
self.assertFalse(apu)
# Add a position and then close it
# Will lose money on the spread
market = "GBP/USD"
# Add a position and then add units to it
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
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))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units(
market, add_units,
add_exposure, bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
self.assertEqual(ps.avg_price, Decimal("1.516122"))
# Remove 3000 units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
# Test removal of (some) of the units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100001.54"))
self.assertTrue(rpu)
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("100007.99"))
# Close the position
cp = self.port.close_position(
market, bid, ask
)
cp = self.port.close_position(currency_pair)
self.assertTrue(cp)
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):
position_type = "short"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
cp = self.port.close_position(
market, bid, ask
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
alt_currency_pair, units
)
self.assertFalse(cp)
self.assertFalse(apu)
# Add a position and then close it
# Will lose money on the spread
market = "GBP/USD"
# Add a position and then add units to it
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
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))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
add_units = Decimal("8000")
apu = self.port.add_position_units(
market, add_units,
add_exposure, bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
self.assertEqual(ps.avg_price, Decimal("1.51568"))
# Remove 3000 units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
# Test removal of (some) of the units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
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"))
self.assertTrue(rpu)
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("99988.84"))
# Close the position
cp = self.port.close_position(
market, bid, ask
)
cp = self.port.close_position(currency_pair)
self.assertTrue(cp)
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__":
unittest.main()