Продолжение. Первая часть см. здесь.
Действия по тику
Тик получаем из API, когда какая-то часть стакана обновляется ( лучший бид или аск, либо их объемы). Внутри кода взаимодействия с API (tws) есть подпрограмма, которая следит за приходом такого обновления и вызывает соответсвующую функцию.
Что делает такая функция? Если мы выставляем пассивный ордер ( помните, это состояние по умолчанию):
- ... и прошло более 5 минут - меняем ордер на агрессивный.
- если мы покупаем и текущий лучший бид уходит вверх от стартового состояния, меняем ордер на агрессивный
- если продаем, и текущий лучший аск уходит вниз - также меняем на агрессивный
-
если мы видим нежелательный дисбаланс ордеров ( например, если количество ордеров на продажу в 5 раз превышает количество ордеров на покупку, в случае, когда мы также продаем) - меняем на агрессивный
Если мы выставляем агрессивный ордер
- ... и более 10 минут проходит, отменяем ордер
- если покупаем и текущий лучший аск движется вверх от стартового состояния, обновляем цену нашего лимитного ордера на равную новому лучшему аску ( движемся за рынком)
- если продаем и текущий лучший бид движется вниз, обновляем цену нашего ордера на равную лучшему биду.
passivetimelimit=5*60 ## макс 5 мин totaltimelimit=10*60 ## макс другие 5 мин для агрессивного состояния maximbalance=5.0 ## величина дисбаланса ордеров def EasyAlgo_on_tick(dbtype, orderid, marketdata, tws, contract): """ Эта функция вызывается на каждый тик Args: dbtype, tws: обозначают базу данных и API orderid: идентификационный номер, ассоциированный с тиком marketdata: текущая маркет дата (стакан) contract: контракт, который мы торгуем """ ## Диагностический код log=logger() diag=diagnostic(dbtype, system="algo", system3=str(int(orderid))) s=state_from_sdict(orderid, diag, log) ## Достаем информацию о нашем ордере из базы данных am=algo_memory_table(dbtype) trade=am.read_value(orderid, "Trade") current_limit=am.read_value(orderid, "Limit") Started=am.read_value(orderid, "Started") Mode=am.read_value(orderid, "Mode") lastsideprice=am.read_value(orderid, "ValidSidePrice") lastoffsideprice=am.read_value(orderid, "ValidOffSidePrice") LastNotice=am.read_value(orderid, "LastNotice") ## если не находим ордер в базе данных if Mode is None or Started is None or current_limit is None or trade is None or LastNotice is None: log.critical("Can't get algo memory values for orderid %d CANCELLING" % orderid) FinishOrder(dbtype, orderid, marketdata, tws, contract) Started=float_as_date(Started) LastNotice=float_as_date(LastNotice) timenow=datetime.datetime.now() ## если покупка, получаем лучший аск (sideprice) и лучший бид (offsideprice) ## если продажа, получаем лучший бид (sideprice) и лучший аск (offsideprice) (sideprice, offsideprice)=get_price_sides(marketdata, trade) s.update(dict(limit_price=current_limit, offside_price=offsideprice, side_price=sideprice, Mode=Mode)) ## получаем время, сколько мы торгуем и время с последнего уведомления 'LastNotice' time_trading=(timenow - Started).total_seconds() time_since_last=(timenow - LastNotice).seconds ## если прошла минута if time_since_last>60: s.update(dict(message="One minute since last noticed now %s, total time %d seconds - waiting %d %s %s" % (str(timenow), time_trading, orderid, contract.code, contract.contractid))) am.update_value(orderid, "LastNotice", date_as_float(timenow)) ## если мы вышли за максимальное время - сбрасываем ордера if time_trading>totaltimelimit: s.update(dict(message="Out of time cancelling for %d %s %s" % (orderid, contract.code, contract.contractid))) FinishOrder(dbtype, orderid, marketdata, tws, contract) return -1 if not np.isnan(sideprice) and sideprice<>lastsideprice: am.update_value(orderid, "ValidSidePrice", sideprice) if not np.isnan(offsideprice) and offsideprice<>lastoffsideprice: am.update_value(orderid, "ValidOffSidePrice", offsideprice) am.close() if Mode=="Passive": ## время вышло для пассивного ордера (5 минут) if time_trading>passivetimelimit: s.update(dict(message="Out of time moving to aggressive for %d %s %s" % (orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 if np.isnan(offsideprice): s.update(dict(message="NAN offside price in passive mode - waiting %d %s %s" % (orderid, contract.code, contract.contractid))) return -5 if trade>0: ## Покупка if offsideprice > current_limit: ## с момента постановки ордера цена ушла вверх s.update(dict(message="Adverse price move moving to aggressive for %d %s %s" % (orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 elif trade<0: ## Selling if offsideprice < current_limit: ## с момента постановки ордера цена ушла вниз s.update(dict(message="Adverse price move moving to aggressive for %d %s %s" % (orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 ## Обнаружен дисбаланс (объем бид/объем аск если покупаем, наоборот - если продаем) balancestat=order_imbalance(marketdata, trade) if balancestat > maximbalance: s.update(dict(message="Order book imbalance of %f developed compared to %f, switching to aggressive for %d %s %s" %(balancestat , maximbalance, orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 elif Mode=="Aggressive": ## агрессивный ордер if np.isnan(sideprice): s.update(dict(message="NAN side price in aggressive mode - waiting %d %s %s" % (orderid, contract.code, contract.contractid))) return -5 if trade>0: ## Покупка if sideprice > current_limit: ## с момента постановки ордера цена ушла вверх. Переставляем ордер s.update(dict(message="Adverse price move in aggressive mode for %d %s %s" % (orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 elif trade<0: ## Selling if sideprice < current_limit ## с момента постановки ордера цена ушла вниз. Переставляем ордер s.update(dict(message="Adverse price move in aggressive mode for %d %s %s" % (orderid, contract.code, contract.contractid))) SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade) return -1 elif Mode=="Finished": ## ничего не делаем pass else: msg="Mode %s not known for order %d" % (Mode, orderid) s.update(dict(message=msg)) log=logger() log.critical(msg) raise Exception(msg) s.update(dict(message="tick no action %d %s %s" % (orderid, contract.code, contract.contractid))) diag.close() return 0 def SwitchToAggresive(dbtype, orderid, marketdata, tws, contract, trade): """ действия при смене текущего ордера на агрессивный или передвижение агрессивного ордера """ ## diagnostics... log=logger() diag=diagnostic(dbtype, system="algo", system3=str(int(orderid))) s=state_from_sdict(orderid, diag, log) if tws is None: log.info("Switch to aggressive didn't get a tws... can't do anything in orderid %d" % orderid) return -1 ## Получаем последнюю лучшую цену (равную цене при пересечении спреда) которая будет ценой нашего нового ордера am=algo_memory_table(dbtype) sideprice=am.read_value(orderid, "ValidSidePrice") ordertable=order_table(dbtype) order=ordertable.read_order_for_orderid(orderid) ordertable.close() if np.isnan(sideprice): s.update(dict(message="To Aggressive: Can't change limit for %d as got nan - will try again" % orderid)) return -1 ## обновляем ордер newlimit=sideprice order.modify(lmtPrice = newlimit) order.modify(orderType="LMT") iborder=from_myorder_to_IBorder(order) ibcontract=make_IB_contract(contract) am.update_value(order.orderid, "Limit", newlimit) am.update_value(order.orderid, "Mode", "Aggressive") am.close() tws.placeOrder( orderid, # orderId, ibcontract, # contract, iborder # order ) s.update(dict(limit_price=newlimit, side_price=sideprice, message="NowAggressive", Mode="Aggresive")) return 0 def FinishOrder(dbtype, orderid, marketdata, tws, contract): """ алгоритм не сработал, отменяем ордер """ diag=diagnostic(dbtype, system="algo", system3=str(int(orderid))) s=state_from_sdict(orderid, diag, log) log=logger() if tws is None: log.info("Finish order didn't get a tws... can't do anything in orderid %d" % orderid) return -1 log=logger() ordertable=order_table(dbtype) order=ordertable.read_order_for_orderid(orderid) log.info("Trying to cancel %d because easy algo failure" % orderid) tws.cancelOrder(int(order.brokerorderid)) order.modify(cancelled=True) ordertable.update_order(order) do_order_completed(dbtype, order) EasyAlgo_on_complete(dbtype, order, tws) s.update(dict(message="NowCancelling", Mode="Finished")) am=algo_memory_table(dbtype) am.update_value(order.orderid, "Mode", "Finished") am.close() return -1
Частичное или полное сведение ордеров
def EasyAlgo_on_partial(dbtype, order, tws): """ Функция запускается при частичном сведении ордера """ diag=diagnostic(dbtype, system="algo", system3=str(int(order.orderid))) diag.w(order.filledtrade, system2="filled") diag.w(order.filledprice, system2="fillprice") return 0 def EasyAlgo_on_complete(dbtype, order_filled, tws): """ Функция запускается при полном сведении ордера """ diag=diagnostic(dbtype, system="algo", system3=str(int(order_filled.orderid))) diag.w("Finished", system2="Mode") diag.w(order_filled.filledtrade, system2="filled") diag.w(order_filled.filledprice, system2="fillprice") am=algo_memory_table(dbtype) am.update_value(order_filled.orderid, "Mode", "Finished") am.close() return 0
Заключение
Представленный алгоритм не является идеальным, но при его использовании автором затраты на исполнение ордеров снизились на 80% по сравнению с выставлением только маркет ордеров, то есть в среднем плата за исполение составила только 1/10 спреда. Это определенно серьезное улучшение торгового алгоритма и рекомендуется к применению для алгоритмической торговли.
Получается фактически главным параметром исполнения ордера является время удержания ордера.
Не понятный момент
Если мы выставляем агрессивный ордер
... и более 10 минут проходит, отменяем ордер
Если агрессивный ордер это, например, выставление ордера на покупку по ask, то как он может быть не исполнен в течении 10 минут?
это бывает очень часто. Ваши ордера попадают на биржу с некоторой задержкой, и может быть так, что ask уже поднялся выше и ваша заявка стала в обычную бидовую очередь