from __future__ import (absolute_import, division, print_function,
unicode_literals)
import collections
from backtrader import BrokerBase, Order, BuyOrder, SellOrder
from backtrader.utils.py3 import with_metaclass
from backtrader.comminfo import CommInfoBase
from backtrader.position import Position
from mql5_zmq_backtrader import mt5store
[docs]class MTraderCommInfo(CommInfoBase):
[docs] def getvaluesize(self, size, price):
# In real life the margin approaches the price
return abs(size) * price
[docs] def getoperationcost(self, size, price):
"""Returns the needed amount of cash an operation would cost"""
# Same reasoning as above
return abs(size) * price
[docs]class MTraderBroker(with_metaclass(MetaMTraderBroker, BrokerBase)):
"""Broker implementation for MetaTrader 5.
This class maps the orders/positions from MetaTrader to the
internal API of `backtrader`.
Params:
- `use_positions` (Ram default:`True`): When connecting to the broker
provider use the existing positions to kickstart the broker.
Set to `False` during instantiation to disregard any existing
position
"""
# TODO: close positions
params = (
('use_positions', True),
)
def __init__(self, **kwargs):
super(MTraderBroker, self).__init__()
self.o = mt5store.MTraderStore(**kwargs)
self.orders = collections.OrderedDict() # orders by order id
self.notifs = collections.deque() # holds orders which are notified
self.opending = collections.defaultdict(list) # pending transmission
self.brackets = dict() # confirmed brackets
self.startingcash = self.cash = 0.0
self.startingvalue = self.value = 0.0
self.positions = collections.defaultdict(Position)
self.addcommissioninfo(self, MTraderCommInfo(mult=1.0, stocklike=False))
[docs] def start(self):
super(MTraderBroker, self).start()
self.addcommissioninfo(self, MTraderCommInfo(mult=1.0, stocklike=False))
self.o.start(broker=self)
# Check MetaTrader account
self.o.check_account()
# Get balance on start
self.o.get_balance()
self.startingcash = self.cash = self.o.get_cash()
self.startingvalue = self.value = self.o.get_value()
if self.p.use_positions:
for p in self.o.get_positions():
# print('position for instrument:', p.symbol)
is_sell = p.type.endswith('_SELL')
size = float(p.volume)
if is_sell:
size = -size
price = float(p.open)
self.positions[p.symbol] = Position(size, price)
[docs] def data_started(self, data):
pos = self.getposition(data)
if pos.size == 0:
return
if pos.size < 0:
order = SellOrder(data=data, size=pos.size, price=pos.price,
exectype=Order.Market, simulated=True)
elif pos.size > 0:
order = BuyOrder(data=data, size=pos.size, price=pos.price,
exectype=Order.Market, simulated=True)
order.addcomminfo(self.getcommissioninfo(data))
order.execute(0, pos.size, pos.price,
0, 0.0, 0.0,
pos.size, 0.0, 0.0,
0.0, 0.0,
pos.size, pos.price)
order.completed()
self.notify(order)
[docs] def stop(self):
super(MTraderBroker, self).stop()
self.o.stop()
[docs] def getcash(self):
# This call cannot block if no answer is available from MTrader
self.cash = cash = self.o.get_cash()
return cash
[docs] def getvalue(self, datas=None):
self.value = self.o.get_value()
return self.value
[docs] def getposition(self, data, clone=True):
# return self.o.getposition(data._dataname, clone=clone)
pos = self.positions[data._dataname]
if clone:
pos = pos.clone()
return pos
[docs] def orderstatus(self, order):
o = self.orders[order.ref]
return o.status
def _submit(self, oref):
order = self.orders[oref]
order.submit(self)
self.notify(order)
def _reject(self, oref):
order = self.orders[oref]
order.reject(self)
self.notify(order)
def _accept(self, oref):
order = self.orders[oref]
order.accept()
self.notify(order)
def _cancel(self, oref):
order = self.orders[oref]
order.cancel()
self.notify(order)
self._bracketize(order, cancel=True)
def _expire(self, oref):
order = self.orders[oref]
order.expire()
self.notify(order)
self._bracketize(order, cancel=True)
def _bracketize(self, order, cancel=False):
pref = getattr(order.parent, 'ref', order.ref) # parent ref or self
br = self.brackets.pop(pref, None) # to avoid recursion
if br is None:
return
if not cancel:
if len(br) == 3: # all 3 orders in place, parent was filled
br = br[1:] # discard index 0, parent
for o in br:
o.activate() # simulate activate for children
self.brackets[pref] = br # not done - reinsert children
elif len(br) == 2: # filling a children
oidx = br.index(order) # find index to filled (0 or 1)
self._cancel(br[1 - oidx].ref) # cancel remaining (1 - 0 -> 1)
else:
# Any cancellation cancel the others
for o in br:
if o.alive():
self._cancel(o.ref)
def _fill_external(self, data, size, price):
if size == 0:
return
pos = self.getposition(data, clone=False)
pos.update(size, price)
if size < 0:
order = SellOrder(data=data,
size=size, price=price,
exectype=Order.Market,
simulated=True)
else:
order = BuyOrder(data=data,
size=size, price=price,
exectype=Order.Market,
simulated=True)
order.addcomminfo(self.getcommissioninfo(data))
order.execute(0, size, price,
0, 0.0, 0.0,
size, 0.0, 0.0,
0.0, 0.0,
size, price)
order.completed()
self.notify(order)
def _fill(self, oref, size, price, reason, **kwargs):
order = self.orders[oref]
if not order.alive(): # can be a bracket
pref = getattr(order.parent, 'ref', order.ref)
if pref not in self.brackets:
msg = ('Order fill received for {}, with price {} and size {} '
'but order is no longer alive and is not a bracket. '
'Unknown situation {}')
msg = msg.format(order.ref, price, size, reason)
self.o.put_notification(msg)
return
# [main, stopside, takeside], neg idx to array are -3, -2, -1
if reason == 'STOP_LOSS_ORDER':
order = self.brackets[pref][-2]
elif reason == 'TAKE_PROFIT_ORDER':
order = self.brackets[pref][-1]
else:
msg = ('Order fill received for {}, with price {} and size {} '
'but order is no longer alive and is a bracket. '
'Unknown situation {}')
msg = msg.format(order.ref, price, size, reason)
self.o.put_notification(msg)
return
data = order.data
pos = self.getposition(data, clone=False)
psize, pprice, opened, closed = pos.update(size, price)
comminfo = self.getcommissioninfo(data)
closedvalue = closedcomm = 0.0
openedvalue = openedcomm = 0.0
margin = pnl = 0.0
order.execute(data.datetime[0], size, price,
closed, closedvalue, closedcomm,
opened, openedvalue, openedcomm,
margin, pnl,
psize, pprice)
if order.executed.remsize:
order.partial()
self.notify(order)
else:
order.completed()
self.notify(order)
self._bracketize(order)
def _transmit(self, order):
oref = order.ref
pref = getattr(order.parent, 'ref', oref) # parent ref or self
if order.transmit:
if oref != pref: # children order
# Put parent in orders dict, but add stopside and takeside
# to order creation. Return the takeside order, to have 3s
takeside = order # alias for clarity
parent, stopside = self.opending.pop(pref)
for o in parent, stopside, takeside:
self.orders[o.ref] = o # write them down
self.brackets[pref] = [parent, stopside, takeside]
self.o.order_create(parent, stopside, takeside)
return takeside # parent was already returned
else: # Parent order, which is not being transmitted
self.orders[order.ref] = order
return self.o.order_create(order)
# Not transmitting
self.opending[pref].append(order)
return order
[docs] def buy(self, owner, data,
size, price=None, plimit=None,
exectype=None, valid=None, tradeid=0, oco=None,
trailamount=None, trailpercent=None,
parent=None, transmit=True,
**kwargs):
#ram
print("mt5broker **kwargs", kwargs)
order = BuyOrder(owner=owner, data=data,
size=size, price=price, pricelimit=plimit,
exectype=exectype, valid=valid, tradeid=tradeid,
trailamount=trailamount, trailpercent=trailpercent,
parent=parent, transmit=transmit)
order.addinfo(**kwargs)
order.addcomminfo(self.getcommissioninfo(data))
return self._transmit(order)
[docs] def sell(self, owner, data,
size, price=None, plimit=None,
exectype=None, valid=None, tradeid=0, oco=None,
trailamount=None, trailpercent=None,
parent=None, transmit=True,
**kwargs):
order = SellOrder(owner=owner, data=data,
size=size, price=price, pricelimit=plimit,
exectype=exectype, valid=valid, tradeid=tradeid,
trailamount=trailamount, trailpercent=trailpercent,
parent=parent, transmit=transmit)
order.addinfo(**kwargs)
order.addcomminfo(self.getcommissioninfo(data))
return self._transmit(order)
[docs] def cancel(self, order):
if not self.orders.get(order.ref, False):
return
if order.status == Order.Cancelled: # already cancelled
return
return self.o.order_cancel(order)
[docs] def notify(self, order):
self.notifs.append(order.clone())
[docs] def get_notification(self):
if not self.notifs:
return None
return self.notifs.popleft()
[docs] def next(self):
self.notifs.append(None) # mark notification boundary