Автор блога 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: