1use brokapi_core::Decimal;
4use std::time::Instant;
5
6use brokapi_bcs_protocol::http::models::TradeRecord;
7use brokapi_bcs_protocol::models::enums::ExecutionType;
8use brokapi_bcs_protocol::ws::models::OrderMessage;
9use brokapi_core::{BrokerOrderId, Execution, Money, Price, Qty, Side};
10use uuid::Uuid;
11
12use super::{instrument_id_ticker, parse_broker_ts, timestamps, unix_nanos};
13
14pub fn map_trade_record(record: &TradeRecord, now: Instant) -> Option<Execution> {
18 let Some(data) = record.data.as_ref() else {
19 return map_flat_trade(record, now);
20 };
21 let ticker = data.ticker.as_deref()?;
22 let class_code = data.class_code.as_deref()?;
23 let order_id = data.order_id.as_ref()?;
24 let execution_id = data.execution_id.as_ref()?;
25 let price = data.price?;
26 let qty = data
27 .last_quantity
28 .or(data.executed_quantity)
29 .unwrap_or(Decimal::ZERO);
30 let side = match data.side.as_deref() {
31 Some("1") | Some("Buy") | Some("BUY") => Side::Buy,
32 Some("2") | Some("Sell") | Some("SELL") => Side::Sell,
33 _ => return None,
35 };
36 let cid = record
37 .client_order_id
38 .as_deref()
39 .and_then(|s| Uuid::parse_str(s).ok());
40 let broker_ts = data
41 .transaction_time
42 .as_deref()
43 .and_then(|s| parse_broker_ts(Some(s)));
44
45 Some(Execution {
46 cid,
47 broker_order_id: BrokerOrderId::new(order_id.clone()),
48 instrument: instrument_id_ticker(ticker, class_code),
49 side,
50 price: Price::new(price),
51 qty: Qty::new(qty),
52 commission: Money::new(data.commission.unwrap_or(Decimal::ZERO)),
53 trade_id: execution_id.clone(),
54 ts: timestamps(now, broker_ts),
55 })
56}
57
58fn map_flat_trade(record: &TradeRecord, now: Instant) -> Option<Execution> {
59 let ticker = record.ticker.as_deref()?;
60 let class_code = record.class_code.as_deref()?;
61 let trade_num = record.trade_num?;
62 let price = record.price?;
63 let qty = record.trade_quantity?;
64 if qty <= Decimal::ZERO {
65 return None;
66 }
67 let side = match record.side.as_deref() {
69 Some("1") => Side::Buy,
70 Some("2") => Side::Sell,
71 _ => return None,
72 };
73 let broker_ts = record
74 .trade_date_time
75 .as_deref()
76 .and_then(|s| parse_broker_ts(Some(s)));
77 Some(Execution {
78 cid: None,
79 broker_order_id: BrokerOrderId::new(
80 record.order_num.map(|n| n.to_string()).unwrap_or_default(),
81 ),
82 instrument: instrument_id_ticker(ticker, class_code),
83 side,
84 price: Price::new(price),
85 qty: Qty::new(qty),
86 commission: Money::ZERO,
87 trade_id: trade_num.to_string(),
88 ts: timestamps(now, broker_ts),
89 })
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct Cumulative {
101 pub qty: Decimal,
103 pub value: Decimal,
106}
107
108pub fn cumulative_of(
111 data: &brokapi_bcs_protocol::ws::models::OrderExecutionData,
112) -> Option<Cumulative> {
113 let qty = data.executed_quantity?;
114 let value = match (data.execution_value, data.average_price) {
115 (Some(v), _) if v > Decimal::ZERO => v,
116 (_, Some(avg)) if avg > Decimal::ZERO => avg * qty,
117 _ => return None,
118 };
119 Some(Cumulative { qty, value })
120}
121
122pub fn map_ws_execution(
139 msg: &OrderMessage,
140 now: Instant,
141 before: Option<Cumulative>,
142) -> Option<Execution> {
143 let data = msg.data.as_ref()?;
144 if data.execution_type == Some(ExecutionType::Corrected) {
147 return None;
148 }
149 let qty = data.last_quantity;
150 if qty <= Decimal::ZERO {
151 return None;
152 }
153 let execution_id = data.execution_id.as_deref().filter(|s| !s.is_empty())?;
154 let cid = super::order::message_cid(msg);
155 let exact = cumulative_of(data).and_then(|after| {
156 let base = match before {
157 Some(b) => b,
158 None if after.qty == qty => Cumulative {
160 qty: Decimal::ZERO,
161 value: Decimal::ZERO,
162 },
163 None => return None,
164 };
165 (after.qty - base.qty == qty && after.value > base.value)
166 .then(|| (after.value - base.value) / qty)
167 });
168 let price = exact.unwrap_or_else(|| match data.average_price {
169 Some(avg) if avg > Decimal::ZERO => avg,
170 _ => data.price,
171 });
172 let side = match data.side {
174 brokapi_bcs_protocol::models::enums::OrderSide::Buy => Side::Buy,
175 brokapi_bcs_protocol::models::enums::OrderSide::Sell => Side::Sell,
176 brokapi_bcs_protocol::models::enums::OrderSide::Unknown => return None,
177 };
178 Some(Execution {
179 cid,
180 broker_order_id: BrokerOrderId::new(data.order_id.clone().unwrap_or_default()),
181 instrument: instrument_id_ticker(&data.ticker, &data.class_code),
182 side,
183 price: Price::new(price),
184 qty: Qty::new(qty),
185 commission: Money::new(data.commission.unwrap_or(Decimal::ZERO)),
186 trade_id: execution_id.to_string(),
187 ts: timestamps(now, data.transaction_time.map(unix_nanos)),
188 })
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn record(side: Option<&str>) -> TradeRecord {
196 serde_json::from_value(serde_json::json!({
197 "data": {
198 "ticker": "SBER", "classCode": "TQBR", "orderId": "o1", "executionId": "e1",
199 "price": 100.0, "lastQuantity": 2.0, "side": side
200 }
201 }))
202 .unwrap()
203 }
204
205 #[test]
206 fn history_trade_side_of_the_older_form_is_read_and_never_guessed() {
207 let now = Instant::now();
208 assert_eq!(
209 map_trade_record(&record(Some("BUY")), now).unwrap().side,
210 Side::Buy
211 );
212 assert_eq!(
213 map_trade_record(&record(Some("2")), now).unwrap().side,
214 Side::Sell
215 );
216 assert!(map_trade_record(&record(None), now).is_none());
217 assert!(map_trade_record(&record(Some("???")), now).is_none());
218 }
219}