Added some more unit tests for Portfolio and Position. Added a README and a requirements file now that qsforex is open-source under an MIT license.

This commit is contained in:
Michael Halls-Moore
2015-03-17 07:48:37 +00:00
parent 6c77cc1deb
commit d9a7444fc2
8 changed files with 189 additions and 39 deletions
+41
View File
@@ -0,0 +1,41 @@
# QuantStart Forex
QSForex is an open-source work-in-progress event-driven backtesting and live trading platform for use in the foreign exchange ("forex") markets.
It has been created as part of the Forex Trading Diary series on QuantStart.com, predominantly for education purposes, but also to provide the systematic trading community with a robust trading engine that allows straightforward forex strategy implementation and testing.
The software is provided under a permissive "MIT" license (see below).
# Current Features
* Currently in alpha mode - very early stage!
* Live trading with one particular forex broker, OANDA, via their Rest API
* Event-driven architecture with price streaming (via the OANDA API)
* Basic local portfolio replication of live trades (ultimately for backtesting purposes)
# Installation and Usage
At this stage, since the software is in alpha mode, the installation instructions are somewhat more involved. As the software evolves they will become more straightforward and documentation will become more extensive. However, the basic approach is as follows:
1. Setup an account with OANDA and obtain the API authentication credentials
2. Clone the repository into a suitable location on your machine
3. Create two environment variables: OANDA_API_ACCESS_TOKEN and OANDA_API_ACCOUNT_ID, which must contain, respectively, the OANDA API Access Token and the OANDA API Account ID, as provided by OANDA
4. Create a virtual environment ("virtualenv") for the QSForex code and utilise pip to install the requirements - `pip install -r requirements.txt`
5. Modify `strategy.py` to create an event-driven strategy class
6. Execute `trading.py` to carry out practice/live trading
If you have any questions about the installation then please feel free to email me at mike AT quantstart DOT com. You might also wish to have a look at the Forex Trading Diary series on QuantStart in order to gain some intuition about how the system is built.
# License Terms
Copyright (c) 2015 Michael Halls-Moore
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Forex Trading Disclaimer
Trading foreign exchange on margin carries a high level of risk, and may not be suitable for all investors. Past performance is not indicative of future results. The high degree of leverage can work against you as well as for you. Before deciding to invest in foreign exchange you should carefully consider your investment objectives, level of experience, and risk appetite. The possibility exists that you could sustain a loss of some or all of your initial investment and therefore you should not invest money that you cannot afford to lose. You should be aware of all the risks associated with foreign exchange trading, and seek advice from an independent financial advisor if you have any doubts.
+13 -13
View File
@@ -46,7 +46,7 @@ class Portfolio(object):
ps.exposure += exposure
ps.avg_price = new_total_cost/new_total_units
ps.units = new_total_units
ps.update_position_price(remove_price)
ps.update_position_price(remove_price, exposure)
return True
def remove_position_units(
@@ -57,9 +57,9 @@ class Portfolio(object):
else:
ps = self.positions[market]
ps.units -= units
exposure = Decimal(units)
exposure = Decimal(str(units))
ps.exposure -= exposure
ps.update_position_price(remove_price)
ps.update_position_price(remove_price, exposure)
pnl = ps.calculate_pips() * exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
return True
@@ -71,7 +71,7 @@ class Portfolio(object):
return False
else:
ps = self.positions[market]
ps.update_position_price(remove_price)
ps.update_position_price(remove_price, ps.exposure)
pnl = ps.calculate_pips() * ps.exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
del[self.positions[market]]
@@ -83,12 +83,12 @@ class Portfolio(object):
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 = self.ticker.cur_bid
#remove_price = self.ticker.cur_ask
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))
# If there is no position, create one
@@ -97,7 +97,7 @@ class Portfolio(object):
side, market, units, exposure,
add_price, remove_price
)
order = OrderEvent(market, units, "market", "buy")
order = OrderEvent(market, units, "market", side)
self.events.put(order)
# If a position exists add or remove units
else:
@@ -114,7 +114,7 @@ class Portfolio(object):
if units == ps.units:
# Close the position
self.close_position(market, remove_price)
order = OrderEvent(market, units, "market", "sell")
order = OrderEvent(market, units, "market", side)
self.events.put(order)
elif units < ps.units:
# Remove from the position
@@ -130,7 +130,7 @@ class Portfolio(object):
if side == "buy":
new_side = "sell"
else:
new_side = "sell"
new_side = "buy"
new_exposure = Decimal(str(units))
self.add_new_position(
new_side, market, new_units,
+107 -6
View File
@@ -17,7 +17,7 @@ class TestPortfolio(unittest.TestCase):
equity=equity, risk_per_trade=risk_per_trade
)
def test_add_position(self):
def test_add_position_long(self):
side = "LONG"
market = "GBP/USD"
units = 2000
@@ -38,7 +38,28 @@ class TestPortfolio(unittest.TestCase):
self.assertEquals(ps.avg_price, add_price)
self.assertEquals(ps.cur_price, remove_price)
def test_add_position_units(self):
def test_add_position_short(self):
side = "SHORT"
market = "GBP/USD"
units = 2000
exposure = Decimal(str(units))
add_price = Decimal("1.51770")
remove_price = Decimal("1.51819")
self.port.add_new_position(
side, market, units, exposure,
add_price, remove_price
)
ps = self.port.positions[market]
self.assertEquals(ps.side, side)
self.assertEquals(ps.market, market)
self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, add_price)
self.assertEquals(ps.cur_price, remove_price)
def test_add_position_units_long(self):
side = "LONG"
market = "GBP/USD"
units = 2000
@@ -72,7 +93,41 @@ class TestPortfolio(unittest.TestCase):
self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.518735"))
def test_remove_position_units(self):
def test_add_position_units_short(self):
side = "SHORT"
market = "GBP/USD"
units = 2000
exposure = Decimal(str(units))
add_price = Decimal("1.51770")
remove_price = Decimal("1.51819")
# Test for no position
market = "EUR/USD"
apu = self.port.add_position_units(
market, units, exposure,
add_price, remove_price
)
self.assertFalse(apu)
# Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position(
side, market, units, exposure,
add_price, remove_price
)
ps = self.port.positions[market]
# Test for addition of units
add_price = Decimal("1.51878")
remove_price = Decimal("1.51928")
apu = self.port.add_position_units(
market, units, exposure,
add_price, remove_price
)
self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.51824"))
def test_remove_position_units_long(self):
side = "LONG"
units = 2000
exposure = Decimal(str(units))
@@ -115,10 +170,56 @@ class TestPortfolio(unittest.TestCase):
self.assertTrue(rpu)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("5.11127"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100002.19"))
def test_close_position(self):
def test_remove_position_units_short(self):
side = "SHORT"
units = 2000
exposure = Decimal(str(units))
add_price = Decimal("1.51770")
remove_price = Decimal("1.51819")
# Test for no position
market = "EUR/USD"
apu = self.port.remove_position_units(
market, units, remove_price
)
self.assertFalse(apu)
# Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position(
side, market, units, exposure,
add_price, remove_price
)
ps = self.port.positions[market]
add_price = Decimal("1.51878")
remove_price = Decimal("1.51928")
add_units = 8000
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, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
# Test removal of (some) of the units
add_price = Decimal("1.52017")
remove_price = Decimal("1.52134")
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, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("-5.48201"))
self.assertEqual(self.port.balance, Decimal("99994.52"))
def test_close_position_long(self):
side = "LONG"
units = 2000
exposure = Decimal(str(units))
@@ -174,7 +275,7 @@ class TestPortfolio(unittest.TestCase):
)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("5.11127"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100001.54"))
cp = self.port.close_position(
market, remove_price
+9 -9
View File
@@ -12,8 +12,8 @@ class Position(object):
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()
self.profit_base = self.calculate_profit_base(self.exposure)
self.profit_perc = self.calculate_profit_perc(self.exposure)
def calculate_pips(self):
getcontext.prec = 6
@@ -24,19 +24,19 @@ class Position(object):
Decimal("0.00001"), ROUND_HALF_DOWN
)
def calculate_profit_base(self):
def calculate_profit_base(self, exposure):
pips = self.calculate_pips()
return (pips * self.exposure / self.cur_price).quantize(
return (pips * exposure / self.cur_price).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def calculate_profit_perc(self):
return (self.profit_base / self.exposure * Decimal("100.00")).quantize(
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, cur_price):
def update_position_price(self, cur_price, exposure):
self.cur_price = cur_price
self.profit_base = self.calculate_profit_base()
self.profit_perc = self.calculate_profit_perc()
self.profit_base = self.calculate_profit_base(exposure)
self.profit_perc = self.calculate_profit_perc(exposure)
+5 -7
View File
@@ -9,7 +9,7 @@ class TestLongGBPUSDPosition(unittest.TestCase):
getcontext.prec = 2
side = "LONG"
market = "GBP/USD"
units = 2000
units = Decimal(str(2000))
exposure = Decimal("2000.00")
avg_price = Decimal("1.51819")
cur_price = Decimal("1.51770")
@@ -23,13 +23,11 @@ class TestLongGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("-0.00049"))
def test_calculate_profit_base(self):
profit_base = self.position.calculate_profit_base()
#self.assertEqual(profit_base, Decimal("-0.6457139"))
profit_base = self.position.calculate_profit_base(self.position.exposure)
self.assertEqual(profit_base, Decimal("-0.64571"))
def test_calculate_profit_perc(self):
profit_perc = self.position.calculate_profit_perc()
#self.assertEqual(profit_perc, Decimal("-0.032285695"))
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
self.assertEqual(profit_perc, Decimal("-0.03229"))
@@ -52,11 +50,11 @@ class TestShortGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("0.00049"))
def test_calculate_profit_base(self):
profit_base = self.position.calculate_profit_base()
profit_base = self.position.calculate_profit_base(self.position.exposure)
self.assertEqual(profit_base, Decimal("0.64571"))
def test_calculate_profit_perc(self):
profit_perc = self.position.calculate_profit_perc()
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
self.assertEqual(profit_perc, Decimal("0.03229"))
+11
View File
@@ -0,0 +1,11 @@
argparse==1.2.1
ipython==2.3.1
numpy==1.9.1
pandas==0.15.2
python-dateutil==2.4.0
pytz==2014.10
requests==2.5.1
scikit-learn==0.15.2
scipy==0.15.1
six==1.9.0
wsgiref==0.1.2
+2 -3
View File
@@ -10,8 +10,7 @@ class TestStrategy(object):
def calculate_signals(self, event):
if event.type == 'TICK':
self.ticks += 1
if self.ticks % 5 == 0:
if self.ticks % 200 == 0:
if self.invested == False:
signal = SignalEvent(self.instrument, "market", "buy")
self.events.put(signal)
@@ -20,4 +19,4 @@ class TestStrategy(object):
signal = SignalEvent(self.instrument, "market", "sell")
self.events.put(signal)
self.invested = False
self.ticks += 1
+1 -1
View File
@@ -41,7 +41,7 @@ if __name__ == "__main__":
heartbeat = 0.5 # Half a second between polling
events = Queue.Queue()
equity = Decimal("99999.65")
equity = Decimal("99949.82")
# Trade "Cable"
instrument = "GBP_USD"