brokapi_finam/map/
order.rs1use std::time::Instant;
4
5use brokapi_core::CidCodec;
6use brokapi_core::{
7 BrokerOrderId, ClientOrderId, Error, InstrumentId, NativeCid, OrderSnapshot, OrderState,
8 OrderType, PlaceOrderRequest, Qty, Side,
9};
10use brokapi_finam_protocol::pb::grpc::tradeapi::v1::Side as PbSide;
11use brokapi_finam_protocol::pb::grpc::tradeapi::v1::orders as pb;
12use brokapi_finam_protocol::pb::grpc::tradeapi::v1::orders::{
13 OrderStatus, OrderType as PbOrderType,
14};
15
16use crate::cid::FinamCidCodec;
17use crate::util::{instrument_from_symbol, price, qty, qty0, symbol_of, timestamps, to_decimal};
18
19pub fn is_active(status: i32) -> bool {
21 !matches!(
22 OrderStatus::try_from(status),
23 Ok(OrderStatus::Filled
24 | OrderStatus::Canceled
25 | OrderStatus::Replaced
26 | OrderStatus::Rejected
27 | OrderStatus::Expired
28 | OrderStatus::Failed
29 | OrderStatus::DeniedByBroker
30 | OrderStatus::RejectedByExchange
31 | OrderStatus::Executed
32 | OrderStatus::Disabled
33 | OrderStatus::SlExecuted
34 | OrderStatus::TpExecuted)
35 )
36}
37
38pub fn map_status(
40 status: i32,
41 cid: ClientOrderId,
42 broker_id: &str,
43 filled: Qty,
44 left: Qty,
45 description: &str,
46) -> OrderState {
47 use OrderStatus as S;
48 let st = S::try_from(status).unwrap_or(S::Unspecified);
49 match st {
50 S::Filled | S::Executed | S::SlExecuted | S::TpExecuted => OrderState::Filled { cid },
51 S::Canceled | S::Replaced | S::Expired | S::Disabled => OrderState::Cancelled { cid },
52 S::Rejected | S::Failed | S::DeniedByBroker | S::RejectedByExchange => {
53 OrderState::Rejected {
54 cid,
55 reason: if description.is_empty() {
56 format!("{st:?}")
57 } else {
58 format!("{st:?}: {description}")
59 },
60 }
61 }
62 S::PartiallyFilled => OrderState::PartiallyFilled { cid, filled, left },
63 _ if filled.is_positive() && left.is_positive() => {
65 OrderState::PartiallyFilled { cid, filled, left }
66 }
67 _ => OrderState::Acked {
68 cid,
69 broker_id: BrokerOrderId::new(broker_id),
70 },
71 }
72}
73
74pub fn side_from_pb(s: i32) -> Side {
77 match PbSide::try_from(s) {
78 Ok(PbSide::Sell) => Side::Sell,
79 _ => Side::Buy,
80 }
81}
82
83pub fn order_type_from_pb(t: i32) -> OrderType {
85 match PbOrderType::try_from(t) {
86 Ok(PbOrderType::Market | PbOrderType::Stop) => OrderType::Market,
87 _ => OrderType::Limit,
88 }
89}
90
91pub fn map_order_state(os: &pb::OrderState, cid: ClientOrderId, now: Instant) -> OrderSnapshot {
93 let order = os.order.clone().unwrap_or_default();
94 let filled = qty0(&os.executed_quantity);
95 let left = qty0(&os.remaining_quantity);
96 let total = qty(&os.initial_quantity)
97 .filter(|q| q.is_positive())
98 .or_else(|| qty(&order.quantity))
99 .unwrap_or(filled + left);
100 let description = os.status_description.clone().unwrap_or_default();
101 let limit = price(&order.limit_price).filter(|p| p.is_positive());
102 OrderSnapshot::new(
103 cid,
104 instrument_from_symbol(&order.symbol),
105 map_status(os.status, cid, &os.order_id, filled, left, &description),
106 side_from_pb(order.side),
107 order_type_from_pb(order.r#type),
108 total,
109 timestamps(os.accept_at.as_ref().or(os.transact_at.as_ref()), now),
110 )
111 .with_broker_id((!os.order_id.is_empty()).then(|| BrokerOrderId::new(os.order_id.clone())))
112 .with_filled(filled)
113 .with_price(limit)
114}
115
116pub fn cid_from_comment(comment: &str) -> Option<ClientOrderId> {
118 FinamCidCodec.decode(&NativeCid::new(comment))
119}
120
121pub fn build_place_order(account_id: &str, req: &PlaceOrderRequest) -> Result<pb::Order, Error> {
123 let codec = FinamCidCodec;
124 let symbol = symbol_of(&req.instrument)?;
125 if !req.qty.is_positive() {
126 return Err(Error::Rejected {
127 code: "invalid_qty".into(),
128 message: format!("quantity must be positive, got {}", req.qty),
129 });
130 }
131 let quantity = to_decimal(req.qty.decimal());
132 let (ty, limit_price) = match req.order_type {
133 OrderType::Market => (PbOrderType::Market, None),
134 OrderType::Limit => {
135 let p = req.price.ok_or_else(|| Error::Rejected {
136 code: "missing_price".into(),
137 message: "limit order requires price".into(),
138 })?;
139 (PbOrderType::Limit, Some(to_decimal(p.decimal())))
140 }
141 };
142 let comment = codec
143 .encode(req.cid)
144 .map_err(|e| Error::Config(brokapi_core::ConfigError::Message(e.to_string())))?;
145 Ok(pb::Order {
146 account_id: account_id.to_string(),
147 symbol,
148 quantity: Some(quantity),
149 side: match req.side {
150 Side::Buy => PbSide::Buy as i32,
151 Side::Sell => PbSide::Sell as i32,
152 },
153 r#type: ty as i32,
154 time_in_force: pb::TimeInForce::Day as i32,
155 limit_price,
156 client_order_id: codec.client_order_id(req.cid),
157 comment: comment.0,
158 ..Default::default()
159 })
160}
161
162pub fn rejection_of(os: &pb::OrderState) -> Option<Error> {
164 match OrderStatus::try_from(os.status) {
165 Ok(
166 s @ (OrderStatus::Rejected
167 | OrderStatus::Failed
168 | OrderStatus::DeniedByBroker
169 | OrderStatus::RejectedByExchange),
170 ) => Some(Error::Rejected {
171 code: format!("{s:?}").to_lowercase(),
172 message: os.status_description.clone().unwrap_or_default(),
173 }),
174 _ => None,
175 }
176}
177
178pub fn order_instrument(os: &pb::OrderState) -> InstrumentId {
180 instrument_from_symbol(os.order.as_ref().map(|o| o.symbol.as_str()).unwrap_or(""))
181}