1use std::cmp::Reverse;
4use std::collections::{BTreeMap, HashMap};
5use std::fmt::Write;
6use std::str::FromStr;
7
8use chrono::{DateTime, Duration, Utc};
9use crc32fast::Hasher;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14pub type Price = Decimal;
16pub type Quantity = Decimal;
18pub type Symbol = String;
20
21pub type OrderId = String;
23
24#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum InstrumentKind {
28 Spot,
29 LinearPerpetual,
30 InversePerpetual,
31}
32
33#[derive(Clone, Debug, Deserialize, Serialize)]
35pub struct Instrument {
36 pub symbol: Symbol,
37 pub base: Symbol,
38 pub quote: Symbol,
39 pub kind: InstrumentKind,
40 pub settlement_currency: Symbol,
41 pub tick_size: Price,
42 pub lot_size: Quantity,
43}
44
45#[derive(Clone, Debug, Deserialize, Serialize)]
47pub struct Cash {
48 pub currency: Symbol,
49 pub quantity: Quantity,
50 pub conversion_rate: Price,
51}
52
53impl Cash {
54 #[must_use]
56 pub fn value(&self) -> Price {
57 self.quantity * self.conversion_rate
58 }
59}
60
61#[derive(Clone, Debug, Default, Deserialize, Serialize)]
63pub struct CashBook(pub HashMap<Symbol, Cash>);
64
65impl CashBook {
66 #[must_use]
67 pub fn new() -> Self {
68 Self(HashMap::new())
69 }
70
71 pub fn upsert(&mut self, cash: Cash) {
72 self.0.insert(cash.currency.clone(), cash);
73 }
74
75 pub fn adjust(&mut self, currency: &str, delta: Quantity) -> Quantity {
76 let entry = self.0.entry(currency.to_string()).or_insert(Cash {
77 currency: currency.to_string(),
78 quantity: Decimal::ZERO,
79 conversion_rate: Decimal::ZERO,
80 });
81 entry.quantity += delta;
82 entry.quantity
83 }
84
85 pub fn update_conversion_rate(&mut self, currency: &str, rate: Price) {
86 let entry = self.0.entry(currency.to_string()).or_insert(Cash {
87 currency: currency.to_string(),
88 quantity: Decimal::ZERO,
89 conversion_rate: Decimal::ZERO,
90 });
91 entry.conversion_rate = rate;
92 }
93
94 #[must_use]
95 pub fn total_value(&self) -> Price {
96 self.0.values().map(Cash::value).sum()
97 }
98
99 #[must_use]
100 pub fn get(&self, currency: &str) -> Option<&Cash> {
101 self.0.get(currency)
102 }
103
104 #[must_use]
105 pub fn get_mut(&mut self, currency: &str) -> Option<&mut Cash> {
106 self.0.get_mut(currency)
107 }
108
109 pub fn iter(&self) -> impl Iterator<Item = (&Symbol, &Cash)> {
110 self.0.iter()
111 }
112}
113
114#[derive(Clone, Debug, Deserialize, Serialize)]
116pub enum ExecutionHint {
117 Twap { duration: Duration },
119 Vwap {
121 duration: Duration,
122 #[serde(default)]
123 participation_rate: Option<Decimal>,
124 },
125 IcebergSimulated {
127 display_size: Quantity,
128 #[serde(default)]
129 limit_offset_bps: Option<Decimal>,
130 },
131 PeggedBest {
133 offset_bps: Decimal,
134 #[serde(default)]
135 clip_size: Option<Quantity>,
136 #[serde(default)]
137 refresh_secs: Option<u64>,
138 #[serde(default)]
139 min_chase_distance: Option<Price>,
140 },
141 Sniper {
143 trigger_price: Price,
144 #[serde(default)]
145 timeout: Option<Duration>,
146 },
147}
148
149#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
151pub enum Side {
152 Buy,
154 Sell,
156}
157
158impl Side {
159 #[must_use]
161 pub fn inverse(self) -> Self {
162 match self {
163 Self::Buy => Self::Sell,
164 Self::Sell => Self::Buy,
165 }
166 }
167
168 #[must_use]
170 pub fn as_i8(self) -> i8 {
171 match self {
172 Self::Buy => 1,
173 Self::Sell => -1,
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
180pub enum OrderType {
181 Market,
183 Limit,
185 StopMarket,
187}
188
189#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
191pub enum TimeInForce {
192 GoodTilCanceled,
193 ImmediateOrCancel,
194 FillOrKill,
195}
196
197#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
199pub enum Interval {
200 OneSecond,
201 OneMinute,
202 FiveMinutes,
203 FifteenMinutes,
204 OneHour,
205 FourHours,
206 OneDay,
207}
208
209impl Interval {
210 #[must_use]
212 pub fn as_duration(self) -> Duration {
213 match self {
214 Self::OneSecond => Duration::seconds(1),
215 Self::OneMinute => Duration::minutes(1),
216 Self::FiveMinutes => Duration::minutes(5),
217 Self::FifteenMinutes => Duration::minutes(15),
218 Self::OneHour => Duration::hours(1),
219 Self::FourHours => Duration::hours(4),
220 Self::OneDay => Duration::days(1),
221 }
222 }
223
224 #[must_use]
226 pub fn to_bybit(self) -> &'static str {
227 match self {
228 Self::OneSecond => "1",
229 Self::OneMinute => "1",
230 Self::FiveMinutes => "5",
231 Self::FifteenMinutes => "15",
232 Self::OneHour => "60",
233 Self::FourHours => "240",
234 Self::OneDay => "D",
235 }
236 }
237
238 pub fn to_binance(self) -> &'static str {
240 match self {
241 Self::OneSecond => "1s",
242 Self::OneMinute => "1m",
243 Self::FiveMinutes => "5m",
244 Self::FifteenMinutes => "15m",
245 Self::OneHour => "1h",
246 Self::FourHours => "4h",
247 Self::OneDay => "1d",
248 }
249 }
250}
251
252impl FromStr for Interval {
253 type Err = String;
254
255 fn from_str(value: &str) -> Result<Self, Self::Err> {
256 match value.to_lowercase().as_str() {
257 "1s" | "1sec" | "1second" | "1" => Ok(Self::OneSecond),
258 "1m" | "1min" | "1minute" => Ok(Self::OneMinute),
259 "5m" | "5min" | "5minutes" => Ok(Self::FiveMinutes),
260 "15m" | "15min" | "15minutes" => Ok(Self::FifteenMinutes),
261 "1h" | "60m" | "1hour" | "60" => Ok(Self::OneHour),
262 "4h" | "240m" | "4hours" | "240" => Ok(Self::FourHours),
263 "1d" | "day" | "d" => Ok(Self::OneDay),
264 other => Err(format!("unsupported interval '{other}'")),
265 }
266 }
267}
268
269#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
271pub struct Tick {
272 pub symbol: Symbol,
273 pub price: Price,
274 pub size: Quantity,
275 pub side: Side,
276 pub exchange_timestamp: DateTime<Utc>,
277 pub received_at: DateTime<Utc>,
278}
279
280#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
282pub struct Candle {
283 pub symbol: Symbol,
284 pub interval: Interval,
285 pub open: Price,
286 pub high: Price,
287 pub low: Price,
288 pub close: Price,
289 pub volume: Quantity,
290 pub timestamp: DateTime<Utc>,
291}
292
293#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
295pub struct OrderBookLevel {
296 pub price: Price,
297 pub size: Quantity,
298}
299
300#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
302pub struct OrderBook {
303 pub symbol: Symbol,
304 pub bids: Vec<OrderBookLevel>,
305 pub asks: Vec<OrderBookLevel>,
306 pub timestamp: DateTime<Utc>,
307 #[serde(default)]
308 pub exchange_checksum: Option<u32>,
309 #[serde(default)]
310 pub local_checksum: Option<u32>,
311}
312
313impl OrderBook {
314 #[must_use]
316 pub fn best_bid(&self) -> Option<&OrderBookLevel> {
317 self.bids.first()
318 }
319
320 #[must_use]
322 pub fn best_ask(&self) -> Option<&OrderBookLevel> {
323 self.asks.first()
324 }
325
326 #[must_use]
328 pub fn imbalance(&self, depth: usize) -> Option<Decimal> {
329 let depth = depth.max(1);
330 let bid_vol: Decimal = self.bids.iter().take(depth).map(|level| level.size).sum();
331 let ask_vol: Decimal = self.asks.iter().take(depth).map(|level| level.size).sum();
332 let denom = bid_vol + ask_vol;
333 if denom.is_zero() {
334 None
335 } else {
336 Some((bid_vol - ask_vol) / denom)
337 }
338 }
339
340 #[must_use]
342 pub fn computed_checksum(&self, depth: Option<usize>) -> u32 {
343 let mut lob = LocalOrderBook::new();
344 let bids = self
345 .bids
346 .iter()
347 .map(|level| (level.price, level.size))
348 .collect::<Vec<_>>();
349 let asks = self
350 .asks
351 .iter()
352 .map(|level| (level.price, level.size))
353 .collect::<Vec<_>>();
354 lob.load_snapshot(&bids, &asks);
355 let depth = depth.unwrap_or_else(|| bids.len().max(asks.len()).max(1));
356 lob.checksum(depth)
357 }
358}
359
360#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
362pub struct DepthUpdate {
363 pub symbol: Symbol,
364 pub bids: Vec<OrderBookLevel>,
365 pub asks: Vec<OrderBookLevel>,
366 pub timestamp: DateTime<Utc>,
367}
368
369#[derive(Clone, Debug, Default, Deserialize, Serialize)]
371pub struct LocalOrderBook {
372 bids: BTreeMap<Reverse<Price>, Quantity>,
373 asks: BTreeMap<Price, Quantity>,
374}
375
376impl LocalOrderBook {
377 #[must_use]
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 pub fn load_snapshot(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
385 self.bids.clear();
386 self.asks.clear();
387 for &(price, qty) in bids {
388 self.add_order(Side::Buy, price, qty);
389 }
390 for &(price, qty) in asks {
391 self.add_order(Side::Sell, price, qty);
392 }
393 }
394
395 pub fn add_order(&mut self, side: Side, price: Price, quantity: Quantity) {
397 if quantity <= Decimal::ZERO {
398 return;
399 }
400 match side {
401 Side::Buy => {
402 let key = Reverse(price);
403 let entry = self.bids.entry(key).or_insert(Decimal::ZERO);
404 *entry += quantity;
405 }
406 Side::Sell => {
407 let entry = self.asks.entry(price).or_insert(Decimal::ZERO);
408 *entry += quantity;
409 }
410 }
411 }
412
413 pub fn apply_delta(&mut self, side: Side, price: Price, quantity: Quantity) {
415 if quantity <= Decimal::ZERO {
416 self.clear_level(side, price);
417 return;
418 }
419 match side {
420 Side::Buy => {
421 self.bids.insert(Reverse(price), quantity);
422 }
423 Side::Sell => {
424 self.asks.insert(price, quantity);
425 }
426 }
427 }
428
429 pub fn apply_deltas(&mut self, bids: &[(Price, Quantity)], asks: &[(Price, Quantity)]) {
431 for &(price, qty) in bids {
432 self.apply_delta(Side::Buy, price, qty);
433 }
434 for &(price, qty) in asks {
435 self.apply_delta(Side::Sell, price, qty);
436 }
437 }
438
439 pub fn remove_order(&mut self, side: Side, price: Price, quantity: Quantity) {
441 if quantity <= Decimal::ZERO {
442 return;
443 }
444 match side {
445 Side::Buy => {
446 let key = Reverse(price);
447 if let Some(level) = self.bids.get_mut(&key) {
448 *level -= quantity;
449 if *level <= Decimal::ZERO {
450 self.bids.remove(&key);
451 }
452 }
453 }
454 Side::Sell => {
455 if let Some(level) = self.asks.get_mut(&price) {
456 *level -= quantity;
457 if *level <= Decimal::ZERO {
458 self.asks.remove(&price);
459 }
460 }
461 }
462 }
463 }
464
465 pub fn clear_level(&mut self, side: Side, price: Price) {
467 match side {
468 Side::Buy => {
469 self.bids.remove(&Reverse(price));
470 }
471 Side::Sell => {
472 self.asks.remove(&price);
473 }
474 }
475 }
476
477 #[must_use]
479 pub fn best_bid(&self) -> Option<(Price, Quantity)> {
480 self.bids.iter().next().map(|(price, qty)| (price.0, *qty))
481 }
482
483 #[must_use]
485 pub fn best_ask(&self) -> Option<(Price, Quantity)> {
486 self.asks.iter().next().map(|(price, qty)| (*price, *qty))
487 }
488
489 pub fn bids(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
491 self.bids.iter().map(|(price, qty)| (price.0, *qty))
492 }
493
494 pub fn asks(&self) -> impl Iterator<Item = (Price, Quantity)> + '_ {
496 self.asks.iter().map(|(price, qty)| (*price, *qty))
497 }
498
499 pub fn take_liquidity(
501 &mut self,
502 aggressive_side: Side,
503 mut quantity: Quantity,
504 ) -> Vec<(Price, Quantity)> {
505 let mut fills = Vec::new();
506 while quantity > Decimal::ZERO {
507 let (price, available) = match aggressive_side {
508 Side::Buy => match self.best_ask() {
509 Some(level) => level,
510 None => break,
511 },
512 Side::Sell => match self.best_bid() {
513 Some(level) => level,
514 None => break,
515 },
516 };
517 let traded = quantity.min(available);
518 let contra_side = aggressive_side.inverse();
519 self.remove_order(contra_side, price, traded);
520 fills.push((price, traded));
521 quantity -= traded;
522 }
523 fills
524 }
525
526 #[must_use]
528 pub fn is_empty(&self) -> bool {
529 self.bids.is_empty() && self.asks.is_empty()
530 }
531
532 #[must_use]
534 pub fn checksum(&self, depth: usize) -> u32 {
535 if depth == 0 {
536 return 0;
537 }
538 let mut buffer = String::new();
539 let mut first = true;
540 for (price, size) in self.bids().take(depth) {
541 if !first {
542 buffer.push(':');
543 }
544 first = false;
545 write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
546 }
547 for (price, size) in self.asks().take(depth) {
548 if !first {
549 buffer.push(':');
550 }
551 first = false;
552 write!(buffer, "{}:{}", price.normalize(), size.normalize()).ok();
553 }
554 let mut hasher = Hasher::new();
555 hasher.update(buffer.as_bytes());
556 hasher.finalize()
557 }
558
559 pub fn bid_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
561 self.bids().take(depth).collect()
562 }
563
564 pub fn ask_levels(&self, depth: usize) -> Vec<(Price, Quantity)> {
566 self.asks().take(depth).collect()
567 }
568}
569
570#[derive(Clone, Debug, Deserialize, Serialize)]
572pub struct OrderRequest {
573 pub symbol: Symbol,
574 pub side: Side,
575 pub order_type: OrderType,
576 pub quantity: Quantity,
577 pub price: Option<Price>,
578 pub trigger_price: Option<Price>,
579 pub time_in_force: Option<TimeInForce>,
580 pub client_order_id: Option<String>,
581 pub take_profit: Option<Price>,
582 pub stop_loss: Option<Price>,
583 pub display_quantity: Option<Quantity>,
584}
585
586#[derive(Clone, Debug, Deserialize, Serialize)]
588pub struct OrderUpdateRequest {
589 pub order_id: OrderId,
590 pub symbol: Symbol,
591 pub side: Side,
592 pub new_price: Option<Price>,
593 pub new_quantity: Option<Quantity>,
594}
595
596#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
598pub enum OrderStatus {
599 PendingNew,
600 Accepted,
601 PartiallyFilled,
602 Filled,
603 Canceled,
604 Rejected,
605}
606
607#[derive(Clone, Debug, Deserialize, Serialize)]
609pub struct Order {
610 pub id: OrderId,
611 pub request: OrderRequest,
612 pub status: OrderStatus,
613 pub filled_quantity: Quantity,
614 pub avg_fill_price: Option<Price>,
615 pub created_at: DateTime<Utc>,
616 pub updated_at: DateTime<Utc>,
617}
618
619#[derive(Clone, Debug, Deserialize, Serialize)]
621pub struct Fill {
622 pub order_id: OrderId,
623 pub symbol: Symbol,
624 pub side: Side,
625 pub fill_price: Price,
626 pub fill_quantity: Quantity,
627 pub fee: Option<Price>,
628 pub timestamp: DateTime<Utc>,
629}
630
631#[derive(Clone, Debug, Deserialize, Serialize)]
633pub struct Trade {
634 pub id: Uuid,
635 pub fill: Fill,
636 pub realized_pnl: Price,
637}
638
639#[derive(Clone, Debug, Deserialize, Serialize)]
641pub struct Position {
642 pub symbol: Symbol,
643 pub side: Option<Side>,
644 pub quantity: Quantity,
645 pub entry_price: Option<Price>,
646 pub unrealized_pnl: Price,
647 pub updated_at: DateTime<Utc>,
648}
649
650impl Position {
651 pub fn mark_price(&mut self, price: Price) {
653 if let (Some(entry), Some(side)) = (self.entry_price, self.side) {
654 let delta = match side {
655 Side::Buy => price - entry,
656 Side::Sell => entry - price,
657 };
658 self.unrealized_pnl = delta * self.quantity;
659 }
660 self.updated_at = Utc::now();
661 }
662}
663
664#[derive(Clone, Debug, Deserialize, Serialize)]
666pub struct AccountBalance {
667 pub currency: String,
668 pub total: Price,
669 pub available: Price,
670 pub updated_at: DateTime<Utc>,
671}
672
673#[derive(Clone, Debug, Deserialize, Serialize)]
675pub struct Signal {
676 pub id: Uuid,
677 pub symbol: Symbol,
678 pub kind: SignalKind,
679 pub confidence: f64,
680 pub generated_at: DateTime<Utc>,
681 pub note: Option<String>,
682 pub stop_loss: Option<Price>,
683 pub take_profit: Option<Price>,
684 pub execution_hint: Option<ExecutionHint>,
685}
686
687#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
689pub enum SignalKind {
690 EnterLong,
691 ExitLong,
692 EnterShort,
693 ExitShort,
694 Flatten,
695}
696
697impl SignalKind {
698 #[must_use]
700 pub fn side(self) -> Side {
701 match self {
702 Self::EnterLong | Self::ExitShort => Side::Buy,
703 Self::EnterShort | Self::ExitLong => Side::Sell,
704 Self::Flatten => {
705 Side::Sell
709 }
710 }
711 }
712}
713
714impl Signal {
715 #[must_use]
717 pub fn new(symbol: impl Into<Symbol>, kind: SignalKind, confidence: f64) -> Self {
718 Self {
719 id: Uuid::new_v4(),
720 symbol: symbol.into(),
721 kind,
722 confidence,
723 generated_at: Utc::now(),
724 note: None,
725 stop_loss: None,
726 take_profit: None,
727 execution_hint: None,
728 }
729 }
730
731 #[must_use]
733 pub fn with_hint(mut self, hint: ExecutionHint) -> Self {
734 self.execution_hint = Some(hint);
735 self
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use rust_decimal::prelude::FromPrimitive;
743
744 #[test]
745 fn interval_duration_matches_definition() {
746 assert_eq!(Interval::OneMinute.as_duration(), Duration::minutes(1));
747 assert_eq!(Interval::FourHours.as_duration(), Duration::hours(4));
748 }
749
750 #[test]
751 fn position_mark_price_updates_unrealized_pnl() {
752 let mut position = Position {
753 symbol: "BTCUSDT".to_string(),
754 side: Some(Side::Buy),
755 quantity: Decimal::from_f64(0.5).unwrap(),
756 entry_price: Some(Decimal::from(60_000)),
757 unrealized_pnl: Decimal::ZERO,
758 updated_at: Utc::now(),
759 };
760 position.mark_price(Decimal::from(60_500));
761 assert_eq!(position.unrealized_pnl, Decimal::from(250));
762 }
763
764 #[test]
765 fn local_order_book_tracks_best_levels() {
766 let mut lob = LocalOrderBook::new();
767 lob.add_order(Side::Buy, Decimal::from(10), Decimal::from(2));
768 lob.add_order(Side::Buy, Decimal::from(11), Decimal::from(1));
769 lob.add_order(
770 Side::Sell,
771 Decimal::from(12),
772 Decimal::from_f64(1.5).unwrap(),
773 );
774 lob.add_order(Side::Sell, Decimal::from(13), Decimal::from(3));
775
776 assert_eq!(lob.best_bid(), Some((Decimal::from(11), Decimal::from(1))));
777 assert_eq!(
778 lob.best_ask(),
779 Some((Decimal::from(12), Decimal::from_f64(1.5).unwrap()))
780 );
781
782 let fills = lob.take_liquidity(Side::Buy, Decimal::from(2));
783 assert_eq!(
784 fills,
785 vec![
786 (Decimal::from(12), Decimal::from_f64(1.5).unwrap()),
787 (Decimal::from(13), Decimal::from_f64(0.5).unwrap())
788 ]
789 );
790 assert_eq!(
791 lob.best_ask(),
792 Some((Decimal::from(13), Decimal::from_f64(2.5).unwrap()))
793 );
794 }
795
796 #[test]
797 fn local_order_book_apply_delta_overwrites_level() {
798 let mut lob = LocalOrderBook::new();
799 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(1));
800 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::from(3));
801 assert_eq!(lob.best_bid(), Some((Decimal::from(100), Decimal::from(3))));
802
803 lob.apply_delta(Side::Buy, Decimal::from(100), Decimal::ZERO);
804 assert!(lob.best_bid().is_none());
805 }
806
807 #[test]
808 fn local_order_book_checksum_reflects_depth() {
809 let mut lob = LocalOrderBook::new();
810 lob.apply_delta(Side::Buy, Decimal::from(10), Decimal::from(1));
811 lob.apply_delta(Side::Buy, Decimal::from(9), Decimal::from(2));
812 lob.apply_delta(Side::Sell, Decimal::from(11), Decimal::from(1));
813 lob.apply_delta(Side::Sell, Decimal::from(12), Decimal::from(2));
814
815 let checksum_full = lob.checksum(2);
816 let checksum_partial = lob.checksum(1);
817 assert_ne!(checksum_full, checksum_partial);
818 }
819}