From b0b19d603df49e8db2ab8ce4c32ad659a27f64be Mon Sep 17 00:00:00 2001 From: Michael Halls-Moore Date: Tue, 3 Feb 2015 13:33:33 +0000 Subject: [PATCH] Added remaining trading system around portfolio in order to compare with OANDA. --- __init__.py | 0 event/__init__.py | 0 event/event.py | 28 ++++++++++++++++ execution/__init__.py | 0 execution/execution.py | 33 +++++++++++++++++++ portfolio/portfolio.py | 38 ++++++++++++--------- settings.py | 21 ++++++++++++ strategy/__init__.py | 0 strategy/strategy.py | 18 ++++++++++ streaming/__init__.py | 0 streaming/streaming.py | 57 ++++++++++++++++++++++++++++++++ trading/__init__.py | 0 trading/trading.py | 75 ++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 255 insertions(+), 15 deletions(-) create mode 100644 __init__.py create mode 100644 event/__init__.py create mode 100644 event/event.py create mode 100644 execution/__init__.py create mode 100644 execution/execution.py create mode 100644 settings.py create mode 100644 strategy/__init__.py create mode 100644 strategy/strategy.py create mode 100644 streaming/__init__.py create mode 100644 streaming/streaming.py create mode 100644 trading/__init__.py create mode 100644 trading/trading.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/event/__init__.py b/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/event/event.py b/event/event.py new file mode 100644 index 0000000..c2ec0fd --- /dev/null +++ b/event/event.py @@ -0,0 +1,28 @@ +class Event(object): + pass + + +class TickEvent(Event): + def __init__(self, instrument, time, bid, ask): + self.type = 'TICK' + self.instrument = instrument + self.time = time + self.bid = bid + self.ask = ask + + +class SignalEvent(Event): + def __init__(self, instrument, order_type, side): + self.type = 'SIGNAL' + self.instrument = instrument + self.order_type = order_type + self.side = side + + +class OrderEvent(Event): + def __init__(self, instrument, units, order_type, side): + self.type = 'ORDER' + self.instrument = instrument + self.units = units + self.order_type = order_type + self.side = side diff --git a/execution/__init__.py b/execution/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/execution/execution.py b/execution/execution.py new file mode 100644 index 0000000..b571c08 --- /dev/null +++ b/execution/execution.py @@ -0,0 +1,33 @@ +import httplib +import urllib + + +class Execution(object): + def __init__(self, domain, access_token, account_id): + self.domain = domain + self.access_token = access_token + self.account_id = account_id + self.conn = self.obtain_connection() + + def obtain_connection(self): + return httplib.HTTPSConnection(self.domain) + + def execute_order(self, event): + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer " + self.access_token + } + params = urllib.urlencode({ + "instrument" : event.instrument, + "units" : event.units, + "type" : event.order_type, + "side" : event.side + }) + self.conn.request( + "POST", + "/v1/accounts/%s/orders" % str(self.account_id), + params, headers + ) + response = self.conn.getresponse().read() + print response + \ No newline at end of file diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index 8036003..2b12489 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -1,20 +1,22 @@ from copy import deepcopy -from position import Position +from qsforex.event.event import OrderEvent +from qsforex.portfolio.position import Position class Portfolio(object): def __init__( - self, ticker, base="GBP", leverage=20, + self, ticker, events, base="GBP", leverage=20, equity=100000.0, risk_per_trade=0.02 ): self.ticker = ticker + self.events = events self.base = base self.leverage = leverage self.equity = equity self.balance = deepcopy(self.equity) self.risk_per_trade = risk_per_trade - self.trade_units = self.calc_risk_position_size() + self.trade_units = 100000#self.calc_risk_position_size() self.positions = {} def calc_risk_position_size(self): @@ -76,16 +78,16 @@ class Portfolio(object): def execute_signal(self, signal_event): side = signal_event.side - market = signal_event.market - units = self.risk_per_trade + market = signal_event.instrument + units = int(self.trade_units) # Check side for correct bid/ask prices - if side == "LONG": - add_price = self.ticker.cur_ask - remove_price = self.ticker.cur_bid - else: - add_price = self.ticker.cur_bid - remove_price = self.ticker.cur_ask + #if side == "buy": + add_price = self.ticker.cur_ask + remove_price = self.ticker.cur_bid + #else: + #add_price = self.ticker.cur_bid + #remove_price = self.ticker.cur_ask exposure = float(units) # If there is no position, create one @@ -94,10 +96,13 @@ class Portfolio(object): side, market, units, exposure, add_price, remove_price ) + order = OrderEvent(market, units, "market", "buy") + self.events.put(order) # If a position exists add or remove units else: + ps = self.positions[market] # Check if the sides equal - if side == ps[market].side: + if side == ps.side: # Add to the position add_position_units( market, units, exposure, @@ -108,6 +113,8 @@ class Portfolio(object): if units == ps.units: # Close the position self.close_position(market, remove_price) + order = OrderEvent(market, units, "market", "sell") + self.events.put(order) elif units < ps.units: # Remove from the position self.remove_position_units( @@ -119,12 +126,13 @@ class Portfolio(object): new_units = units - ps.units self.close_position(market, remove_price) - if side == "LONG": - new_side = "SHORT" + if side == "buy": + new_side = "sell" else: - new_side = "LONG" + new_side = "sell" new_exposure = float(units) self.add_new_position( new_side, market, new_units, new_exposure, add_price, remove_price ) + print "Balance: %0.2f" % self.balance \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..c7be22d --- /dev/null +++ b/settings.py @@ -0,0 +1,21 @@ +import os + + +ENVIRONMENTS = { + "streaming": { + "real": "stream-fxtrade.oanda.com", + "practice": "stream-fxpractice.oanda.com", + "sandbox": "stream-sandbox.oanda.com" + }, + "api": { + "real": "api-fxtrade.oanda.com", + "practice": "api-fxpractice.oanda.com", + "sandbox": "api-sandbox.oanda.com" + } +} + +DOMAIN = "practice" +STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN] +API_DOMAIN = ENVIRONMENTS["api"][DOMAIN] +ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None) +ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None) \ No newline at end of file diff --git a/strategy/__init__.py b/strategy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/strategy/strategy.py b/strategy/strategy.py new file mode 100644 index 0000000..89cd890 --- /dev/null +++ b/strategy/strategy.py @@ -0,0 +1,18 @@ +from qsforex.event.event import SignalEvent + + +class TestRandomStrategy(object): + def __init__(self, instrument, events): + self.instrument = instrument + self.events = events + self.ticks = 0 + + def calculate_signals(self, event): + if event.type == 'TICK': + self.ticks += 1 + if self.ticks == 2: + signal = SignalEvent(self.instrument, "market", "buy") + self.events.put(signal) + if self.ticks == 10: + signal = SignalEvent(self.instrument, "market", "sell") + self.events.put(signal) diff --git a/streaming/__init__.py b/streaming/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streaming/streaming.py b/streaming/streaming.py new file mode 100644 index 0000000..eb95e66 --- /dev/null +++ b/streaming/streaming.py @@ -0,0 +1,57 @@ +import requests +import json + +from qsforex.event.event import TickEvent + + +class StreamingForexPrices(object): + def __init__( + self, domain, access_token, + account_id, instruments, events_queue + ): + self.domain = domain + self.access_token = access_token + self.account_id = account_id + self.instruments = instruments + self.events_queue = events_queue + self.cur_bid = None + self.cur_ask = None + + def connect_to_stream(self): + try: + s = requests.Session() + url = "https://" + self.domain + "/v1/prices" + headers = {'Authorization' : 'Bearer ' + self.access_token} + params = {'instruments' : self.instruments, 'accountId' : self.account_id} + req = requests.Request('GET', url, headers=headers, params=params) + pre = req.prepare() + resp = s.send(pre, stream=True, verify=False) + return resp + except Exception as e: + s.close() + print "Caught exception when connecting to stream\n" + str(e) + + def stream_to_queue(self): + response = self.connect_to_stream() + if response.status_code != 200: + return + for line in response.iter_lines(1): + if line: + try: + msg = json.loads(line) + except Exception as e: + print "Caught exception when converting message into json\n" + str(e) + return + if msg.has_key("instrument") or msg.has_key("tick"): + print msg + instrument = msg["tick"]["instrument"] + time = msg["tick"]["time"] + bid = msg["tick"]["bid"] + ask = msg["tick"]["ask"] + self.cur_bid = bid + self.cur_ask = ask + tev = TickEvent(instrument, time, bid, ask) + self.events_queue.put(tev) + + + diff --git a/trading/__init__.py b/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trading/trading.py b/trading/trading.py new file mode 100644 index 0000000..ab8a251 --- /dev/null +++ b/trading/trading.py @@ -0,0 +1,75 @@ +import copy +import Queue +import threading +import time + +from qsforex.execution.execution import Execution +from qsforex.portfolio.portfolio import Portfolio +from qsforex.settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID +from qsforex.strategy.strategy import TestRandomStrategy +from qsforex.streaming.streaming import StreamingForexPrices + + +def trade(events, strategy, portfolio, execution): + """ + Carries out an infinite while loop that polls the + events queue and directs each event to either the + strategy component of the execution handler. The + loop will then pause for "heartbeat" seconds and + continue. + """ + while True: + try: + event = events.get(False) + except Queue.Empty: + pass + else: + if event is not None: + if event.type == 'TICK': + strategy.calculate_signals(event) + elif event.type == 'SIGNAL': + portfolio.execute_signal(event) + elif event.type == 'ORDER': + execution.execute_order(event) + time.sleep(heartbeat) + + +if __name__ == "__main__": + heartbeat = 0.5 # Half a second between polling + events = Queue.Queue() + + # Trade "Cable" + instrument = "GBP_USD" + + # Create the OANDA market price streaming class + # making sure to provide authentication commands + prices = StreamingForexPrices( + STREAM_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID, + instrument, events + ) + + # Create the strategy/signal generator, passing the + # instrument and the events queue + strategy = TestRandomStrategy(instrument, events) + + # Create the portfolio object that will be used to + # compare the OANDA positions with the local, to + # ensure backtesting integrity. + portfolio = Portfolio(prices, events, equity=98505.02) + + # Create the execution handler making sure to + # provide authentication commands + execution = Execution(API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID) + + # Create two separate threads: One for the trading loop + # and another for the market price streaming class + trade_thread = threading.Thread( + target=trade, args=( + events, strategy, portfolio, execution + ) + ) + price_thread = threading.Thread(target=prices.stream_to_queue, args=[]) + + # Start both threads + trade_thread.start() + price_thread.start() \ No newline at end of file