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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user