|
Professor Seleznov
|
Почитал у известного чела о пост-условиях (“посткондишнс”) на свифте - и знаете, раст все равно лучше. Тупо приятнее язык даже для создания торговой биржи со своей мудреной бизнес-логикой - тикерами, логикой стопов и лимитных ордеров, многопоточностью, паттерн матчингом, децентрализованной сетью, графиками, распределениями парето, гаусса, стьюдента. Не трогая деревативов и разницу форвард/фьючерс, есть “аск” и “бид”, три вида ордеров, тикеры, взлеты и падения, вискарь-тревоги-роботы - короче, что нужно сделаем (кроме роботов), а для всего остального мастер кард (или МИР, а для европейцев “е-ц” карты). Простейшая реализаций биржы (тикера) складывается всего из двух куч (binary heap) или двух стаканов: “аск” (продажа) и “бид” (покупка), и следствием их пересечения является сделка. Кто-либо создает ордер на покупку, указывая количество, и ордер исполняется мгновенно по верхней цене в стакане. Если хочется купить дешевле, создается лимитный ордер с указанием цены, а сделка ожидает продавца, согласного продать вам по указанной цене. С точки зрения реализации нет особой разницы между “asap” и лимитными ордерами. Чуть сложнее стопы. Стопы (ордеры) зависят от цен на табло и исполняются на продажу по любой цене (мгновенно) при цене на табло равной или меньше указанной в ордере. Стопы могут тригернуть другие стопы, поэтому в реализцаии чуть сложнее. Биржа на бумаге выглядит как-то так: StockRaw -> StockRawWithStop -> StockWithHistory -> Broker -> Client. В расте вполне естественно завернуть в “енамы” данные и удобно раскрыть паттерн матчингом принимающей стороны на соответсвуещем уровне, например: Intent::Buy(Limit(Price(p), Quantity(q))) или Intent::Sell(Asap(Quantity(q))) - брокеру важно лишь намерение клиента (intent), или лимитный ордер обработается на нижнем уровне. Можно отметить, что в расте очень удобно компоновать - ниже четыре варианта покрыты двумя “кейзами”, содержимое элегантно развернуто, слеплено и завернуто обратно:
match (x,y) { (Some(xs), Some(ys)) => Some(vec![xs, ys].concat()), (x,y) => x.or(y) }
Пост-условие (“посткондишнс”) - это такой способ не потерять корректность в слегка запутанной бизнес-логике. В примере (ниже) мы используем “дифференц” между нашей биржей и ордером, и далее (в методе “deal”) усиливаем пост-условие, гарантируя количество ордеров на бирже до и после сделки.
impl Stock { /// postcondition: /// stock.quantity - new_stock.quantity <= order.quantity * 2 fn difference(&mut self, x: Order) { self.ask.pop() .map(|Reverse(a)| a - x) .map(|a| if a.quantity != 0 { self.ask.push(Reverse(a)); } ); self.bid.pop() .map(|b| b - x) .map(|b| if b.quantity != 0 { self.bid.push(b); } ); } /// postcondition: /// stock.quantity - new_stock.quantity = ret.quantity * 2 fn deal(&mut self) -> Option<Deal> { self.ask.peek() .zip(self.bid.peek()) .and_then(|(Reverse(a), b)| if a <= b { Some((*a,*b)) } else { None }) .map(|(a, b)| { self.difference(a & b); Deal::new(a, b) }) } }
Многопоточное программирование сложная штука, и в расте, и где-либо еще. Тупо, как мне кажется, не хватает примитивов - не тех которые атомики, а на уровне компоновки. Сейчас многопоточное выглядит, сорян за метафору - как одновременная обжарка и заморозка мясного. В обычном программировании у нас есть функции, структуры, петли (loop), рекурсия - у раста, кстати, прекрасные примитивы для работы с петлями (std::iter::successors и std::iter::from_fn) - и мы можем действительно создавать хорошие системы, но относительно медленные. Поэтому ускоряем самым тупым и простым способом - мутексом (что-то типа static STOCK: Mutex<Stock> = Mutex::new(Stock::new())), не забывая подавать ордеры пачками (batch), компенсируя стоимость лока. Пишем юнит тесты, строим графиками три распределения (на рисунке ниже), и одно из них (парето) взято по логарифмической шкале. На графике в течение года четыре клиента пытаются выиграть у рынка на каждой минуте, а три фонда - со сделками раз в день (что-то типа Client{vol: 0.01, p: 0.5, quantity: 1000, distribution, s: Strategy::Limit}).

Три распределения Для сравнения график (геометрического) броуновского движения актива с волатильностью в один процент на день или 15.7 % в год.

Броуновское движение Исполнение биржи (тикера) довольно-таки прямолинейное, и для реализации децентрализованной биржи нам нужен еще и хеш, и проверка голосованием - отправляем (на проверку) результат сделки - если не ок, берем версию ребят, в противном случае коммитимся после “драйрана” (dry_run). Хеш берем равным ордеру юзера и результату сделки. “ДрайРан” удобно вынести в “трэйт”, и для реализации без багов помогает соблюдение пост-условий.
impl Stock { /// postcondition: /// stock.quantity == new_stock.quantity fn ask_dry_run(&mut self, order: Order) -> Vec<Deal> { self.ask.push(Reverse(order)); let (quantity, deals) = self.execute_ask_dry_run(); if quantity != order.quantity { self.ask.pop(); } deals } /// postcondition: /// stock.bid.quantity == new_stock.bid.quantity fn execute_ask_dry_run(&mut self) -> (u32, Vec<Deal>) { let deals = self.execute(); for d in &deals { self.bid.push(d.bid); } (deals.iter().map(|d| d.quantity).sum(), deals) } }
Раст не “эрланг”, но что-то умеет в сообщениях: довольно удобно добавить обратный адрес (часть канала) в сообщение:
impl Host { fn ask_verify(&self, p: Payload) -> Vec<Receiver<Msg>> { self.hosts.iter().map(|host| { let (tx, rx) = channel(); host.send((p, tx)); return rx; }) .collect() } }
На сегодня все. Удачного кодинга -Источник
|