def handle_opened_position(self, position: Position, order: Order, account: Account, bars: List[Bar]): position.status = PositionStatus.OPEN position.filled_entry = order.executed_price if order is not None else None position.entry_tstamp = order.execution_tstamp if order is not None and order.execution_tstamp > 0 else bars[ 0].tstamp self.position_got_opened(position, bars, account)
def init(self, bars: List[Bar], account: Account, symbol: Symbol, unique_id: str = ""): '''init open position etc.''' self.symbol = symbol self.unique_id = unique_id if unique_id is not None: base = 'openPositions/' try: os.makedirs(base) except Exception: pass try: with open(base + self._get_pos_file(), 'r') as file: data = json.load(file) self.last_time = data["last_time"] for pos_json in data["positions"]: pos: Position = Position.from_json(pos_json) self.open_positions[pos.id] = pos self.logger.info("done loading " + str( len(self.open_positions)) + " positions from " + self._get_pos_file() + " last time " + str( self.last_time)) except Exception as e: self.logger.warn("Error loading open positions: " + str(e)) self.open_positions = {} # init positions from existing orders self.sync_positions_with_open_orders(bars, account)
def read_open_positions(self,bars: List[Bar]): if self.unique_id is not None: base = 'openPositions/' try: os.makedirs(base) except Exception: pass try: with open(base + self._get_pos_file(), 'r') as file: data = json.load(file) self.last_time = data["last_time"] if "max_equity" in data.keys(): self.max_equity= data["max_equity"] self.time_of_max_equity= data["time_of_max_equity"] for pos_json in data["positions"]: pos: Position = Position.from_json(pos_json) self.open_positions[pos.id] = pos if "moduleData" in data.keys(): if str(bars[0].tstamp) in data["moduleData"].keys(): moduleData = data['moduleData'][str(bars[0].tstamp)] ExitModule.set_data_from_json(bars[0], moduleData) if str(bars[1].tstamp) in data["moduleData"].keys(): moduleData = data['moduleData'][str(bars[1].tstamp)] ExitModule.set_data_from_json(bars[1], moduleData) self.logger.info("done loading " + str( len(self.open_positions)) + " positions from " + self._get_pos_file() + " last time " + str( self.last_time)) except Exception as e: self.logger.warn("Error loading open positions: " + str(e)) self.open_positions = {}
def position_closed(self, position: Position, account: Account): if position.exit_tstamp == 0: position.exit_tstamp = time.time() position.exit_equity = account.equity self.position_history.append(position) del self.open_positions[position.id] # cancel other open orders of this position (sl/tp etc) self.logger.info("canceling remaining orders for position: " + position.id) self.cancel_all_orders_for_position(position.id, account) if self.unique_id is None: return base = 'positionHistory/' filename = base + self._get_pos_file() size = 0 try: os.makedirs(base) except Exception: pass try: size = os.path.getsize(filename) except Exception: pass with open(filename, 'a') as file: writer = csv.writer(file) if size == 0: csv_columns = ['signalTStamp', 'size', 'wantedEntry', 'initialStop', 'openTime', 'openPrice', 'closeTime', 'closePrice', 'equityOnExit'] writer.writerow(csv_columns) writer.writerow([ datetime.fromtimestamp(position.signal_tstamp).isoformat(), position.amount, position.wanted_entry, position.initial_stop, datetime.fromtimestamp(position.entry_tstamp).isoformat(), position.filled_entry, datetime.fromtimestamp(position.exit_tstamp).isoformat(), position.filled_exit, position.exit_equity ])
def fill_openpositions(self, data, bars: List[Bar]): self.last_time = data["last_time"] if "last_tick_tstamp" in data: self.last_tick_time = datetime.fromtimestamp(data["last_tick_tstamp"]) if "max_equity" in data.keys(): self.max_equity = data["max_equity"] self.time_of_max_equity = data["time_of_max_equity"] for pos_json in data["positions"]: pos: Position = Position.from_json(pos_json) self.open_positions[pos.id] = pos if "moduleData" in data.keys(): for idx in range(5): if str(bars[idx].tstamp) in data["moduleData"].keys(): moduleData = data['moduleData'][str(bars[idx].tstamp)] ExitModule.set_data_from_json(bars[idx], moduleData)
def sync_positions_with_open_orders(self, bars: List[Bar], account: Account): open_pos = 0 for pos in self.open_positions.values(): pos.connectedOrders= [] # will be filled now if pos.status == PositionStatus.OPEN: open_pos += pos.amount if not self.got_data_for_position_sync(bars): self.logger.warn("got no initial data, can't sync positions") return remaining_pos_ids = [] remaining_pos_ids += self.open_positions.keys() remaining_orders = [] remaining_orders += account.open_orders # first check if there even is a diparity (positions without stops, or orders without position) for order in account.open_orders: if not order.active: remaining_orders.remove(order) continue # got cancelled during run orderType = self.order_type_from_order_id(order.id) if orderType is None: remaining_orders.remove(order) continue # none of ours posId = self.position_id_from_order_id(order.id) if posId in self.open_positions.keys(): pos = self.open_positions[posId] pos.connectedOrders.append(order) remaining_orders.remove(order) if posId in remaining_pos_ids: if (orderType == OrderType.SL and pos.status == PositionStatus.OPEN) \ or (orderType == OrderType.ENTRY and pos.status == PositionStatus.PENDING): # only remove from remaining if its open with SL or pending with entry. every position needs # a stoploss! remaining_pos_ids.remove(posId) for pos in self.open_positions.values(): self.check_open_orders_in_position(pos) if len(remaining_orders) == 0 and len(remaining_pos_ids) == 0 and abs( open_pos - account.open_position.quantity) < 0.1: return self.logger.info("Has to start order/pos sync with bot vs acc: %.3f vs. %.3f and %i vs %i, remaining: %i, %i" % ( open_pos, account.open_position.quantity, len(self.open_positions), len(account.open_orders), len(remaining_orders), len(remaining_pos_ids))) remainingPosition = account.open_position.quantity for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN: remainingPosition -= pos.amount waiting_tps = [] # now remaining orders and remaining positions contain the not matched ones for order in remaining_orders: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if not order.active: # already canceled or executed continue if orderType == OrderType.ENTRY: # add position for unkown order stop = self.get_stop_for_unmatched_amount(order.amount, bars) if stop is not None: newPos = Position(id=posId, entry=order.limit_price if order.limit_price is not None else order.stop_price, amount=order.amount, stop=stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.PENDING if not order.stop_triggered else PositionStatus.TRIGGERED self.open_positions[posId] = newPos self.logger.warn("found unknown entry %s %.1f @ %.1f, added position" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: self.logger.warn( "found unknown entry %s %.1f @ %.1f, but don't know what stop to use -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) elif orderType == OrderType.SL and remainingPosition * order.amount < 0 and abs(remainingPosition) > abs( order.amount): # only assume open position for the waiting SL with the remainingPosition also indicates it, # otherwise it might be a pending cancel (from executed TP) or already executed newPos = Position(id=posId, entry=None, amount=-order.amount, stop=order.stop_price, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN remainingPosition -= newPos.amount self.open_positions[posId] = newPos self.logger.warn("found unknown exit %s %.1f @ %.1f, opened position for it" % ( order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: waiting_tps.append(order) # cancel orphaned TPs for order in waiting_tps: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if posId not in self.open_positions.keys(): # still not in (might have been added in previous for) self.logger.warn( "didn't find matching position for order %s %.1f @ %.1f -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) self.logger.info("found " + str(len(self.open_positions)) + " existing positions on sync") # positions with no exit in the market for posId in remaining_pos_ids: pos = self.open_positions[posId] if pos.status == PositionStatus.PENDING or pos.status == PositionStatus.TRIGGERED: # should have the opening order in the system, but doesn't # not sure why: in doubt: not create wrong orders if remainingPosition * pos.amount > 0 and abs(remainingPosition) >= abs(pos.amount): # assume position was opened without us realizing (during downtime) self.logger.warn( "pending position with no entry order but open position looks like it was opened: %s" % (posId)) self.handle_opened_position(position=pos, order=None, bars=bars, account=account) remainingPosition -= pos.amount else: self.logger.warn( "pending position with no entry order and no sign of opening -> close missed: %s" % (posId)) pos.status = PositionStatus.MISSED self.position_closed(pos, account) elif pos.status == PositionStatus.OPEN: if remainingPosition == 0 and pos.initial_stop is not None: # for some reason everything matches but we are missing the stop in the market self.logger.warn( "found position with no stop in market. added stop for it: %s with %.1f contracts" % ( posId, pos.amount)) self.order_interface.send_order( Order(orderId=self.generate_order_id(posId, OrderType.SL), amount=-pos.amount, stop=pos.initial_stop)) else: self.logger.warn( "found position with no stop in market. %s with %.1f contracts. but remaining Position doesn't match so assume it was already closed." % ( posId, pos.amount)) self.position_closed(pos, account) remainingPosition += pos.amount else: self.logger.warn( "pending position with noconnection order not pending or open? closed: %s" % (posId)) self.position_closed(pos, account) # now there should not be any mismatch between positions and orders. if remainingPosition != 0: unmatched_stop = self.get_stop_for_unmatched_amount(remainingPosition, bars) signalId = str(bars[1].tstamp) + '+' + str(randint(0, 99)) if unmatched_stop is not None: posId = self.full_pos_id(signalId, PositionDirection.LONG if remainingPosition > 0 else PositionDirection.SHORT) newPos = Position(id=posId, entry=None, amount=remainingPosition, stop=unmatched_stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN self.open_positions[posId] = newPos # add stop self.logger.info( "couldn't account for " + str(newPos.amount) + " open contracts. Adding position with stop for it") self.order_interface.send_order(Order(orderId=self.generate_order_id(posId, OrderType.SL), stop=newPos.initial_stop, amount=-newPos.amount)) elif account.open_position.quantity * remainingPosition > 0: self.logger.info( "couldn't account for " + str(remainingPosition) + " open contracts. Market close") self.order_interface.send_order(Order(orderId=signalId + "_marketClose", amount=-remainingPosition)) else: self.logger.info( "couldn't account for " + str( remainingPosition) + " open contracts. But close would increase exposure-> ignored")
def __open_position(self, direction, bars, swing, open_positions): directionFactor = 1 oppDirection = PositionDirection.SHORT extreme = bars[1].low capFunc = min if direction == PositionDirection.SHORT: directionFactor = -1 oppDirection = PositionDirection.LONG extreme = bars[1].high capFunc = max oppDirectionFactor = directionFactor * -1 expectedEntrySplipagePerc = 0.0015 expectedExitSlipagePerc = 0.0015 data: Data = self.channel.get_data(bars[1]) if self.close_on_opposite: for pos in open_positions.values(): if pos.status == PositionStatus.OPEN and \ TradingBot.split_pos_Id(pos.id)[1] == oppDirection: # execution will trigger close and cancel of other orders self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( pos.id, OrderType.SL), amount=-pos.amount, stop=None, limit=None)) if self.init_stop_type == 1: stop = extreme elif self.init_stop_type == 2: stop = extreme + (extreme - bars[1].close) * 0.5 else: stop = capFunc(swing, (extreme + bars[1].close) / 2) stop = stop + oppDirectionFactor # buffer entry = bars[0].open signalId = self.get_signal_id(bars) #case long: entry * (1 + oppDirectionFactor*self.min_stop_diff_perc / 100) >= stop if 0 <= directionFactor*(entry * (1 + oppDirectionFactor*self.min_stop_diff_perc / 100) - stop) \ or not self.ignore_on_tight_stop: stop = capFunc( stop, entry * (1 + oppDirectionFactor * self.min_stop_diff_perc / 100)) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 + oppDirectionFactor * expectedExitSlipagePerc), entry=entry * (1 + directionFactor * expectedEntrySplipagePerc), atr=data.atr) posId = TradingBot.full_pos_id(signalId, direction) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) if self.tp_fac > 0: ref = entry - stop if self.tp_use_atr: ref = math.copysign(data.atr, entry - stop) tp = entry + ref * self.tp_fac self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.TP), amount=-amount, stop=None, limit=tp)) pos.status = PositionStatus.OPEN
def open_orders(self, is_new_bar, directionFilter, bars, account, open_positions): if (not is_new_bar) or len(bars) < self.min_bars_needed(): return # only open orders on beginning of bar if not self.entries_allowed(bars): self.logger.info("no entries allowed") return # check for signal. we are at the open of the new bar. so bars[0] contains of only 1 tick. # we look at data bars[1] and bars[2] prevFast = self.fastMA.get_data(bars[2]) currentFast = self.fastMA.get_data(bars[1]) prevSlow = self.slowMA.get_data(bars[2]) currentSlow = self.slowMA.get_data(bars[1]) swingData: Data = self.swings.get_data(bars[1]) # for stops # include the expected slipage in the risk calculation expectedEntrySplipagePerc = 0.0015 expectedExitSlipagePerc = 0.0015 signalId = "MACross+" + str(bars[0].tstamp) if prevFast <= prevSlow and currentFast > currentSlow: # cross up -> long entry entry = bars[0].open # current price stop = swingData.swingLow if stop is None: stop = lowest(bars, self.swings.before + self.swings.after, 1, BarSeries.LOW) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 - expectedExitSlipagePerc), entry=entry * (1 + expectedEntrySplipagePerc)) # open the position and save it posId = TradingBot.full_pos_id(signalId, PositionDirection.LONG) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos # send entry as market, immediatly send SL too self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) pos.status = PositionStatus.OPEN elif prevFast >= prevSlow and currentFast < currentSlow: # cross down -> short entry entry = bars[0].open # current price stop = swingData.swingHigh if stop is None: stop = highest(bars, self.swings.before + self.swings.after, 1, BarSeries.HIGH) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 + expectedExitSlipagePerc), entry=entry * (1 - expectedEntrySplipagePerc)) # open the position and save it posId = TradingBot.full_pos_id(signalId, PositionDirection.SHORT) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos # send entry as market, immediatly send SL too self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) pos.status = PositionStatus.OPEN
def open_orders(self, bars: List[Bar], account: Account): if (not self.is_new_bar) or len(bars) < 5: return # only open orders on beginning of bar last_data: Data = self.channel.get_data(bars[2]) data: Data = self.channel.get_data(bars[1]) if data is not None: self.logger.info( "---- analyzing: %s atr: %.1f buffer: %.1f swings: %s/%s trails: %.1f/%.1f resets:%i/%i" % (str(datetime.fromtimestamp( bars[0].tstamp)), data.atr, data.buffer, ("%.1f" % data.longSwing) if data.longSwing is not None else "-", ("%.1f" % data.shortSwing) if data.shortSwing is not None else "-", data.longTrail, data.shortTrail, data.sinceLongReset, data.sinceShortReset)) if data is not None and last_data is not None and \ data.shortSwing is not None and data.longSwing is not None and \ (not self.delayed_entry or (last_data.shortSwing is not None and last_data.longSwing is not None)): swing_range = data.longSwing - data.shortSwing atr = clean_range(bars, offset=0, length=self.channel.max_look_back * 2) if atr * self.min_channel_size_factor < swing_range < atr * self.max_channel_size_factor: risk = self.risk_factor stopLong = int(max(data.shortSwing, data.longTrail)) stopShort = int(min(data.longSwing, data.shortTrail)) longEntry = int(max(data.longSwing, bars[0].high)) shortEntry = int(min(data.shortSwing, bars[0].low)) expectedEntrySplipagePerc = 0.0015 if self.stop_entry else 0 expectedExitSlipagePerc = 0.0015 # first check if we should update an existing one longAmount = self.calc_pos_size( risk=risk, exitPrice=stopLong * (1 - expectedExitSlipagePerc), entry=longEntry * (1 + expectedEntrySplipagePerc), data=data) shortAmount = self.calc_pos_size( risk=risk, exitPrice=stopShort * (1 + expectedExitSlipagePerc), entry=shortEntry * (1 - expectedEntrySplipagePerc), data=data) if longEntry < stopLong or shortEntry > stopShort: self.logger.warn("can't put initial stop above entry") foundLong = False foundShort = False for position in self.open_positions.values(): if position.status == PositionStatus.PENDING: if position.amount > 0: foundLong = True entry = longEntry stop = stopLong entryFac = (1 + expectedEntrySplipagePerc) exitFac = (1 - expectedExitSlipagePerc) else: foundShort = True entry = shortEntry stop = stopShort entryFac = (1 - expectedEntrySplipagePerc) exitFac = (1 + expectedExitSlipagePerc) for order in account.open_orders: if self.position_id_from_order_id( order.id) == position.id: newEntry = int(position.wanted_entry * (1 - self.entry_tightening) + entry * self.entry_tightening) newStop = int(position.initial_stop * (1 - self.entry_tightening) + stop * self.entry_tightening) amount = self.calc_pos_size( risk=risk, exitPrice=newStop * exitFac, entry=newEntry * entryFac, data=data) if amount * order.amount < 0: self.logger.warn( "updating order switching direction") changed = False changed = changed or order.stop_price != newEntry order.stop_price = newEntry if not self.stop_entry: changed = changed or order.limit_price != newEntry - math.copysign( 1, amount) order.limit_price = newEntry - math.copysign( 1, amount) changed = changed or order.amount != amount order.amount = amount if changed: self.order_interface.update_order(order) else: self.logger.info( "order didn't change: %s" % order.print_info()) position.initial_stop = newStop position.amount = amount position.wanted_entry = newEntry break # if len(self.open_positions) > 0: # return signalId = str(bars[0].tstamp) if not foundLong and self.directionFilter >= 0: posId = self.full_pos_id(signalId, PositionDirection.LONG) self.order_interface.send_order( Order(orderId=self.generate_order_id( posId, OrderType.ENTRY), amount=longAmount, stop=longEntry, limit=longEntry - 1 if not self.stop_entry else None)) self.open_positions[posId] = Position( id=posId, entry=longEntry, amount=longAmount, stop=stopLong, tstamp=bars[0].tstamp) if not foundShort and self.directionFilter <= 0: posId = self.full_pos_id(signalId, PositionDirection.SHORT) self.order_interface.send_order( Order(orderId=self.generate_order_id( posId, OrderType.ENTRY), amount=shortAmount, stop=shortEntry, limit=shortEntry + 1 if not self.stop_entry else None)) self.open_positions[posId] = Position( id=posId, entry=shortEntry, amount=shortAmount, stop=stopShort, tstamp=bars[0].tstamp)
def open_orders(self, is_new_bar, directionFilter, bars, account, open_positions, all_open_pos: dict): if (not is_new_bar) or len(bars) < self.min_bars_needed(): return # only open orders on beginning of bar if not self.entries_allowed(bars): self.logger.info("no entries allowed") return # include the expected slipage in the risk calculation expectedExitSlipagePerc = 0.0015 data = self.mean.get_data(bars[1]) # long: longEntry = self.symbol.normalizePrice( data.mean - data.std * self.entry_factor, False) longStop = self.symbol.normalizePrice( data.mean - data.std * self.sl_factor, True) longAmount = self.calc_pos_size(risk=self.risk_factor, exitPrice=longStop * (1 - expectedExitSlipagePerc), entry=longEntry) # short: shortEntry = self.symbol.normalizePrice( data.mean + data.std * self.entry_factor, True) shortStop = self.symbol.normalizePrice( data.mean + data.std * self.sl_factor, False) shortAmount = self.calc_pos_size(risk=self.risk_factor, exitPrice=shortStop * (1 + expectedExitSlipagePerc), entry=shortEntry) gotLong = False gotShort = False for pos in open_positions.values(): if pos.amount > 0: gotLong = True if pos.status == PositionStatus.PENDING: pos.amount = longAmount pos.initialStop = longStop pos.wantedEntry = longEntry for order in pos.connectedOrders: if order.limit_price != longEntry or order.amount != longAmount: order.limit_price = longEntry order.amount = longAmount self.order_interface.update_order(order) else: gotShort = True if pos.status == PositionStatus.PENDING: pos.amount = shortAmount pos.initialStop = shortStop pos.wantedEntry = shortEntry for order in pos.connectedOrders: if order.limit_price != shortEntry or order.amount != shortAmount: order.limit_price = shortEntry order.amount = shortAmount self.order_interface.update_order(order) if not gotLong and bars[0].close > longEntry: posId = TradingBot.full_pos_id( self.get_signal_id(bars, self.myId()), PositionDirection.LONG) open_positions[posId] = Position(id=posId, entry=longEntry, amount=longAmount, stop=longStop, tstamp=bars[0].tstamp) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=longAmount, limit=longEntry)) if not gotShort and bars[0].close < shortEntry: posId = TradingBot.full_pos_id( self.get_signal_id(bars, self.myId()), PositionDirection.SHORT) open_positions[posId] = Position(id=posId, entry=shortEntry, amount=shortAmount, stop=shortStop, tstamp=bars[0].tstamp) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=shortAmount, limit=shortEntry))
def sync_positions_with_open_orders(self, bars: List[Bar], account: Account): open_pos = 0 for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN: open_pos += pos.current_open_amount if not self.got_data_for_position_sync(bars): self.logger.warn("got no initial data, can't sync positions") self.unaccounted_position_cool_off = 0 return remaining_pos_ids = [] remaining_pos_ids += self.open_positions.keys() remaining_orders = [] remaining_orders += account.open_orders # first check if there even is a diparity (positions without stops, or orders without position) for order in account.open_orders: if not order.active: remaining_orders.remove(order) continue # got cancelled during run [posId, orderType] = self.position_id_and_type_from_order_id(order.id) if orderType is None: remaining_orders.remove(order) continue # none of ours if posId in self.open_positions.keys(): pos = self.open_positions[posId] remaining_orders.remove(order) if posId in remaining_pos_ids: if (orderType == OrderType.SL and pos.status == PositionStatus.OPEN) \ or (orderType == OrderType.ENTRY and pos.status in [PositionStatus.PENDING, PositionStatus.TRIGGERED]): # only remove from remaining if its open with SL or pending with entry. every position needs # a stoploss! remaining_pos_ids.remove(posId) for pos in self.open_positions.values(): self.check_open_orders_in_position(pos) if len(remaining_orders) == 0 and len(remaining_pos_ids) == 0 and abs( open_pos - account.open_position.quantity) < self.symbol.lotSize / 10: self.unaccounted_position_cool_off = 0 return self.logger.info("Has to start order/pos sync with bot vs acc: %.3f vs. %.3f and %i vs %i, remaining: %i, %i" % ( open_pos, account.open_position.quantity, len(self.open_positions), len(account.open_orders), len(remaining_orders), len(remaining_pos_ids))) remainingPosition = account.open_position.quantity for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN: remainingPosition -= pos.current_open_amount waiting_tps = [] # now remaining orders and remaining positions contain the not matched ones for order in remaining_orders: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if not order.active: # already canceled or executed continue if orderType == OrderType.ENTRY: # add position for unkown order stop = self.get_stop_for_unmatched_amount(order.amount, bars) if stop is not None: newPos = Position(id=posId, entry=order.limit_price if order.limit_price is not None else order.stop_price, amount=order.amount, stop=stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.PENDING if not order.stop_triggered else PositionStatus.TRIGGERED self.open_positions[posId] = newPos self.logger.warn("found unknown entry %s %.1f @ %.1f, added position" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: self.logger.warn( "found unknown entry %s %.1f @ %.1f, but don't know what stop to use -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) elif orderType == OrderType.SL and remainingPosition * order.amount < 0 and abs( round(remainingPosition, self.symbol.quantityPrecision)) > abs( order.amount): # only assume open position for the waiting SL with the remainingPosition also indicates it, # otherwise it might be a pending cancel (from executed TP) or already executed newPos = Position(id=posId, entry=None, amount=-order.amount, stop=order.stop_price, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN remainingPosition -= newPos.amount self.open_positions[posId] = newPos self.logger.warn("found unknown exit %s %.1f @ %.1f, opened position for it" % ( order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: waiting_tps.append(order) # cancel orphaned TPs for order in waiting_tps: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if posId not in self.open_positions.keys(): # still not in (might have been added in previous for) self.logger.warn( "didn't find matching position for order %s %.1f @ %.1f -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) self.logger.info("found " + str(len(self.open_positions)) + " existing positions on sync") # positions with no exit in the market for posId in remaining_pos_ids: pos = self.open_positions[posId] if pos.status == PositionStatus.PENDING or pos.status == PositionStatus.TRIGGERED: # should have the opening order in the system, but doesn't # not sure why: in doubt: not create wrong orders if remainingPosition * pos.amount > 0 and abs( round(remainingPosition, self.symbol.quantityPrecision)) >= abs(pos.amount): # assume position was opened without us realizing (during downtime) self.logger.warn( "pending position with no entry order but open position looks like it was opened: %s" % (posId)) pos.last_filled_entry = pos.wanted_entry pos.entry_tstamp = time.time() pos.max_filled_amount += pos.amount pos.current_open_amount = pos.amount self.handle_opened_or_changed_position(position=pos, bars=bars, account=account) remainingPosition -= pos.amount else: self.logger.warn( "pending position with no entry order and no sign of opening -> close missed: %s" % (posId)) pos.status = PositionStatus.MISSED self.position_closed(pos, account) elif pos.status == PositionStatus.OPEN: if pos.changed: self.logger.info(f"pos has no exit, but is marked changed, so its probably just a race {pos}") continue if pos.initial_stop is not None: # for some reason we are missing the stop in the market self.logger.warn( "found position with no stop in market. added stop for it: %s with %.1f contracts" % ( posId, pos.current_open_amount)) self.order_interface.send_order( Order(orderId=self.generate_order_id(posId, OrderType.SL), amount=-pos.current_open_amount, stop=pos.initial_stop)) else: self.logger.warn( "found position with no stop in market. %s with %.1f contracts. but no initial stop on position had to close" % ( posId, pos.current_open_amount)) self.order_interface.send_order( Order(orderId=self.generate_order_id(posId, OrderType.SL), amount=-pos.current_open_amount)) else: self.logger.warn( "pending position with noconnected order not pending or open? closed: %s" % (posId)) self.position_closed(pos, account) remainingPosition = round(remainingPosition, self.symbol.quantityPrecision) # now there should not be any mismatch between positions and orders. if remainingPosition != 0: if self.unaccounted_position_cool_off > 1: unmatched_stop = self.get_stop_for_unmatched_amount(remainingPosition, bars) signalId = str(bars[1].tstamp) + '+' + str(randint(0, 99)) if unmatched_stop is not None: posId = self.full_pos_id(signalId, PositionDirection.LONG if remainingPosition > 0 else PositionDirection.SHORT) newPos = Position(id=posId, entry=None, amount=remainingPosition, stop=unmatched_stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN self.open_positions[posId] = newPos # add stop self.logger.info( "couldn't account for " + str( newPos.current_open_amount) + " open contracts. Adding position with stop for it") self.order_interface.send_order(Order(orderId=self.generate_order_id(posId, OrderType.SL), stop=newPos.initial_stop, amount=-newPos.current_open_amount)) elif account.open_position.quantity * remainingPosition > 0: self.logger.info( "couldn't account for " + str(remainingPosition) + " open contracts. Market close") self.order_interface.send_order(Order(orderId=signalId + "_marketClose", amount=-remainingPosition)) else: self.logger.info( "couldn't account for " + str( remainingPosition) + " open contracts. But close would increase exposure-> mark positions as closed") for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN and abs( remainingPosition + pos.current_open_amount) < self.symbol.lotSize: self.logger.info(f"marked position {pos.id} with exact size as closed ") self.position_closed(pos, account) remainingPosition += pos.current_open_amount break if abs(remainingPosition) >= self.symbol.lotSize: # close orders until size closed # TODO: sort by size, close until position flips side pos_to_close = [] for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN and pos.current_open_amount * remainingPosition < 0: # rough sorting to have the smallest first if len(pos_to_close) > 0 and abs(pos.current_open_amount) <= abs( pos_to_close[0].current_open_amount): pos_to_close.insert(0, pos) else: pos_to_close.append(pos) direction = 1 if remainingPosition > 0 else -1 for pos in pos_to_close: if direction * remainingPosition <= 0 or abs(remainingPosition) < self.symbol.lotSize: break self.logger.info(f"marked position {pos.id} as closed ") remainingPosition += pos.current_open_amount self.position_closed(pos, account) else: self.logger.info( "couldn't account for " + str( remainingPosition) + " open contracts. cooling off, hoping it's a glitch") self.unaccounted_position_cool_off += 1 else: self.unaccounted_position_cool_off = 0
def handle_opened_or_changed_position(self, position: Position, account: Account, bars: List[Bar]): position.status = PositionStatus.OPEN self.position_got_opened_or_changed(position, bars, account)
def open_orders(self, is_new_bar, directionFilter, bars, account, open_positions): if (not is_new_bar) or len(bars) < 5: return # only open orders on beginning of bar if not self.entries_allowed(bars): self.logger.info(" no entries allowed") return atr = clean_range(bars, offset=0, length=self.channel.max_look_back * 2) risk = self.risk_factor # test for SFP: # High > HH der letzten X # Close < HH der vorigen X # ? min Wick size? # initial SL data: Data = self.channel.get_data(bars[1]) maxLength = min(len(bars), self.range_length) minRejLength = min(len(bars),self.min_rej_length) highSupreme = 0 hhBack = 0 hh = bars[2].high swingHigh = 0 gotHighSwing = False for idx in range(2, maxLength): if bars[idx].high < bars[1].high: highSupreme = idx - 1 if hh < bars[idx].high: hh = bars[idx].high hhBack = idx elif self.min_swing_length < hhBack <= idx - self.min_swing_length: gotHighSwing = True swingHigh = hh # confirmed else: break lowSupreme = 0 llBack = 0 ll = bars[2].low swingLow = 0 gotLowSwing = False for idx in range(2, maxLength): if bars[idx].low > bars[1].low: lowSupreme = idx - 1 if ll > bars[idx].low: ll = bars[idx].low llBack = idx elif self.min_swing_length < llBack <= idx - self.min_swing_length: gotLowSwing = True swingLow = ll # confirmed else: break rangeMedian = (bars[maxLength - 1].high + bars[maxLength - 1].low) / 2 alpha = 2 / (maxLength + 1) for idx in range(maxLength - 2, 0, -1): rangeMedian = rangeMedian * alpha + (bars[idx].high + bars[idx].low) / 2 * (1 - alpha) expectedEntrySplipagePerc = 0.0015 expectedExitSlipagePerc = 0.0015 signalId = "sfp+" + str(bars[0].tstamp) # SHORT longSFP = self.entries != 1 and gotHighSwing and bars[1].close + data.buffer < swingHigh longRej = self.entries != 2 and bars[1].high > hh > bars[1].close + data.buffer and \ highSupreme > minRejLength and bars[1].high - bars[1].close > (bars[1].high - bars[1].low) / 2 # LONG shortSFP = self.entries != 1 and gotLowSwing and bars[1].close - data.buffer > swingLow shortRej = self.entries != 2 and bars[1].low < ll < bars[1].close - data.buffer and lowSupreme > minRejLength \ and bars[1].close - bars[1].low > (bars[1].high - bars[1].low) / 2 self.logger.info("---- analyzing: %s: %.1f %.1f %.0f | %s %.0f %i or %i %.0f %.0f | %s %.0f %i or %i %.0f %.0f " % (str(datetime.fromtimestamp(bars[0].tstamp)), data.buffer, atr, rangeMedian, gotHighSwing, swingHigh, hhBack, highSupreme, hh ,bars[1].high - bars[1].close, gotLowSwing, swingLow, llBack, lowSupreme, ll ,bars[1].close - bars[1].low )) if (longSFP or longRej) and (bars[1].high - bars[1].close) > atr * self.min_wick_fac \ and directionFilter <= 0 and bars[1].high > rangeMedian + atr * self.range_filter_fac: # close existing short pos if self.close_on_opposite: for pos in open_positions.values(): if pos.status == PositionStatus.OPEN and TradingBot.split_pos_Id(pos.id)[1] == PositionDirection.LONG: # execution will trigger close and cancel of other orders self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id(pos.id, OrderType.SL), amount=-pos.amount, stop=None, limit=None)) if self.init_stop_type == 1: stop = bars[1].high elif self.init_stop_type == 2: stop = bars[1].high + (bars[0].high - bars[0].close) * 0.5 else: stop = max(swingHigh, (bars[1].high + bars[1].close) / 2) stop = stop + 1 # buffer entry = bars[0].open amount = self.calc_pos_size(risk=risk, exitPrice=stop * (1 + expectedExitSlipagePerc), entry=entry * (1 - expectedEntrySplipagePerc), data=data) posId = TradingBot.full_pos_id(signalId, PositionDirection.SHORT) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId]= pos self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) if self.tp_fac > 0: tp = entry - (stop - entry) * self.tp_fac self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.TP), amount=-amount, stop=None, limit=tp)) pos.status= PositionStatus.OPEN if (shortSFP or shortRej) and (bars[1].close - bars[1].low) > atr * self.min_wick_fac \ and directionFilter >= 0 and bars[1].low < rangeMedian - self.range_filter_fac: # close existing short pos if self.close_on_opposite: for pos in open_positions.values(): if pos.status == PositionStatus.OPEN and TradingBot.split_pos_Id(pos.id)[1] == PositionDirection.SHORT: # execution will trigger close and cancel of other orders self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id(pos.id, OrderType.SL), amount=-pos.amount, stop=None, limit=None)) if self.init_stop_type == 1: stop = bars[1].low elif self.init_stop_type == 2: stop = bars[1].low + (bars[0].low - bars[0].close) * 0.5 else: stop = min(swingLow, (bars[1].low + bars[1].close) / 2) stop = stop - 1 # buffer entry = bars[0].open amount = self.calc_pos_size(risk=risk, exitPrice=stop * (1 - expectedExitSlipagePerc), entry=entry * (1 + expectedEntrySplipagePerc), data=data) posId = TradingBot.full_pos_id(signalId, PositionDirection.LONG) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) pos.status= PositionStatus.TRIGGERED open_positions[posId]= pos self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) if self.tp_fac > 0: tp = entry + (entry - stop) * self.tp_fac self.order_interface.send_order(Order(orderId=TradingBot.generate_order_id(posId, OrderType.TP), amount=-amount, stop=None, limit=tp))
def open_orders(self, is_new_bar, directionFilter, bars, account, open_positions, all_open_pos: dict): if (not is_new_bar) or len(bars) < 5: return # only open orders on beginning of bar entriesAllowed = self.entries_allowed(bars) if not entriesAllowed: self.logger.info("new entries not allowed by filter") last_data: Data = self.channel.get_data(bars[2]) data: Data = self.channel.get_data(bars[1]) if data is None: return self.logger.info( "---- analyzing: %s atr: %.1f buffer: %.1f swings: %s/%s trails: %.1f/%.1f resets:%i/%i" % (str(datetime.fromtimestamp( bars[0].tstamp)), data.atr, data.buffer, ("%.1f" % data.longSwing) if data.longSwing is not None else "-", ("%.1f" % data.shortSwing) if data.shortSwing is not None else "-", data.longTrail, data.shortTrail, data.sinceLongReset, data.sinceShortReset)) if last_data is not None and \ data.shortSwing is not None and data.longSwing is not None and \ (not self.delayed_entry or (last_data.shortSwing is not None and last_data.longSwing is not None)): swing_range = data.longSwing - data.shortSwing atr = clean_range(bars, offset=0, length=self.channel.max_look_back * 2) if atr * self.min_channel_size_factor < swing_range < atr * self.max_channel_size_factor: risk = self.risk_factor #longEntry = self.symbol.normalizePrice(data.shortSwing, roundUp=False) #shortEntry = self.symbol.normalizePrice(data.longSwing, roundUp=True) longEntry = self.symbol.normalizePrice(data.longTrail, roundUp=False) shortEntry = self.symbol.normalizePrice(data.shortTrail, roundUp=True) stopLong = self.symbol.normalizePrice( longEntry - self.sl_fac * (shortEntry - data.longTrail), roundUp=False) stopShort = self.symbol.normalizePrice( shortEntry + self.sl_fac * (data.shortTrail - longEntry), roundUp=False) stopLong = min(stopLong, longEntry - self.min_stop_diff_atr * atr) stopShort = max(stopShort, shortEntry + self.min_stop_diff_atr * atr) marketTrend = self.markettrend.get_market_trend() expectedEntrySlippagePer = 0.0015 if self.limit_entry_offset_perc is None else 0 expectedExitSlippagePer = 0.0015 # first check if we should update an existing one longAmount = self.calc_pos_size( risk=risk, exitPrice=stopLong * (1 - expectedExitSlippagePer), entry=longEntry * (1 + expectedEntrySlippagePer), atr=data.atr) shortAmount = self.calc_pos_size( risk=risk, exitPrice=stopShort * (1 + expectedExitSlippagePer), entry=shortEntry * (1 - expectedEntrySlippagePer), atr=data.atr) if longEntry < stopLong or shortEntry > stopShort: self.logger.warn("can't put initial stop above entry") foundLong = False foundShort = False for position in open_positions.values(): if position.status == PositionStatus.PENDING: if position.amount > 0: foundLong = True entry = longEntry stop = stopLong entryFac = (1 + expectedEntrySlippagePer) exitFac = (1 - expectedExitSlippagePer) else: foundShort = True entry = shortEntry stop = stopShort entryFac = (1 - expectedEntrySlippagePer) exitFac = (1 + expectedExitSlippagePer) entryBuffer = entry * self.limit_entry_offset_perc * 0.01 if self.limit_entry_offset_perc is not None else None for order in account.open_orders: if TradingBot.position_id_from_order_id( order.id) == position.id: newEntry = position.wanted_entry * ( 1 - self.entry_tightening ) + entry * self.entry_tightening newEntry = self.symbol.normalizePrice( newEntry, roundUp=order.amount > 0) newStop = position.initial_stop * ( 1 - self.entry_tightening ) + stop * self.entry_tightening newStop = self.symbol.normalizePrice( newStop, roundUp=order.amount < 0) amount = self.calc_pos_size( risk=risk, exitPrice=newStop * exitFac, entry=newEntry * entryFac, atr=data.atr) if amount * order.amount < 0: self.logger.warn( "updating order switching direction") changed = False changed = changed or order.stop_price != newEntry order.stop_price = newEntry if self.limit_entry_offset_perc is not None: newLimit = newEntry - entryBuffer * math.copysign( 1, amount) changed = changed or order.limit_price != newLimit order.limit_price = newLimit changed = changed or order.amount != amount order.amount = amount if changed: self.order_interface.update_order(order) else: self.logger.info( "order didn't change: %s" % order.print_info()) position.initial_stop = newStop position.amount = amount position.wanted_entry = newEntry break # if len(self.open_positions) > 0: # return signalId = self.get_signal_id(bars) if not foundLong and directionFilter >= 0 and entriesAllowed and ( marketTrend == 0 or marketTrend == 1): posId = TradingBot.full_pos_id(signalId, PositionDirection.LONG) entryBuffer = longEntry * self.limit_entry_offset_perc * 0.01 if self.limit_entry_offset_perc is not None else None self.order_interface.send_order( Order( orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=longAmount, stop=longEntry, limit=longEntry - entryBuffer if entryBuffer is not None else None)) open_positions[posId] = Position(id=posId, entry=longEntry, amount=longAmount, stop=stopLong, tstamp=bars[0].tstamp) if not foundShort and directionFilter <= 0 and entriesAllowed and ( marketTrend == 0 or marketTrend == -1): posId = TradingBot.full_pos_id(signalId, PositionDirection.SHORT) entryBuffer = shortEntry * self.limit_entry_offset_perc * 0.01 if self.limit_entry_offset_perc is not None else None self.order_interface.send_order( Order( orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=shortAmount, stop=shortEntry, limit=shortEntry + entryBuffer if entryBuffer is not None else None)) open_positions[posId] = Position(id=posId, entry=shortEntry, amount=shortAmount, stop=stopShort, tstamp=bars[0].tstamp)