
Автор блога Financial Hacker рассказал о том, как на самом деле устроен процесс разработки высокочастотных стратегий для торговли на бирже — от важности анализа возможных задержек, до вопросов получения данных и тестирования (все с примерами кода). Для примера используется стратегия арбитражной торговли на американских биржах. Мы подготовили адаптированный перевод этого материала.
Введение
По сравнению с алгоритмами машинного обучения или обработки сигналов, использующиеся в традиционных торговых стратегиях, системы высокочастотной торговли (HFT) могут быть и на удивление простыми. Им не нужно пытаться предсказывать будущую цену акций — они и так ее знают. Точнее они узнают текущую цену чуть раньше, чем остальные, более медленные участники рынка.
Преимущество HFT в получении рыночных данных и исполнении своих заявок раньше большинства участников. Итоговая прибыльность системы зависит от ее скорости задержки, временем между получением котировки и исполнением заявки в торговом ядре биржи. Задержка (latency) — наиболее релевантный фактор при оценке HFT-системы. Его можно оптимизировать двумя способами: минимизируя физическое расстояние до биржи, и увеличивая скорость работы самой системы. И первое гораздо важнее второго.
Местоположение
В идеале, HFT-сервер должен быть расположен прямо на бирже. И большинство торговых площадок в мире с удовольствием продают серверные места в своих дата-центрах — чем ближе к хабу главной сети биржи, тем лучше считается место. Электрические сигналы в экранированном проводе передаются со скоростью в 0,7 — 0,9 от скорости света (300 км/мс). Сокращение расстояния до источника сигнала на один метр выливается в целых 8 наносекунд преимущества в раундтрипе (времени от отправк заявки до получения информации об ее исполнении). Сколько торговых возможностей можно упустить за 8 наносекунд? Никто не знает, но люди готовы платить за каждую сэкономленную наносекунду.
К сожалению (или к счастью с точки зрения экономии — размещение в дата-центрах бирж стоит огромных денег), анализируемая в данной статье HFT-система по ряду причин не может быть размещена на колокации �� ЦОД торговой площадки. При этом для торговли ей нужно получать данные с бирж NYSE в Нью-Йорке и CME (Чикаго) одновременно.
Между двумя этими городами протянуты высокоскоростные кабели, а также функционирует микроволновая сеть. В теории, идеальное расположение для системы c аналогичными требованиями — это городок Уоррен, штат Огайо. Он расположен ровно посередине между Нью-Йорком и Чикаго. Неизвестно, есть ли там хаб для высокоскоростных торговцев, однако расстояние в 357 миль до обеих бирж выливается примерно в 4 мс задержку раундтрипа.

Уоррен, Огайо – Мекка HFT торговцев (изображение: Jack Pearce / Wikipedia Commons)
Вне всяких сомнений, сервер в этом чудесном городке обойдется гораздо дешевле сервера в стойке на бирже в Нью-Йорка. Идея для стартапа: купить пару гаражей в Уоррене, подключиться к высокоскоростному кабелю между Нью-Йорков и Чикаго и зарабатывать, сдавая серверные стойки!
Софт
Когда вы уже вложили деньги в выбор оптимальной локации и каналы связи для HFT-системы, вам определенно захочется получить и софт, который будет соответствовать необходимой скорости. Коммерческие торговые платформы обычно недостаточно быстры, к тому же их код всегда закрыт, точно неизвестно, что и как в них работает. Поэтому HFT-системы почти никогда не базируются на существующих платформах, а пишутся с нуля. Не на R или Python, а на каком-либо из «быстрых» языков. В этот список входят:
- C или C++ — отличная комбинация высокоуровневости и высокой скорости. C легко читать, при этом он почти также быстр и эффективен, как машинные языки.
- Pentium Assembler — напишите свой алгоритм с помощью машинных инструкций и он обгонит даже разработанные на C системы. Из минусов такого подхода: поддерживать такой код будет непросто, все программисты знают, насколько тяжело читать программы на ассемблере, написанные кем-то другим.
- CUDA, HLSL или ассемблер GPU — если алгоритм активно использует векторные или матричные операции, то запустить его на видеокарте может быть отличное идеей.
- VHDL — если любой софт будет слишком медленным, а успех сделки для конкретного алгоритма будет зависеть от наносекунд, то «ультимативным решением» здесь будет кодирование системы напрямую в железе. В VHDL можно определять арифметические единицы, цифровые фильтры и секвенсоры FPGA чипов с тактовой частотой до нескольких сотен мегагерц. Такие чипы можно напрямую подключать к сетевому интерфейсу.
За исключением VHDL, все вышеописанное должно быть знакомо многим специалистам (особенном разработчикам компьютерных игр в 3D). Но стандартным языком для высокочастотной стратегии можно назвать C/C++. В этом материале используется именно он.
Алгоритм
Многие HFT-системы «охотятся» на трейдеров-конкурентов с помощью «методов обгона». Они замечают вашу заявку, а затем покупают тот же актив по той же цене на пару микросекунд раньше вас и продают его вам чуть дороже, зарабатывая на этом. На некоторых биржах такая торговля запрещена для создания равных условий для всех участников, другие площадки могут это разрешать, надеясь больше з��работать на комиссиях. В примере из этой статьи подобные механизмы использоваться не будут, вместо этого будет описана арбитражная стратегия. Предположим, что наши серверы расположены в Уоррене и у нас есть высокоскоростной канал до Чикаго и Нью-Йорка.
Арбитраж будет происходить между финансовыми инструментами ES и SPY. ES — это торгуемый в Чикаго фьючерс S&P500. SPY — торгуемая в Нью-Йорке ETF, которая также привязана к индексу S&P500. Один пункт ES равняется 10 центам SPY, так что цена ES примерно в десять раз выше SPY. Поскольку оба актива основаны на одном и том же индексе, можно ожидать высокой корреляции их цен. Существуют публикации, авторы которых доказывают, что эта корреляция будет «ломаться» на небольших временных отрезках. Любая возникающая на короткое время разница в ценах пары ES-SPY, превышающая спред бид-аск, создает возможности для арбитража. Алгоритм из примера будет работать по следующей стратегии:
- Определять разницу SPY-ES.
- Определить ее отклонение от среднего.
- Если отклонение превышает спред бид-аск и выходит за определенное пороговое значение, то открываются позиции в ES и SPY в противоположных направлениях.
- Если отклонение разворачивает свое направление и превышает заданный (чуть меньший) порог, позиции закрываются.
Алгоритм записан на C. Если вы до этого никогда не видели кода HFT-алгоритмов, он может показаться немного странным:
#define THRESHOLD 0.4 // Entry/Exit threshold
// Алгоритм HFT арбитража
// возвращает 0 для закрытия всех позиций
// возвращает 1 для открытия длинной позиции по ES, короткой по SPY
// возвращает 2 для открытия короткой позиции по SPY, длинной по ES
// в противном случае возвращает -1
int tradeHFT(double AskSPY,double BidSPY,double AskES,double BidES)
{
double SpreadSPY = AskSPY-BidSPY, SpreadES = AskES-BidES;
double Arbitrage = 0.5*(AskSPY+BidSPY-AskES-BidES);
static double ArbMean = Arbitrage;
ArbMean = 0.999*ArbMean + 0.001*Arbitrage;
static double Deviation = 0;
Deviation = 0.75*Deviation + 0.25*(Arbitrage - ArbMean);
static int Position = 0;
if(Position == 0) {
if(Deviation > SpreadSPY+THRESHOLD)
return Position = 1;
if(-Deviation > SpreadES+THRESHOLD)
return Position = 2;
} else {
if(Position == 1 && -Deviation > SpreadES+THRESHOLD/2)
return Position = 0;
if(Position == 2 && Deviation > SpreadSPY+THRESHOLD/2)
return Position = 0;
}
return -1;
}Функция traderHFT вызывается из некого фреймворка (в статье он не рассматривается), который получает котировки и отправляет приказы. В качестве параметров используются текущие лучшие цены на покупку и продажу по ES и SPY из верхней части книги заявок (предполагается, что цена SPY умножается на десять, чтобы оба актива находились в одном масштабе). Функция возвращает код, который говорит фреймворку, открывать или закрывать позиции, или ничего не делать. Переменная Arbitrage представляет средняя разница цен между SPY и ES. Ее среднее (ArbMean) фильтруется медленной экспоненциальной скользящей средней, а Deviation от среднего также фильтруется быстрой скользящей средней для предотвращение реакций на котировки вне нужного диапазона. Переменная Position обозначает машинное состояние, которое может принимать значение лонг, шорт и ничего. Пороговое значение для входа или выхода из позиции (Threshold) установлен на отметке в 40 центов. Это единственный регулируемый параметр системы. Если бы стратегия предназначалась для реальной торговли, нужно было бы также оптимизировать пороговое значение с использованием нескольких месяцев данных по ES и SPY.
Такую минималистичную систему совсем не трудно перевести на ассемблер или даже запрограммировать в чипе FPGA. Однако такой необходимости нет: даже если использовать для компиляции компилятор фреймворка Zorro (его развивает автор статьи), функция tradeHFT исполняется всего за 750 наносекунд. Если использовать более продвинутый компилятор вроде Microsoft VC++, это значение можно снизить до 650 наносекунд. Поскольку время между двумя котировками по ES составляет 10 микросекунд или более, скорости C вполне достаточно.
В ходе нашего HFT-эксперимента требуется ответить на два вопроса. Во-первых, действительно ли возникает разница в ценах двух инструментов достаточная для извлечения арбитражной прибыли? Во-вторых, при какой максимальной задержке система по-прежнему будет работать?
Данные
Для бэктестинга HFT-системы данные, которые можно обычно получить у брокеров бесплатно, не подойдут. Нужно раскошелиться на покупку данных по книге заявок в нужном разрешении или данных BBO (Best Bid and Offer), с включенными временными метками биржи. Без информации о том, в какое время котировка была получена на бирже, определить максимальную задержку не получится.
Некоторые компании записывают все котировки, приходящие с бирж, а затем продают эти данные. У каждой из них свой формат данных, поэтому для начала их придется привести к общему формату. В данном примере используется следующий целевой формат данных:
typedef struct T1 // single tick
{
double time; // time stamp, OLE DATE format
float fVal; // positive = ask price, negative = bid price
} T1; Одна из компаний, отслеживающих ситуацию на бирже CME, поставляет данные в формате CSV с множеством дополнительных полей, большинство из которых для решаемой задачи не нужны. Все котировки за день хранятся в одном CSV-файле. Ниже скрипт для «вытягивания» из него данных по ES за декабрь 2016 и его конвертации в датасет котировок Т1:
//////////////////////////////////////////////////////
// Convert price history from Nanotick BBO to .t1
//////////////////////////////////////////////////////
#define STARTDAY 20161004
#define ENDDAY 20161014
string InName = "History\\CME.%08d-%08d.E.BBO-C.310.ES.csv"; // name of a day file
string OutName = "History\\ES_201610.t1";
string Code = "ESZ"; // December contract symbol
string Format = "2,,%Y%m%d,%H:%M:%S,,,s,,,s,i,,"; // Nanotick csv format
void main()
{
int N,Row,Record,Records;
for(N = STARTDAY; N <= ENDDAY; N++)
{
string FileName = strf(InName,N,N+1);
if(!file_date(FileName)) continue;
Records = dataParse(1,Format,FileName); // read BBO data
printf("\n%d rows read",Records);
dataNew(2,Records,2); // create T1 dataset
for(Record = 0,Row = 0; Record < Records; Record++)
{
if(!strstr(Code,dataStr(1,Record,1))) continue; // select only records with correct symbol
T1* t1 = dataStr(2,Row,0); // store record in T1 format
float Price = 0.01 * dataInt(1,Record,3); // price in cents
if(Price < 1000) continue; // no valid price
string AskBid = dataStr(1,Record,2);
if(AskBid[0] == 'B') // negative price for Bid
Price = -Price;
t1->fVal = Price;
t1->time = dataVar(1,Record,0) + 1./24.; // add 1 hour Chicago-NY time difference
Row++;
}
printf(", %d stored",Row);
dataAppend(3,2,0,Row); // append dataset
if(!wait(0)) return;
}
dataSave(3,OutName); // store complete dataset
}
Скрипт сначала парсит CSV в промежуточный двоичный датасет, который затем конвертируется в целевой формат Т1. Поскольку временные метки проставляются по чикагскому времени, к ним нужно еще добавить один час, чтобы конвертировать их во время по Нью-Йорку.
Компания, отслеживающая Нью-Йоркскую биржу, поставляет данные в сильно сжатом специально формате NxCore Tape, его нужно сконвертировать во второй список Т1 с помощью специального плагина:
//////////////////////////////////////////////////////
// Convert price history from Nanex .nx2 to .t1
//////////////////////////////////////////////////////
#define STARTDAY 20161004
#define ENDDAY 20161014
#define BUFFER 10000
string InName = "History\\%8d.GS.nx2"; // name of a single day tape
string OutName = "History\\SPY_201610.t1";
string Code = "eSPY";
int Row,Rows;
typedef struct QUOTE {
char Name[24];
var Time,Price,Size;
} QUOTE;
int callback(QUOTE *Quote)
{
if(!strstr(Quote->Name,Code)) return 1;
T1* t1 = dataStr(1,Row,0); // store record in T1 format
t1->time = Quote->Time;
t1->fVal = Quote->Price;
Row++; Rows++;
if(Row >= BUFFER) { // dataset full?
Row = 0;
dataAppend(2,1); // append to dataset 2
}
return 1;
}
void main()
{
dataNew(1,BUFFER,2); // create a small dataset
login(1); // open the NxCore plugin
int N;
for(N = STARTDAY; N <= ENDDAY; N++) {
string FileName = strf(InName,N);
if(!file_date(FileName)) continue;
printf("\n%s..",FileName);
Row = Rows = 0; // initialize global variables
brokerCommand(SET_HISTORY,FileName); // parse the tape
dataAppend(2,1,0,Row); // append the rest to dataset 2
printf("\n%d rows stored",Rows);
if(!wait(0)) return; // abort when [Stop] was hit
}
dataSave(2,OutName); // store complete dataset
}
Функция Callback вызывается любой котировкой в исходном файле, однако большая часть данных не нужна, поэтому отфильтровываются только котировки по SPY (“eSPY”).
Подтверждение рыночной неэффективности
Получив данные из двух источников, теперь мы можем сравнивать цены ES и SPY в высоком разрешении. Вот типичный десятисекундный семпл из кривых цен:

SPY (черный) vs. ES (красный), 5 октября, 2017, 10:01:25 – 10:01.35
Разрешение здесь — одна миллисекунда. ES отрисован в долларовых единицах, SPY — в десятицентовых. Цены на графики — это цены «аск» (запрашиваемая цена). Кажется, что цены сильно коррелируют даже на столь малом интервале. ES чуть отстает.
Возможность для арбитража возникает на участке в центре — примерно в 10:01:30 ES реагировал на изменения чуть медленнее, но сильнее. Причиной могло послужить какое-то событие вроде резкого скачка цен одной из акций, входящих в индекс S&P 500. На протяжение нескольких миллисекунд разница ES-SPY превысила спред бид-аск двух активов (обычно это 25 центов по ES и 1-4 цента по SPY). В идеале, здесь можно было бы продать ES и купить SPY. Таким образом, мы подтвердили ранее предполагаемое в теории наличие рыночной неэффективности, открывающей возможности для заработка.
Скрипт для отрисовки графиков в высоком разрешении:
#define ES_HISTORY "ES_201610.t1"
#define SPY_HISTORY "SPY_201610.t1"
#define TIMEFORMAT "%Y%m%d %H:%M:%S"
#define FACTOR 10
#define OFFSET 3.575
void main()
{
var StartTime = wdatef(TIMEFORMAT,"20161005 10:01:25"),
EndTime = wdatef(TIMEFORMAT,"20161005 10:01:35");
MaxBars = 10000;
BarPeriod = 0.001/60.; // 1 ms plot resolution
Outlier = 1.002; // filter out 0.2% outliers
assetList("HFT.csv");
dataLoad(1,ES_HISTORY,2);
dataLoad(2,SPY_HISTORY,2);
int RowES=0, RowSPY=0;
while(Bar < MaxBars)
{
var TimeES = dataVar(1,RowES,0),
PriceES = dataVar(1,RowES,1),
TimeSPY = dataVar(2,RowSPY,0),
PriceSPY = dataVar(2,RowSPY,1);
if(TimeES < TimeSPY) RowES++;
else RowSPY++;
if(min(TimeES,TimeSPY) < StartTime) continue;
if(max(TimeES,TimeSPY) > EndTime) break;
if(TimeES < TimeSPY) {
asset("ES");
priceQuote(TimeES,PriceES);
} else {
asset("SPY");
priceQuote(TimeSPY,PriceSPY);
}
asset("ES");
if(AssetBar > 0) plot("ES",AskPrice+OFFSET,LINE,RED);
asset("SPY");
if(AssetBar > 0) plot("SPY",FACTOR*AskPrice,LINE,BLACK);
}
}Сначала скрипт считывает два файла с историческими данными, к��торые мы создали ранее, а затем парсит их построчно.
Тестирование системы
Для бэктестинга получившейся HFT-системы необходимо немного изменить скрипт, и вызвать функцию tradeHFT в цикле:
#define LATENCY 4.0 // milliseconds
function main()
{
var StartTime = wdatef(TIMEFORMAT,"20161005 09:30:00"),
EndTime = wdatef(TIMEFORMAT,"20161005 15:30:00");
MaxBars = 200000;
BarPeriod = 0.1/60.; // 100 ms bars
Outlier = 1.002;
assetList("HFT.csv");
dataLoad(1,ES_HISTORY,2);
dataLoad(2,SPY_HISTORY,2);
int RowES=0, RowSPY=0;
EntryDelay = LATENCY/1000.;
Hedge = 2;
Fill = 8; // HFT fill mode;
Slippage = 0;
Lots = 100;
while(Bar < MaxBars)
{
var TimeES = dataVar(1,RowES,0),
PriceES = dataVar(1,RowES,1),
TimeSPY = dataVar(2,RowSPY,0),
PriceSPY = dataVar(2,RowSPY,1);
if(TimeES < TimeSPY) RowES++;
else RowSPY++;
if(min(TimeES,TimeSPY) < StartTime) continue;
if(max(TimeES,TimeSPY) > EndTime) break;
if(TimeES < TimeSPY) {
asset("ES");
priceQuote(TimeES,PriceES);
} else {
asset("SPY");
priceQuote(TimeSPY,FACTOR*PriceSPY);
}
asset("ES");
if(!AssetBar) continue;
var AskES = AskPrice, BidES = AskPrice-Spread;
asset("SPY");
if(!AssetBar) continue;
var AskSPY = AskPrice, BidSPY = AskPrice-Spread;
int Order = tradeHFT(AskSPY,BidSPY,AskES,BidES);
switch(Order) {
case 1:
asset("ES"); enterLong();
asset("SPY"); enterShort();
break;
case 2:
asset("ES"); enterShort();
asset("SPY"); enterLong();
break;
case 0:
asset("ES"); exitLong(); exitShort();
asset("SPY"); exitLong(); exitShort();
break;
}
}
printf("\nProfit %.2f at NY Time %s",
Equity,strdate(TIMEFORMAT,dataVar(1,RowES,0)));
}
Скрипт запускает бэктест для одного торгового дня в период с 9:30 до 15:30 по Нью-Йорку. По сути, просто происходит вызов функции HFT с ценами ES и SPY, а затем выполняется код для переключения состояний. Он открывает позиции по ста единицам каждого актива (2 контракта по ES и 1000 по SPY). Задержка устанавливается с помощью переменной EntryDelay. В режиме HFT (Fill = 8) сделка проходит по последней цене после времени задержки. Это позволяет приблизить симуляцию к реальным условиям.
В таблице ниже показана прибыль по итогам симуляции с разными значениями задержки:
| Задержка | 0.5 мс | 4.0 мс | 6.0 мс | 10 мс |
|---|---|---|---|---|
| Прибыль / день | + $793 | + $273 | + $205 | – $15 |
Как видно, арбитражная стратегия ES-SPY может зарабатывать по $800 в день — при нереалистично маленькой задержке в 500 микросекунд. К сожалению, при наличии 700 миль между NYSE и CME, чтобы добиться такого результата понадобится машина времени (или какой-то инструмент квантовой телепортации). Сервер в Уоррене, штат Огайо, при задержке в 4 мс принесет примерно $300 в день. Если сервер будет чуть в стороне от высокоскоростного канала между Нью-Йорков и Чикаго, прибыль составит $200. Если инфраструктура для торговли будет еще дальше — скажем, в Нэшвилле — то заработать не удастся ничего.
Даже $300 в день выльются в годовой доход на уровне $75 000. Но для достижения такого результата понадобится, помимо железа и софта, еще и много денег. Контракт SPY стоит $250, 100 единиц для торговли выльются в 100*$2500 + 100*10*$250 = полмиллиона долларов объема торгов. Так что годовой возврат на инвестиции не превысит 15%. Результаты, однако, можно улучшить, добавив больше пар финансовых инструментов для арбитража.
Выводы
- Если система реагирует достаточно быстро, заработать она может даже очень примитивными методами, вроде арбитража между сильно коррелированными финансовыми инструментами на разных биржах.
- Физическое расположение сервера очень важно в HFT.
- ES-SPY арбитраж нельзя проводить откуда угодно. Вам придется соперничать с теми, кто уже этим занимается, и весьма вероятно из Уоррена в штате Огайо.
Другие материалы по теме финансов и фондового рынка от ITI Capital:
- Аналитика и обзоры рынка
- Назад в будущее: проверка работоспособности торгового робота с помощью исторических данных
- Событийно-ориентированный бэктестинг на Python шаг за шагом (Часть 1, Часть 2, Часть 3, Часть 4, Часть 5)
