import collections
from backtrader import BrokerBase, BuyOrder, Order, SellOrder
from backtrader.comminfo import CommInfoBase
from backtrader.position import Position
from backtrader.utils.py3 import with_metaclass
from koapy.backtrader.KiwoomOpenApiPlusStore import KiwoomOpenApiPlusStore
[docs]class KiwoomOpenApiPlusCommInfo(CommInfoBase):
# pylint: disable=no-member
[docs] params = (
("stocklike", True),
("commtype", CommInfoBase.COMM_PERC),
("percabs", False),
("commission", 0.015),
("tax", 0.25),
("mult", 1.0),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._commtype == self.COMM_PERC and not self.p.percabs:
self.p.tax /= 100.0
def _getcommissionbuy(self, size, price, pseudoexec):
return size * price * self.p.commission * self.p.mult
def _getcommissionsell(self, size, price, pseudoexec):
return abs(size) * price * (self.p.commission + self.p.tax) * self.p.mult
def _getcommission(self, size, price, pseudoexec):
if size < 0:
return self._getcommissionsell(size, price, pseudoexec)
else:
return self._getcommissionbuy(size, price, pseudoexec)
[docs]class KiwoomOpenApiPlusBroker(with_metaclass(MetaKiwoomOpenApiPlusBroker, BrokerBase)):
[docs] params = (
("use_positions", True),
("commission", KiwoomOpenApiPlusCommInfo()),
)
_store = KiwoomOpenApiPlusStore
def __init__(self, *args, **kwargs):
super().__init__()
self.k = self._store(*args, **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)
[docs] def start(self):
super().start()
self.k.start(broker=self)
self.startingcash = self.cash = self.k.get_cash()
self.startingvalue = self.value = self.k.get_value()
if self.p.use_positions: # pylint: disable=no-member
for p in self.k.get_positions():
is_sell = p["side"] == "sell"
size = p["units"]
if is_sell:
size = -size
price = p["avgPrice"]
self.positions[p["instrument"]] = Position(size, price)
[docs] def data_started(self, data):
pos = self.getposition(data)
if pos.size < 0:
# pylint: disable=unexpected-keyword-arg
order = SellOrder(
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)
elif pos.size > 0:
# pylint: disable=unexpected-keyword-arg
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().stop()
self.k.stop()
[docs] def getcash(self):
self.cash = self.k.get_cash()
return self.cash
[docs] def getvalue(self, datas=None):
self.value = self.k.get_value()
return self.value
[docs] def getposition(self, data, clone=True): # pylint: disable=arguments-differ
pos = self.positions[data._dataname] # pylint: disable=protected-access
if clone:
pos = pos.clone()
return pos
[docs] def orderstatus(self, order):
order = self.orders[order.ref]
return order.status
def _submit(self, oref):
order = self.orders[oref]
order.submit(self)
self.notify(order)
for o in self._bracketnotif(order):
o.submit(self)
self.notify(o)
def _reject(self, oref):
order = self.orders[oref]
order.reject(self)
self.notify(order)
self._bracketize(order, cancel=True)
def _accept(self, oref):
order = self.orders[oref]
order.accept()
self.notify(order)
for o in self._bracketnotif(order):
o.accept(self)
self.notify(o)
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 _bracketnotif(self, order):
pref = getattr(order.parent, "ref", order.ref) # parent ref or self
br = self.brackets.get(pref, None) # to avoid recursion
return br[-2:] if br is not None else []
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(self, oref, size, price, ttype, **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.format(order.ref, price, size)
self.put_notification(
msg, order, price, size
) # pylint: disable=no-member
return
# [main, stopside, takeside], neg idx to array are -3, -2, -1
if ttype == "STOP_LOSS_FILLED":
order = self.brackets[pref][-2]
elif ttype == "TAKE_PROFIT_FILLED":
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.format(order.ref, price, size)
self.put_notification(
msg, order, price, size
) # pylint: disable=no-member
return
data = order.data
pos = self.getposition(data, clone=False)
psize, pprice, opened, closed = pos.update(size, price)
comminfo = self.getcommissioninfo(data) # pylint: disable=unused-variable
closedvalue = closedcomm = 0.0
openedvalue = openedcomm = 0.0
margin = pnl = 0.0
if size >= 0:
openedvalue = comminfo.getvaluesize(size, price)
openedcomm = comminfo.confirmexec(size, price)
else:
closedvalue = comminfo.getvaluesize(size, price)
closedcomm = comminfo.confirmexec(size, price)
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.k.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.k.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
): # pylint: disable=arguments-differ
# pylint: disable=unexpected-keyword-arg
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
): # pylint: disable=arguments-differ
# pylint: disable=unexpected-keyword-arg
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):
order = self.orders[order.ref]
if order.status == Order.Cancelled:
return
return self.k.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)
# below may not accurate
[docs] def submit(self, order):
return self._transmit(order)
[docs] def add_order_history(self, orders, notify=False):
return self.k.add_order_history(orders, notify)
[docs] def set_fund_history(self, fund):
return self.k.set_fund_history(fund)