diff --git a/README.md b/README.md new file mode 100644 index 0000000..adbbf9b --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index 789247a..4f12a3d 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -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, diff --git a/portfolio/portfolio_test.py b/portfolio/portfolio_test.py index 4c747a8..0e7fec9 100644 --- a/portfolio/portfolio_test.py +++ b/portfolio/portfolio_test.py @@ -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 diff --git a/portfolio/position.py b/portfolio/position.py index 4d6a28f..6d14e4e 100644 --- a/portfolio/position.py +++ b/portfolio/position.py @@ -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) diff --git a/portfolio/position_test.py b/portfolio/position_test.py index 2e6eec9..1040510 100644 --- a/portfolio/position_test.py +++ b/portfolio/position_test.py @@ -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")) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3c78ae1 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/strategy/strategy.py b/strategy/strategy.py index f765d5e..0db1426 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -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 - \ No newline at end of file + self.ticks += 1 \ No newline at end of file diff --git a/trading/trading.py b/trading/trading.py index 236b9ee..01e95d4 100644 --- a/trading/trading.py +++ b/trading/trading.py @@ -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"