Улучшенная стратегия
В базовой стратегии мы ставим заявку на уровень, который отстоит от средней цены middle_price
на фиксированную разницу offset
.
Однако, такое поведение не является разумным, если, например, на одном из направлений лучшая цена является несправедливой.
middle_price
очень чувствителен к колебаниям в стакане: если на направлении появится заявка, стоящая перед предыдущей лучшей ценой, то изменится middle_price
, и, соответственно, изменятся уровни, на которые мы хотим выставить заявку.
В том числе middle_price
изменится, если, например, заявка размером 1 лот встанет сильно выше предыдущей лучшей цены покупки.
Понятно, что в большинстве случаев в такой ситуации заявка не отражает справедливую цену покупки инструмента.
Рекомендуем подумать о том, как по-другому можно считать middle_price, чтобы снизить зависимость от "случайных" заявок.
Отметим, что стакан на предложенном инструменте является разреженным — между котировками могут быть большие промежутки (т.е. большое количество ценовых уровней без заявок), а также на многих котировках стоит небольшой объём. Запомним это на будущее, ведь понятно, что вставать на пустую цену, в целом, лучше, чем на уже занятую — в таком случае мы будем иметь приоритет в очереди.
Ценовой уровень, на который выставим заявку, будем выбирать таким образом, чтобы перед нашей заявкой по данному направлению уже стоял определённый объём лотов — в нашем случае за это отвечает параметр volume_before_our_order
.
Это позволит нам исключить возможность того, что мы встанем на "несправедливую" цену.
Если написать стратегию в таком виде, мы столкнёмся с двумя проблемами:
- Если мы будем стоять на той же цене, на которой достигается
volume_before_our_order
, то наша стратегия будет заведомо стоять в очереди за чужими заявками. - Помимо ограничения на объём перед нашей заявкой, стоит следить за тем, чтобы наши заявки (на покупку и продажу) не стояли слишком близко друг к другу. Если обе заявки исполнятся почти одновременно, то мы рискуем оказаться в минусе из-за того, что мы дважды заплатим комиссию за проведение сделки.
Вот как мы попытаемся решить эти проблемы:
- Будем стоять на цене, которая расположена на один
min_step
ближе кmiddle_price
чем та, которую мы выбрали изначально. Есть вероятность, что это будет пустая цена, что хорошо для нас. Это упрощенная версия более сложной идеи: в целом, нужно выбирать цену так, чтобы объём лотов на следующей за нами цене существенно превосходил объём лотов на нашей котировке. - Мы не будем ставить заявку, если от противоположной лучшей цены она отстоит меньше, чем на заранее заданное значение.
(Это условие имеет смысл ослабить, если окажется, что из-за его невыполнения зачастую на протяжении длительного периода времени мы вообще не выставляем заявки на данном направлении).
Это значение мы также назовём
offset
, т.к. оно имеет почти такой же смысл, как и в предыдущей стратегии.
В итоге мы получим следующую стратегию:
#include "participant_strategy.h"
using namespace hftbattle;
namespace {
class UserStrategy : public ParticipantStrategy {
public:
UserStrategy(const JsonValue& config) :
volume_(config["volume"].as<Amount>(2)),
max_pos_(config["max_pos"].as<Amount>(1)),
offset_(config["offset"].as<Price>(17)),
volume_before_our_order_(config["volume_before_our_order"].as<Amount>(2)) {
set_max_total_amount(max_pos_);
}
Amount max_available_order_amount(Amount pos, Dir dir) {
Amount max_amount = std::min(max_pos_ - dir_sign(dir) * pos, volume_);
return std::max(0, max_amount);
}
void trading_book_update(const OrderBook& order_book) override {
const auto& orders = order_book.orders();
Price middle_price = order_book.middle_price();
Amount pos = executed_amount();
add_chart_point("middle_price", middle_price);
for (Dir dir : {BID, ASK}) {
Amount accumulated_volume = 0;
size_t idx = 0;
for (; idx < order_book.depth(); ++idx) {
accumulated_volume += order_book.volume_by_index(dir, idx);
if (accumulated_volume >= volume_before_our_order_) {
break;
}
}
Price target_price = order_book.price_by_index(dir, idx) + dir_sign(dir) * order_book.min_step();
Price diff = abs(target_price - order_book.best_price(opposite_dir(dir)));
Amount order_amount = max_available_order_amount(pos, dir);
if (orders.active_orders_count(dir) == 0) {
if (order_amount > 0 && diff > offset_) {
add_limit_order(dir, target_price, order_amount);
}
} else {
Order* current_order = orders.orders_by_dir(dir).front();
if (current_order->price() != target_price) {
delete_order(current_order);
if (order_amount > 0 && diff > offset_) {
add_limit_order(dir, target_price, order_amount);
}
}
}
}
}
private:
Amount volume_;
Amount max_pos_;
Price offset_;
Amount volume_before_our_order_;
};
} // namespace
REGISTER_CONTEST_STRATEGY(UserStrategy, user_strategy)
# -*- coding: utf-8 -*-
from py_defs import *
from py_defs import Decimal as Price
from common_enums import *
class Params:
pass
def init(strat, config):
Params.VOLUME = config.get('VOLUME', 2)
Params.MAX_POS = config.get('MAX_POS', 1)
Params.OFFSET = Price(config.get('OFFSET', 17))
Params.VOLUME_BEFORE_OUR_ORDER = config.get('VOLUME_BEFORE_OUR_ORDER', 2)
strat.set_max_total_amount(Params.MAX_POS)
def max_available_order_amount(pos, dir):
max_amount = min(Params.MAX_POS - dir_sign(dir) * pos, Params.VOLUME)
return max(max_amount, 0)
def trading_book_update(strat, order_book):
orders = order_book.orders()
middle_price = order_book.middle_price()
pos = strat.executed_amount()
strat.add_chart_point('middle_price', middle_price)
for dir in (BID, ASK):
accumulated_volume = 0
for idx in xrange(order_book.depth()):
accumulated_volume += order_book.volume_by_index(dir, idx)
if accumulated_volume >= Params.VOLUME_BEFORE_OUR_ORDER:
break
target_price = order_book.price_by_index(dir, idx) + dir_sign(dir) * order_book.min_step()
diff = abs(target_price - order_book.best_price(opposite_dir(dir)))
order_amount = max_available_order_amount(pos, dir)
if orders.active_orders_count(dir) == 0:
if order_amount > 0 and diff > Params.OFFSET:
strat.add_limit_order(dir, target_price, order_amount)
else:
current_order = orders.orders_by_dir(dir)[0]
if current_order.price() != target_price:
strat.delete_order(current_order)
if order_amount > 0 and diff > Params.OFFSET:
strat.add_limit_order(dir, target_price, order_amount)
Идеи для реализации
- Серьёзный фактор, ограничивающий результат стратегии — низкое значение максимальной позиции. Увеличение данного параметра позволит стратегии совершать большее количество сделок, тем самым (почти) пропорционально увеличить заработок. Тем не менее увеличение максимальной позиции повышает риск длительного нахождения стратегии в односторонней позиции, что отрицательно влияет на итоговый результат.
- Описанные выше стратегии поддерживают лишь одну заявку на одном ценовом уровне по каждому направлению. Заняв несколько различных ценовых уровней в стакане, можно совершать значительно больше выгодных сделок.
- Выше уже отмечалось, что очень важно быстро закрывать позицию. Одним из таких методов является динамический выбор цены и объёма выставляемых заявок. Чем выше текущая позиция вдоль некоторого направления, тем активнее необходимо действовать в противоположном направлении и, наоборот, стараться воздерживаться от совершения сделок в текущем направлении.
- Помимо поддержания "котирующих" заявок на каждом из направлений, в определенные моменты можно забирать "чужой объём", который находится в стакане. Это можно делать как для уменьшения текущей позиции, так и увидев, что какой-то участник торгов стоит на несправедливой цене.
- Выше уже отмечалось, что
middle_price
плохо отражает справедливую цену в случае разреженного стакана. Рекомендуем подумать над метрикой, которая будет правильнее оценивать реальную цену инструмента. - У описанной выше стратегии есть проблема — при подсчетах объемов она учитывает свои заявки в стакане. Это может плохо повлиять на поведение стратегии: например, мы можем начать перемещать свою заявку на предыдущую цену из-за того, что при подсчёте объёма учитываем объём своих заявок на ценовых уровнях.
В ходе контеста идеи будут дополняться и описываться более подробно.