Почитал у известного чела о пост-условиях (“посткондишнс”) на свифте — и знаете, раст все равно лучше. Тупо приятнее язык даже для создания торговой биржи со своей мудреной бизнес-логикой — тикерами, логикой стопов и лимитных ордеров, многопоточностью, паттерн матчингом, децентрализованной сетью, графиками, распределениями парето, гаусса, стьюдента. Не трогая деревативов и разницу форвард/фьючерс, есть “аск” и “бид”, три вида ордеров, тикеры, взлеты и падения, вискарь-тревоги-роботы — короче, что нужно сделаем (кроме роботов), а для всего остального мастер кард (или МИР, а для европейцев “е-ц” карты).
Простейшая реализаций биржы (тикера) складывается всего из двух куч (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()}}
На сегодня все. Удачного кодинга 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/1041308/