1use std::time::Instant;
4
5use brokapi_alor_protocol::http::models::OrderRecord;
6use brokapi_core::{
7 BrokerOrderId, ClientOrderId, OrderAck, OrderSnapshot, OrderState, OrderType, Price, Qty,
8};
9
10use super::market::side_of;
11use super::{parse_time, timestamps};
12use crate::util::instrument_id;
13
14pub fn state_of(
25 status: Option<&str>,
26 cid: ClientOrderId,
27 broker_id: &str,
28 qty: Qty,
29 filled: Qty,
30) -> OrderState {
31 match status.map(str::to_ascii_lowercase).as_deref() {
32 Some("filled") => OrderState::Filled { cid },
33 Some("canceled") | Some("cancelled") => OrderState::Cancelled { cid },
34 Some("rejected") => OrderState::Rejected {
35 cid,
36 reason: "rejected by the broker or the exchange".to_string(),
37 },
38 _ if filled.is_positive() => OrderState::PartiallyFilled {
39 cid,
40 filled,
41 left: Qty::new((qty.decimal() - filled.decimal()).max(brokapi_core::Decimal::ZERO)),
42 },
43 _ => OrderState::Acked {
44 cid,
45 broker_id: BrokerOrderId::new(broker_id),
46 },
47 }
48}
49
50pub fn order_snapshot(r: &OrderRecord, cid: ClientOrderId, now: Instant) -> Option<OrderSnapshot> {
52 let qty = Qty::new(r.qty_units?);
53 let filled = Qty::new(r.filled_qty_units.unwrap_or_default());
54 let side = side_of(r.side.as_deref())?;
55 let order_type = match r.kind.as_deref() {
56 Some("market") => OrderType::Market,
57 _ => OrderType::Limit,
58 };
59 let instrument = instrument_id(&r.symbol, r.board.as_deref().unwrap_or_default());
60 let broker_ts =
61 parse_time(r.update_time.as_deref()).or_else(|| parse_time(r.trans_time.as_deref()));
62 Some(
63 OrderSnapshot::new(
64 cid,
65 instrument,
66 state_of(r.status.as_deref(), cid, &r.id, qty, filled),
67 side,
68 order_type,
69 qty,
70 timestamps(now, broker_ts),
71 )
72 .with_broker_id(Some(BrokerOrderId::new(r.id.clone())))
73 .with_filled(filled)
74 .with_price(r.price.filter(|p| !p.is_zero()).map(Price::new)),
75 )
76}
77
78pub fn order_ack(cid: ClientOrderId, order_number: &str, now: Instant) -> OrderAck {
80 OrderAck {
81 cid,
82 broker_id: BrokerOrderId::new(order_number),
83 ts: timestamps(now, None),
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use uuid::Uuid;
91
92 fn rec(json: &str) -> OrderRecord {
93 serde_json::from_str(json).unwrap()
94 }
95
96 #[test]
97 fn working_orders_are_acked_or_partly_filled() {
98 let cid = Uuid::new_v4();
99 let r = rec(
100 r#"{"id":"77","symbol":"SBER","board":"TQBR","type":"limit","side":"buy","status":"working","qtyUnits":20,"filledQtyUnits":0,"price":100.5}"#,
101 );
102 let s = order_snapshot(&r, cid, Instant::now()).unwrap();
103 assert!(matches!(s.state, OrderState::Acked { .. }));
104 assert_eq!(s.price.unwrap().to_string(), "100.5");
105 assert_eq!(s.broker_id.unwrap().as_str(), "77");
106
107 let r = rec(
108 r#"{"id":"77","symbol":"SBER","board":"TQBR","type":"limit","side":"buy","status":"working","qtyUnits":20,"filledQtyUnits":10}"#,
109 );
110 let s = order_snapshot(&r, cid, Instant::now()).unwrap();
111 match s.state {
112 OrderState::PartiallyFilled { filled, left, .. } => {
113 assert_eq!((filled, left), (Qty::from(10), Qty::from(10)));
114 }
115 other => panic!("{other:?}"),
116 }
117 }
118
119 #[test]
120 fn final_states_follow_the_status() {
121 let cid = Uuid::new_v4();
122 let st = |s: &str| state_of(Some(s), cid, "1", Qty::from(1), Qty::ZERO);
123 assert!(matches!(st("filled"), OrderState::Filled { .. }));
124 assert!(matches!(st("canceled"), OrderState::Cancelled { .. }));
125 assert!(matches!(st("rejected"), OrderState::Rejected { .. }));
126 assert!(matches!(st("something_new"), OrderState::Acked { .. }));
128 }
129
130 #[test]
131 fn a_record_without_pieces_is_skipped() {
132 let r = rec(r#"{"id":"1","symbol":"SBER","side":"buy","status":"working","qty":2}"#);
133 assert!(order_snapshot(&r, Uuid::new_v4(), Instant::now()).is_none());
134 }
135}