...

суббота, 19 мая 2018 г.

[Перевод] Выбор места для сервера и софта, тестирование рыночной неэффективности: как на самом деле создают роботов для торговли н

Автор блога Financial Hacker рассказал о том, как на самом деле устроен процесс разработки высокочастотных стратегий для торговли на бирже — от важности анализа возможных задержек, до вопросов получения данных и тестирования (все с примерами кода). Для примера используется стратегия арбитражной торговли на американских биржах. Мы подготовили адаптированный перевод этого материала.

Введение


По сравнению с алгоритмами машинного обучения или обработки сигналов, использующиеся в традиционных торговых стратегиях, системы высокочастотной торговли (HFT) могут быть и на удивление простыми. Им не нужно пытаться предсказывать будущую цену акций — они и так ее знают. Точнее они узнают текущую цену чуть раньше, чем остальные, более медленные участники рынка.

Преимущество HFT в получении рыночных данных и исполнении своих заявок раньше большинства участников. Итоговая прибыльность системы зависит от ее скорости задержки, временем между получением котировки и исполнением заявки в торговом ядре биржи. Задержка (latency) — наиболее релевантный фактор при оценке HFT-системы. Его можно оптимизировать двумя способами: минимизируя физическое расстояние до биржи, и увеличивая скорость работы самой системы. И первое гораздо важнее второго.

Местоположение


В идеале, HFT-сервер должен быть расположен прямо на бирже. И большинство торговых площадок в мире с удовольствием продают серверные места в своих дата-центрах — чем ближе к хабу главной сети биржи, тем лучше считается место. Электрические сигналы в экранированном проводе передаются со скоростью в 0,7 — 0,9 от скорости света (300 км/мс). Сокращение расстояния до источника сигнала на один метр выливается в целых 8 наносекунд преимущества в раундтрипе (времени от отправк заявки до получения информации об ее исполнении). Сколько торговых возможностей можно упустить за 8 наносекунд? Никто не знает, но люди готовы платить за каждую сэкономленную наносекунду.

К сожалению (или к счастью с точки зрения экономии — размещение в дата-центрах бирж стоит огромных денег), анализируемая в данной статье HFT-система по ряду причин не может быть размещена на колокации в ЦОД торговой площадки. При этом для торговли ей нужно получать данные с бирж NYSE в Нью-Йорке и CME (Чикаго) одновременно.

Между двумя этими городами протянуты высокоскоростные кабели, а также функционирует микроволновая сеть. В теории, идеальное расположение для системы c аналогичными требованиями — это городок Уоррен, штат Огайо. Он расположен ровно посередине между Нью-Йорком и Чикаго. Неизвестно, есть ли там хаб для высокоскоростных торговцев, однако расстояние в 357 миль до обеих бирж выливается примерно в 4 мс задержку раундтрипа.

image

Уоррен, Огайо – Мекка 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 в высоком разрешении. Вот типичный десятисекундный семпл из кривых цен:

image

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:


Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий