1use std::time::Instant;
4
5use brokapi_core::{
6 BrokerOrderId, ClientOrderId, Decimal, Execution, InstrumentId, Money, Price, Qty,
7};
8use brokapi_tbank_protocol::v1 as pb;
9
10use crate::ids::{id_from_parts, money_of, price_of_money, price_of_quotation, stamps};
11use crate::map::order::side;
12
13pub fn from_order_trades(
16 t: &pb::OrderTrades,
17 instrument: InstrumentId,
18 cid: Option<ClientOrderId>,
19 now: Instant,
20) -> Vec<Execution> {
21 t.trades
22 .iter()
23 .map(|tr| Execution {
24 cid,
25 broker_order_id: BrokerOrderId::new(t.order_id.clone()),
26 instrument: instrument.clone(),
27 side: side(t.direction()),
28 price: tr
29 .price
30 .as_ref()
31 .map(price_of_quotation)
32 .unwrap_or(Price::ZERO),
33 qty: Qty::from(tr.quantity),
34 commission: Money::ZERO,
35 trade_id: tr.trade_id.clone(),
36 ts: stamps(now, tr.date_time.as_ref().or(t.created_at.as_ref())),
37 })
38 .collect()
39}
40
41pub fn from_operation(op: &pb::OperationItem, now: Instant) -> Vec<Execution> {
46 use pb::OperationType as T;
47 let side = match op.r#type() {
48 T::Buy | T::BuyCard | T::BuyMargin => brokapi_core::Side::Buy,
49 T::Sell | T::SellCard | T::SellMargin => brokapi_core::Side::Sell,
50 _ => return Vec::new(),
51 };
52 let Some(info) = &op.trades_info else {
53 return Vec::new();
54 };
55 let instrument = id_from_parts(&op.instrument_uid, &op.figi, &op.ticker, &op.class_code);
56 let total_qty: i64 = info.trades.iter().map(|t| t.quantity).sum();
57 let commission = op
58 .commission
59 .as_ref()
60 .map(money_of)
61 .unwrap_or(Money::ZERO)
62 .abs();
63 let mut left = commission;
66 let last = info.trades.len().saturating_sub(1);
67 info.trades
68 .iter()
69 .enumerate()
70 .map(|(i, tr)| {
71 let share = if total_qty <= 0 {
72 Money::ZERO
73 } else if i == last {
74 left
75 } else {
76 let part =
77 commission.decimal() * Decimal::from(tr.quantity) / Decimal::from(total_qty);
78 Money::new(part.trunc_with_scale(9))
79 };
80 left -= share;
81 (tr, share)
82 })
83 .map(|(tr, share)| Execution {
84 cid: None,
85 broker_order_id: BrokerOrderId::new(op.id.clone()),
86 instrument: instrument.clone(),
87 side,
88 price: tr.price.as_ref().map(price_of_money).unwrap_or(Price::ZERO),
89 qty: Qty::from(tr.quantity),
90 commission: share,
91 trade_id: tr.num.clone(),
92 ts: stamps(now, tr.date.as_ref()),
93 })
94 .collect()
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use brokapi_core::dec;
101 use brokapi_core::{InstrumentKey, NativeId, Side};
102 use brokapi_tbank_protocol::money::{money, quotation};
103
104 #[test]
105 fn stream_trades_one_execution_each() {
106 let t = pb::OrderTrades {
107 order_id: "55".into(),
108 direction: pb::OrderDirection::Sell as i32,
109 trades: vec![
110 pb::OrderTrade {
111 price: Some(quotation("100.5")),
112 quantity: 2,
113 trade_id: "t1".into(),
114 ..Default::default()
115 },
116 pb::OrderTrade {
117 price: Some(quotation("100.6")),
118 quantity: 3,
119 trade_id: "t2".into(),
120 ..Default::default()
121 },
122 ],
123 ..Default::default()
124 };
125 let inst = InstrumentId::new(
126 InstrumentKey::new("SBER", "TQBR"),
127 NativeId::Uid("u".into()),
128 );
129 let cid = ClientOrderId::from_bytes([7; 16]);
130 let ex = from_order_trades(&t, inst, Some(cid), Instant::now());
131 assert_eq!(ex.len(), 2);
132 assert_eq!(
133 (ex[0].side, ex[0].price.decimal(), ex[0].qty.decimal()),
134 (Side::Sell, dec!(100.5), dec!(2.0))
135 );
136 assert_eq!(ex[1].trade_id, "t2");
137 assert_eq!(ex[0].cid, Some(cid));
138 assert_eq!(ex[0].broker_order_id.as_str(), "55");
139 }
140
141 #[test]
142 fn operation_commission_is_spread_by_quantity() {
143 let op = pb::OperationItem {
144 id: "op1".into(),
145 r#type: pb::OperationType::Buy as i32,
146 ticker: "SBER".into(),
147 class_code: "TQBR".into(),
148 instrument_uid: "u".into(),
149 commission: Some(money("rub", "-4.0")),
150 trades_info: Some(pb::OperationItemTrades {
151 trades: vec![
152 pb::OperationItemTrade {
153 num: "a".into(),
154 quantity: 1,
155 price: Some(money("rub", "10.0")),
156 ..Default::default()
157 },
158 pb::OperationItemTrade {
159 num: "b".into(),
160 quantity: 3,
161 price: Some(money("rub", "11.0")),
162 ..Default::default()
163 },
164 ],
165 }),
166 ..Default::default()
167 };
168 let ex = from_operation(&op, Instant::now());
169 assert_eq!(ex.len(), 2);
170 assert_eq!(
171 (ex[0].commission.decimal(), ex[1].commission.decimal()),
172 (dec!(1.0), dec!(3.0))
173 );
174 assert_eq!(ex[0].commission + ex[1].commission, Money::new(dec!(4)));
176 assert_eq!(ex[0].side, Side::Buy);
177 }
178
179 #[test]
180 fn non_trade_operations_are_skipped() {
181 let op = pb::OperationItem {
182 r#type: pb::OperationType::Dividend as i32,
183 ..Default::default()
184 };
185 assert!(from_operation(&op, Instant::now()).is_empty());
186 }
187}