12 июня 2015 QuantAlgos
Для построения прибыльного биржевого робота недостаточно разработать только алгоритм с положительным математическим ожиданием определения будущего движения цены. Чтобы не истратить это преимущество на проскальзывание, комиссию и т.п. при входе в сделку и закрытии позиций, необходимо предусмотреть механизм правильного выставления и исполнения ордеров. Подобный механизм в упрощенном виде представлен в блоге Investment Idiocy, здесь привожу перевод этой статьи.
Автор использует полностью автоматические системы для торговли на бирже. Сначала использовался простейший процесс исполнения:
проверить, что на лучшем биде (если собираетесь продавать) или на лучшем аске (при покупке) есть достаточно большой объем ордеров, чтобы удовлетворить вашу заявку;
послать маркет-ордер (или лимитную заявку с ценой, равной цене лучшего бида/аска)
Вот так, очень примитивно. Но затем автор решил проверить, во сколько ему обходится такой алгоритм исполнения. Большую часть времени стоимость такой постановки заявок составила половину спреда (разницы между лучшим аском и лучшим бидом).
После пары месяцев такой торговли было принято решение об уменьшении потерь при исполнении ордеров и для этого необходимо было сделать правильный алгоритм выставления заявок.
Автор признает, что не является экспертом в создании подобных алгоритмов, поэтому должен руководствоваться простыми принципами. Так как разработка в основном ведется на языке Python, который является достаточно медленным, то и алгоритм не будет содержать особо сложного кода, который необходим для исполнения при высокочастотной торговле. Также выставляемые ордера не содержат большого объема, поэтому не нужно разбивать их на малые части и затем управлять каждой. В связи со всем этим, алгоритм должен быть несложным.
Нужно упомянуть одну важную деталь - перестановка ордеров может быть небесплатна. Некоторые биржи взимают плату за транзакции, или за превышение порога транзакций. Это не очень большая плата, и преимущества интеллектуального исполнения должны превысить эти затраты.
Хороший ручной трейдер, желающий исполнить небольшой ордер на покупку и не бепокоящийся о своем влиянии на рынок, вероятно поступит так:
отправит лимитный ордер на ту сторону спреда, где он хочет торговать, добавив его на текущий уровень. Если мы покупаем, то это текущий лучший бид. По биржевой терминологии это пассивное поведение, мы ждем. когда кто-то возьмет наш ордер.
в идеальных условиях этот начальный ордер будет исполнен. Мы выиграем половину спреда, то есть стоимость исполнения становится отрицательной ( лучший бид минус средняя цена).
но если:
ордер не будет исполнен в течение нескольких минут
или есть предположение, что рынок пойдет в сторону от вашей заявки и далее
или рынок уже двинулся в эту сторону
... то умный трейдер будет ограничивать свои убытки и модифицирует свою заявку - понесет затраты на исполнение и пересечет спред. Это агрессивное поведение.
Новый ордер купит актив по лучшему аску. В теории он исполнится, стоимость исполнения составит половину спреда ( если рынок уже двигался некоторое время в другую сторону от заявки, то и более).
Если вы слишком медленный и рынок продолжает движение, продолжайте перестановку ордера, пытаясь исполнится на лучшем аске, пока весь ордер не исполнится.
Хотя алгоритм выглядит просто, есть много подводных камней при его реализации, и получить правильное исполнение можно, если учесть все эти особенности. Ниже рассмотрим стратегию детально на языке программирования. Хотя мы представим все на языке Python, код не будет исполняться, так как автор не включил некоторые необходимые подпрограммы. Но это все равно даст вам основную идею, как создать самостоятельно подобный алгоритм.
Подготовительный этап перед сделкой
Очень опасно торговать с помощью автоматических алгоритмов, если рынок недостаточно ликвиден. Следующая программа проверяет это:
pretrademultiplier=4.0
def EasyAlgo_pretrade(ctrade, contract, dbtype, tws):
"""
Функция ЕasуАlgo запускается перед отправкой нового ордера
ctrade: планируемая сделка, знаковая целая переменная
contract: актив, который мы торгуем
dbtype, tws: обозначает базу данных и API, которые используем
Функция возвращает значение объема, который безопасен для торговли
Ноль означает, что биржа не поддерживает этот вид ордера
"""
## Получаем маркет-дату (список, содержащий спред и объемы)
bookdata=get_market_data(dbtype, tws, contract, snapshot=False, maxstaleseconds=5, maxwaitseconds=5)
## None означает, что API не запущен и рынок закрыт
if bookdata is None:
return (0, bookdata)
## Проверка, ликвиден ли рынок, спред и объем должен быть в рамках определенных лимитов. Мы используем множитель, так как менее требовательны при использовании лимитных ордеров - широкий спред играет нам на руку
market_liquid=check_is_market_liquid(bookdata, contract.code, multiplier=pretrademultiplier)
if not market_liquid:
return (0, bookdata)
## Если рынок ликвиден, но наш ордер слишком велик в сравнении с объемами на концах спреда, мы можем урезать его до нужного объема
cutctrade=cut_down_trade_to_order_book(bookdata, ctrade, multiplier=pretrademultiplier)
return (cutctrade, bookdata)
Новый ордер
Следующая программа нужна для постановки нового ордера:
MAX_DELAY=0.03
def EasyAlgo_new_order(order, tws, dbtype, use_orderid, bookdata):
"""
Эта функция запускается для получения нового ордера
Args:
order - тип ордера, нужный для сделки
tws - объект для связи с API Interactive Brokers
dbtype - используемая база данных
use_orderid- идентификационный номер ордера
bookdata- список, содержащий лучший бид и аск и их объемы
"""
## s, состояние, переменная, используемая для логгирования и диагностики
log=logger()
diag=diagnostic(dbtype, system="algo", system3=str(order.orderid))
s=state_from_sdict(order.orderid, diag, log)
## Из стакана и сделок получаем цену по которой мы ставим агрессивные (sideprice) и пассивные ордера (offsideprice)
(sideprice, offsideprice)=get_price_sides(bookdata, order.submit_trade)
if np.isnan(offsideprice) or offsideprice==0:
log.warning("No offside / limit price in market data so can't issue the order")
return None
if np.isnan(sideprice) or sideprice==0:
log.warning("No sideprice in market data so dangerous to issue the order")
return None
## объект order содержит цену, установленную при генерации ордера, проверка - есть ли большое движение цены с этого момента (должно быть менее, чем за 1 сек)
if not np.isnan(order.submit_price):
delay=abs((offsideprice/order.submit_price) - 1.0)
if delay>MAX_DELAY:
log.warning("Large move since submission - not trading a limit order on that")
return None
## Если мы удовлетворены состоянием стакана, устанавливаем цену лимит ордера 'offside' - лучший аск при продаже, лучший бид при покупке
limitprice=offsideprice
## меняем объект ордер - сейчас это лимитный ордер с нужной ценой
order.modify(lmtPrice = limitprice)
order.modify(orderType="LMT")
## необходима трансляция нашего объекта в объекты API
iborder=from_myorder_to_IBorder(order)
contract=Contract(code=order.code, contractid=order.contractid)
ibcontract=make_IB_contract(contract)
## диагностический блок
s.update(dict(limit_price=limitprice, offside_price=offsideprice, side_price=sideprice,
message="StartingPassive", Mode="Passive"))
timenow=datetime.datetime.now()
## таблица сохранения алгоритма используется для сохранения информации состояний. Важно - пременная Mode=PASSIVE в начальном состоянии!
am=algo_memory_table(dbtype)
am.update_value(order.orderid, "Limit", limitprice)
am.update_value(order.orderid, "ValidSidePrice", sideprice)
am.update_value(order.orderid, "ValidOffSidePrice", offsideprice)
am.update_value(order.orderid, "Trade", order.submit_trade)
am.update_value(order.orderid, "Started", date_as_float(timenow))
am.update_value(order.orderid, "Mode", "Passive")
am.update_value(order.orderid, "LastNotice", date_as_float(timenow))
am.close()
## Отправляем ордер
tws.placeOrder(
use_orderid, # orderId,
ibcontract, # contract,
iborder # order
)
## Возвращает поток ордеров для сохранения в базе данных. Если программа завершается раньше, возвращает None; таким образом, подпрограмма знает, что ни один ордер не был выставлен
return order
В следующей части рассмотрим действия при обновлении стакана и сделок, отраженные в соответствующих подпрограммах на языке Python.
Действия по тику
Тик получаем из 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 спреда. Это определенно серьезное улучшение торгового алгоритма и рекомендуется к применению для алгоритмической торговли.
Не является индивидуальной инвестиционной рекомендацией | При копировании ссылка обязательна | Нашли ошибку - выделить и нажать Ctrl+Enter | Жалоба