diff --git a/data/streaming.py b/data/streaming.py index fed07ea..3abe7dd 100644 --- a/data/streaming.py +++ b/data/streaming.py @@ -1,9 +1,11 @@ from __future__ import print_function from decimal import Decimal, getcontext, ROUND_HALF_DOWN -import requests +import logging import json +import requests + from qsforex.event.event import TickEvent from qsforex.data.price import PriceHandler @@ -19,6 +21,7 @@ class StreamingForexPrices(PriceHandler): self.events_queue = events_queue self.pairs = pairs self.prices = self._set_up_prices_dict() + self.logger = logging.getLogger(__name__) def invert_prices(self, pair, bid, ask): """ @@ -38,12 +41,13 @@ class StreamingForexPrices(PriceHandler): def connect_to_stream(self): pairs_oanda = ["%s_%s" % (p[:3], p[3:]) for p in self.pairs] + pair_list = ",".join(pairs_oanda) try: requests.packages.urllib3.disable_warnings() s = requests.Session() url = "https://" + self.domain + "/v1/prices" headers = {'Authorization' : 'Bearer ' + self.access_token} - params = {'instruments' : pairs_oanda, 'accountId' : self.account_id} + params = {'instruments' : pair_list, 'accountId' : self.account_id} req = requests.Request('GET', url, headers=headers, params=params) pre = req.prepare() resp = s.send(pre, stream=True, verify=False) @@ -62,10 +66,12 @@ class StreamingForexPrices(PriceHandler): dline = line.decode('utf-8') msg = json.loads(dline) except Exception as e: - print("Caught exception when converting message into json\n" + str(e)) + self.logger.error( + "Caught exception when converting message into json: %s" % str(e) + ) return if "instrument" in msg or "tick" in msg: - print(msg) + self.logger.debug(msg) getcontext().rounding = ROUND_HALF_DOWN instrument = msg["tick"]["instrument"].replace("_", "") time = msg["tick"]["time"] diff --git a/event/event.py b/event/event.py index 0552a1c..0a7c118 100644 --- a/event/event.py +++ b/event/event.py @@ -10,15 +10,33 @@ class TickEvent(Event): self.bid = bid self.ask = ask + def __str__(self): + return "Type: %s, Instrument: %s, Time: %s, Bid: %s, Ask: %s" % ( + str(self.type), str(self.instrument), + str(self.time), str(self.bid), str(self.ask) + ) + + def __repr__(self): + return str(self) + class SignalEvent(Event): def __init__(self, instrument, order_type, side, time): self.type = 'SIGNAL' self.instrument = instrument self.order_type = order_type - self.side = side + self.side = side self.time = time # Time of the last tick that generated the signal + def __str__(self): + return "Type: %s, Instrument: %s, Order Type: %s, Side: %s" % ( + str(self.type), str(self.instrument), + str(self.order_type), str(self.side) + ) + + def __repr__(self): + return str(self) + class OrderEvent(Event): def __init__(self, instrument, units, order_type, side): @@ -26,4 +44,13 @@ class OrderEvent(Event): self.instrument = instrument self.units = units self.order_type = order_type - self.side = side + self.side = side + + def __str__(self): + return "Type: %s, Instrument: %s, Units: %s, Order Type: %s, Side: %s" % ( + str(self.type), str(self.instrument), str(self.units), + str(self.order_type), str(self.side) + ) + + def __repr__(self): + return str(self) \ No newline at end of file diff --git a/execution/execution.py b/execution/execution.py index 1f4ff40..3197dc7 100644 --- a/execution/execution.py +++ b/execution/execution.py @@ -5,6 +5,7 @@ try: import httplib except ImportError: import http.client as httplib +import logging try: from urllib import urlencode except ImportError: @@ -47,6 +48,7 @@ class OANDAExecutionHandler(ExecutionHandler): self.access_token = access_token self.account_id = account_id self.conn = self.obtain_connection() + self.logger = logging.getLogger(__name__) def obtain_connection(self): return httplib.HTTPSConnection(self.domain) @@ -68,6 +70,6 @@ class OANDAExecutionHandler(ExecutionHandler): "/v1/accounts/%s/orders" % str(self.account_id), params, headers ) - response = self.conn.getresponse().read() - print(response) + response = self.conn.getresponse().read().decode("utf-8").replace("\n","").replace("\t","") + self.logger.debug(response) \ No newline at end of file diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..ee8ec95 --- /dev/null +++ b/logging.conf @@ -0,0 +1,28 @@ +[loggers] +keys=root,qsforex.trading.trading + +[handlers] +keys=consoleHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[logger_qsforex.trading.trading] +level=DEBUG +handlers=consoleHandler +qualname=qsforex.trading.trading +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index faa14ed..c86aafb 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -2,6 +2,7 @@ from __future__ import print_function from copy import deepcopy from decimal import Decimal, getcontext, ROUND_HALF_DOWN +import logging import os import pandas as pd @@ -30,6 +31,7 @@ class Portfolio(object): self.positions = {} if self.backtest: self.backtest_file = self.create_equity_file() + self.logger = logging.getLogger(__name__) def calc_risk_position_size(self): return self.equity * self.risk_per_trade @@ -126,51 +128,66 @@ class Portfolio(object): print(out_line[:-2]) self.backtest_file.write(out_line) - def execute_signal(self, signal_event): - side = signal_event.side - currency_pair = signal_event.instrument - units = int(self.trade_units) - time = signal_event.time - - # If there is no position, create one - if currency_pair not in self.positions: - if side == "buy": - position_type = "long" + def execute_signal(self, signal_event): + # Check that the prices ticker contains all necessary + # currency pairs prior to executing an order + execute = True + tp = self.ticker.prices + for pair in tp: + if tp[pair]["ask"] is None or tp[pair]["bid"] is None: + execute = False + + # All necessary pricing data is available, + # we can execute + if execute: + side = signal_event.side + currency_pair = signal_event.instrument + units = int(self.trade_units) + time = signal_event.time + + # If there is no position, create one + if currency_pair not in self.positions: + if side == "buy": + position_type = "long" + else: + position_type = "short" + self.add_new_position( + position_type, currency_pair, + units, self.ticker + ) + + # If a position exists add or remove units else: - position_type = "short" - self.add_new_position( - position_type, currency_pair, - units, self.ticker - ) + ps = self.positions[currency_pair] - # If a position exists add or remove units + if side == "buy" and ps.position_type == "long": + add_position_units(currency_pair, units) + + elif side == "sell" and ps.position_type == "long": + if units == ps.units: + self.close_position(currency_pair) + # TODO: Allow units to be added/removed + elif units < ps.units: + return + elif units > ps.units: + return + + elif side == "buy" and ps.position_type == "short": + if units == ps.units: + self.close_position(currency_pair) + # TODO: Allow units to be added/removed + elif units < ps.units: + return + elif units > ps.units: + return + + elif side == "sell" and ps.position_type == "short": + add_position_units(currency_pair, units) + + order = OrderEvent(currency_pair, units, "market", side) + self.events.put(order) + + self.logger.info("Portfolio Balance: %s" % self.balance) else: - ps = self.positions[currency_pair] - - if side == "buy" and ps.position_type == "long": - add_position_units(currency_pair, units) - - elif side == "sell" and ps.position_type == "long": - if units == ps.units: - self.close_position(currency_pair) - # TODO: Allow units to be added/removed - elif units < ps.units: - return - elif units > ps.units: - return - - elif side == "buy" and ps.position_type == "short": - if units == ps.units: - self.close_position(currency_pair) - # TODO: Allow units to be added/removed - elif units < ps.units: - return - elif units > ps.units: - return - - elif side == "sell" and ps.position_type == "short": - add_position_units(currency_pair, units) - - order = OrderEvent(currency_pair, units, "market", side) - self.events.put(order) + self.logger.info("Unable to execute order as price data was insufficient.") \ No newline at end of file diff --git a/portfolio/portfolio_test.py b/portfolio/portfolio_test.py index 6c8d939..e6b8602 100644 --- a/portfolio/portfolio_test.py +++ b/portfolio/portfolio_test.py @@ -212,7 +212,7 @@ class TestPortfolio(unittest.TestCase): ) self.assertTrue(rpu) self.assertEqual(ps.units, Decimal("7000")) - self.assertEqual(self.port.balance, Decimal("99988.84")) + self.assertEqual(self.port.balance, Decimal("99988.83")) def test_close_position_long(self): position_type = "long" @@ -265,7 +265,7 @@ class TestPortfolio(unittest.TestCase): cp = self.port.close_position(currency_pair) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("100026.64")) + self.assertEqual(self.port.balance, Decimal("100026.63")) def test_close_position_short(self): position_type = "short" @@ -312,13 +312,13 @@ class TestPortfolio(unittest.TestCase): ) self.assertTrue(rpu) self.assertEqual(ps.units, Decimal("7000")) - self.assertEqual(self.port.balance, Decimal("99988.84")) + self.assertEqual(self.port.balance, Decimal("99988.83")) # Close the position cp = self.port.close_position(currency_pair) self.assertTrue(cp) self.assertRaises(ps) # Key doesn't exist - self.assertEqual(self.port.balance, Decimal("99962.80")) + self.assertEqual(self.port.balance, Decimal("99962.77")) if __name__ == "__main__": diff --git a/portfolio/position.py b/portfolio/position.py index f2fc385..def814b 100644 --- a/portfolio/position.py +++ b/portfolio/position.py @@ -24,7 +24,7 @@ class Position(object): ticker_cur = self.ticker.prices[self.currency_pair] if self.position_type == "long": self.avg_price = Decimal(str(ticker_cur["ask"])) - self.cur_price = Decimal(str(ticker_cur["bid"])) + self.cur_price = Decimal(str(ticker_cur["bid"])) else: self.avg_price = Decimal(str(ticker_cur["bid"])) self.cur_price = Decimal(str(ticker_cur["ask"])) @@ -83,11 +83,11 @@ class Position(object): ticker_cp = self.ticker.prices[self.currency_pair] ticker_qh = self.ticker.prices[self.quote_home_currency_pair] if self.position_type == "long": - remove_price = ticker_cp["ask"] - qh_close = ticker_qh["bid"] - else: remove_price = ticker_cp["bid"] qh_close = ticker_qh["ask"] + else: + remove_price = ticker_cp["ask"] + qh_close = ticker_qh["bid"] self.units -= dec_units self.update_position_price() # Calculate PnL @@ -99,11 +99,9 @@ class Position(object): ticker_cp = self.ticker.prices[self.currency_pair] ticker_qh = self.ticker.prices[self.quote_home_currency_pair] if self.position_type == "long": - remove_price = ticker_cp["ask"] - qh_close = ticker_qh["bid"] - else: - remove_price = ticker_cp["bid"] qh_close = ticker_qh["ask"] + else: + qh_close = ticker_qh["bid"] self.update_position_price() # Calculate PnL pnl = self.calculate_pips() * qh_close * self.units diff --git a/strategy/strategy.py b/strategy/strategy.py index 0c1f874..248e592 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -20,7 +20,7 @@ class TestStrategy(object): self.invested = False def calculate_signals(self, event): - if event.type == 'TICK': + if event.type == 'TICK' and event.instrument == self.pairs[0]: if self.ticks % 5 == 0: if self.invested == False: signal = SignalEvent(self.pairs[0], "market", "buy", event.time) diff --git a/trading/trading.py b/trading/trading.py index c2e9b79..e418896 100644 --- a/trading/trading.py +++ b/trading/trading.py @@ -1,5 +1,7 @@ import copy from decimal import Decimal, getcontext +import logging +import logging.config try: import Queue as queue except ImportError: @@ -30,16 +32,23 @@ def trade(events, strategy, portfolio, execution, heartbeat): else: if event is not None: if event.type == 'TICK': + logger.info("Received new tick event: %s", event) strategy.calculate_signals(event) portfolio.update_portfolio(event) elif event.type == 'SIGNAL': + logger.info("Received new signal event: %s", event) portfolio.execute_signal(event) elif event.type == 'ORDER': + logger.info("Received new order event: %s", event) execution.execute_order(event) time.sleep(heartbeat) if __name__ == "__main__": + # Set up logging + logging.config.fileConfig('../logging.conf') + logger = logging.getLogger('qsforex.trading.trading') + # Set the number of decimal places to 2 getcontext().prec = 2 @@ -47,8 +56,8 @@ if __name__ == "__main__": events = queue.Queue() equity = settings.EQUITY - # Trade "Cable" - pairs = ["GBPUSD"] + # Pairs to include in streaming data set + pairs = ["EURUSD", "GBPUSD"] # Create the OANDA market price streaming class # making sure to provide authentication commands @@ -86,5 +95,7 @@ if __name__ == "__main__": price_thread = threading.Thread(target=prices.stream_to_queue, args=[]) # Start both threads + logger.info("Starting trading thread") trade_thread.start() + logger.info("Starting price streaming thread") price_thread.start()