Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IG Integration and logging feature #207

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ install:
- pip install multipledispatch
- pip install requests_cache
- pip install pandas_datareader
- git clone https://github.com/ig-python/ig-markets-api-python-library.git
- cd ig-markets-api-python-library
- python setup.py install
- cd ..
- conda list
- python setup.py install

Expand Down
87 changes: 87 additions & 0 deletions qstrader/base_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from qstrader.order.suggested import SuggestedOrder


class AbstractPortfolioHandler(object):
def _create_order_from_signal(self, signal_event):
"""
Take a SignalEvent object and use it to form a
SuggestedOrder object. These are not OrderEvent objects,
as they have yet to be sent to the RiskManager object.
At this stage they are simply "suggestions" that the
RiskManager will either verify, modify or eliminate.
"""
if signal_event.suggested_quantity is None:
quantity = 0
else:
quantity = signal_event.suggested_quantity
order = SuggestedOrder(
signal_event.ticker,
signal_event.action,
quantity=quantity
)
return order

def _place_orders_onto_queue(self, order_list):
"""
Once the RiskManager has verified, modified or eliminated
any order objects, they are placed onto the events queue,
to ultimately be executed by the ExecutionHandler.
"""
for order_event in order_list:
self.events_queue.put(order_event)

def _convert_fill_to_portfolio_update(self, fill_event):
"""
Upon receipt of a FillEvent, the PortfolioHandler converts
the event into a transaction that gets stored in the Portfolio
object. This ensures that the broker and the local portfolio
are "in sync".

In addition, for backtesting purposes, the portfolio value can
be reasonably estimated in a realistic manner, simply by
modifying how the ExecutionHandler object handles slippage,
transaction costs, liquidity and market impact.
"""
action = fill_event.action
ticker = fill_event.ticker
quantity = fill_event.quantity
price = fill_event.price
commission = fill_event.commission
# Create or modify the position from the fill info
self.portfolio.transact_position(
action, ticker, quantity,
price, commission
)

def on_signal(self, signal_event):
"""
This is called by the backtester or live trading architecture
to form the initial orders from the SignalEvent.

These orders are sized by the PositionSizer object and then
sent to the RiskManager to verify, modify or eliminate.

Once received from the RiskManager they are converted into
full OrderEvent objects and sent back to the events queue.
"""
raise NotImplementedError("on_signal is not implemented in the base class!")

def on_fill(self, fill_event):
"""
This is called by the backtester or live trading architecture
to take a FillEvent and update the Portfolio object with new
or modified Positions.

In a backtesting environment these FillEvents will be simulated
by a model representing the execution, whereas in live trading
they will come directly from a brokerage (such as Interactive
Brokers).
"""
raise NotImplementedError("on_fill is not implemented in the base class!")

def update_portfolio_value(self):
"""
Update the portfolio to reflect current market value as
based on last bid/ask of each ticker.
"""
raise NotImplementedError("update_portfolio_value is not implemented in the base class!")
120 changes: 120 additions & 0 deletions qstrader/execution_handler/ig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import logging

from trading_ig import IGStreamService

from qstrader import setup_logging
from .base import AbstractExecutionHandler
from ..event import (FillEvent, EventType)
from ..price_parser import PriceParser

setup_logging


class IGExecutionHandler(AbstractExecutionHandler):
"""
The execution handler for IG Markets
executes Market orders at the moment.

This classes establishes a REST cached IG session and also stream subscription
to get real time updates and confirm when the order has been filled and the spread/commission
"""
# TODO Working orders will be implemented in the future

def __init__(self, events_queue, ig_service, config, compliance=None):
"""
Initialises the handler, setting the event queue
as well as accessing the pricing handler and setting a new cached session against IG Markets API.

Parameters:
events_queue - The Queue of Event objects.
price_handler - The price handlers to obtain price details before executing order
compliance - Compliance object
config - Configuration object
"""
self.events_queue = events_queue
self.fill_event = None
self.ig_service = ig_service
self.compliance = compliance
self.config = config
# Set up logging
self.logger = logging.getLogger(__name__)
self.ig_stream_service, self.ig_stream_session = self._create_streaming_session(ig_service)

def _create_streaming_session(self, ig_service):

# Cretes Streaming service and session
ig_stream_service = IGStreamService(ig_service)
ig_stream_session = ig_stream_service.create_session()

return ig_stream_service, ig_stream_session

def _create_fill_event(self, executed_order):
executed_order["dealReference"]
epic = executed_order["epic"]
# ticker = epic[5:8]
exchange = epic[1:4]
action = executed_order["direction"]
quantity = executed_order["size"]
fill_price = PriceParser.parse(executed_order["level"]) / 10000
timestamp = executed_order["date"]
commission = self.calculate_ig_commission(quantity, fill_price)
self.logger.info("Created filled event")
self.logger.debug('Data received: %s' % executed_order)
self.fill_event = FillEvent(timestamp, epic, action, quantity, exchange, fill_price, commission)
self.events_queue.put(self.fill_event)

def calculate_ig_commission(self, quantity, fill_price):
"""
Calculate the IG Markets commission for
a transaction.
"""
# TODO implement logic if required (IG Market doesn't have commission as it is included in spread)
commission = 0
return PriceParser.parse(commission)

def execute_order(self, event):
"""
Executes Market orders directly, OrderEvents into FillEvents through IG Markets API,
this means it has to track the order synchronously and return confirmation

Parameters:
event - An Event object with order information.
"""

if event.type == EventType.ORDER:
# Obtain values from the OrderEvent
ticker = event.ticker
# Mapping original IB order action to IG terminology
if event.action == "BOT":
action = "BUY"
else:
action = "SELL"
quantity = event.quantity

# Execute Market order
executed_order = self.ig_service.create_open_position("GBP",
action,
ticker,
# str(datetime.now() + timedelta(hours=3)),
"DFB",
False,
False,
None,
None,
None,
"MARKET",
None,
quantity,
None,
None
)

# Track confirmation from IG and update fill price with
if executed_order["dealStatus"] == "ACCEPTED":
self._create_fill_event(executed_order)
if self.compliance is not None:
self.compliance.record_trade(self.fill_event)
else:
self.logger.error('Order execution failed, reason: %s, quantity: %s, ticker: %s' % (executed_order["reason"], quantity, ticker))

return executed_order
77 changes: 77 additions & 0 deletions qstrader/ig_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from qstrader.base_handler import AbstractPortfolioHandler
from qstrader.ig_portfolio import Portfolio


class PortfolioHandler(AbstractPortfolioHandler):
def __init__(
self, initial_cash, events_queue,
price_handler, position_sizer, risk_manager
):
"""
The PortfolioHandler is designed to interact with the
backtesting or live trading overall event-driven
architecture. It exposes two methods, on_signal and
on_fill, which handle how SignalEvent and FillEvent
objects are dealt with.

Each PortfolioHandler contains a Portfolio object,
which stores the actual Position objects.

The PortfolioHandler takes a handle to a PositionSizer
object which determines a mechanism, based on the current
Portfolio, as to how to size a new Order.

The PortfolioHandler also takes a handle to the
RiskManager, which is used to modify any generated
Orders to remain in line with risk parameters.
"""
self.initial_cash = initial_cash
self.events_queue = events_queue
self.price_handler = price_handler
self.position_sizer = position_sizer
self.risk_manager = risk_manager
self.portfolio = Portfolio(price_handler, initial_cash)

def on_signal(self, signal_event):
"""
This is called by the backtester or live trading architecture
to form the initial orders from the SignalEvent.

These orders are sized by the PositionSizer object and then
sent to the RiskManager to verify, modify or eliminate.

Once received from the RiskManager they are converted into
full OrderEvent objects and sent back to the events queue.
"""
# Create the initial order list from a signal event
initial_order = self._create_order_from_signal(signal_event)
# Size the quantity of the initial order
sized_order = self.position_sizer.size_order(
self.portfolio, initial_order
)
# Refine or eliminate the order via the risk manager overlay
order_events = self.risk_manager.refine_orders(
self.portfolio, sized_order
)
# Place orders onto events queue
self._place_orders_onto_queue(order_events)

def on_fill(self, fill_event):
"""
This is called by the backtester or live trading architecture
to take a FillEvent and update the Portfolio object with new
or modified Positions.

In a backtesting environment these FillEvents will be simulated
by a model representing the execution, whereas in live trading
they will come directly from a brokerage (such as Interactive
Brokers).
"""
self._convert_fill_to_portfolio_update(fill_event)

def update_portfolio_value(self):
"""
Update the portfolio to reflect current market value as
based on last bid/ask of each ticker.
"""
self.portfolio._update_portfolio()
Loading