1use brokapi_core::Decimal;
4use std::time::Instant;
5
6use brokapi_bcs_protocol::http::models::{
7 OrderExecutionData as RestOrderExecutionData, OrderOperationResponse, OrderRecord,
8 OrderStateListResponse, OrderStatusResponse,
9};
10use brokapi_bcs_protocol::models::enums::{OrderSide, OrderStatus, OrderType as BcsOrderType};
11use brokapi_bcs_protocol::ws::models::{OrderExecutionData, OrderMessage};
12use brokapi_core::{
13 BrokerOrderId, ClientOrderId, Event, OrderAck, OrderSnapshot, OrderState, OrderType, Price,
14 Qty, Side, StopExpiry, StopOrderKind, StopOrderSnapshot, StopOrderState,
15};
16use uuid::Uuid;
17
18use super::{instrument_id_ticker, timestamps};
19
20pub fn map_place_response(resp: &OrderOperationResponse, now: Instant) -> Event {
26 Event::Order(map_operation_ack(
27 resp,
28 now,
29 OrderState::Acked {
30 cid: resp.client_order_id,
31 broker_id: BrokerOrderId::new(resp.client_order_id.hyphenated().to_string()),
32 },
33 ))
34}
35
36pub fn map_order_status(resp: &OrderStatusResponse, now: Instant) -> Option<Event> {
39 let cid = parse_cid(&resp.client_order_id)?;
40 let data = resp.data.as_ref()?;
41 Some(Event::Order(map_execution_data(cid, data, now)))
42}
43
44pub fn message_cid(msg: &OrderMessage) -> Option<ClientOrderId> {
58 use brokapi_bcs_protocol::models::enums::OrderStatus as S;
59 let client = parse_cid(&msg.client_order_id);
60 let original = msg.original_client_order_id.as_deref().and_then(parse_cid);
61 match (client, original) {
62 (Some(c), Some(o)) => {
63 let about_the_original = msg.data.as_ref().is_some_and(|d| {
64 matches!(
65 d.order_status,
66 S::Cancelled | S::Replaced | S::PendingCancel
67 )
68 });
69 Some(if about_the_original { o } else { c })
70 }
71 (c, o) => c.or(o),
72 }
73}
74
75pub fn map_ws_order_message(msg: &OrderMessage, now: Instant) -> Option<Event> {
80 let cid = message_cid(msg)?;
81 let data = msg.data.as_ref()?;
82 let original = msg
83 .original_client_order_id
84 .as_deref()
85 .unwrap_or(&msg.client_order_id);
86 Some(Event::Order(map_ws_execution_data(
87 cid, data, now, original,
88 )))
89}
90
91pub const PLAIN_LIST_TYPES: [i32; 4] = [1, 2, 3, 10];
94pub const STOP_LIST_TYPES: [i32; 5] = [4, 5, 6, 7, 11];
97pub const LIST_STATUS_ACTIVE: i32 = 3;
99
100pub fn map_order_record(
103 record: &OrderRecord,
104 cid: ClientOrderId,
105 now: Instant,
106) -> Option<OrderSnapshot> {
107 let ticker = record.ticker.as_deref()?;
108 let class_code = record.class_code.as_deref()?;
109 let instrument = instrument_id_ticker(ticker, class_code);
110 let side = map_side_int(record.side)?;
111 let order_type = map_order_type_int(record.order_type)?;
112 let qty = record.order_quantity.unwrap_or(Decimal::ZERO);
113 let filled = record.executed_quantity.unwrap_or(Decimal::ZERO);
114 let state = list_state(record, cid);
115 Some(
116 OrderSnapshot::new(
117 cid,
118 instrument,
119 state,
120 side,
121 order_type,
122 Qty::new(qty),
123 timestamps(now, None),
124 )
125 .with_broker_id(Some(BrokerOrderId::new(record.order_id.clone())))
126 .with_filled(Qty::new(filled))
127 .with_price(record.price.filter(|p| *p > Decimal::ZERO).map(Price::new)),
128 )
129}
130
131fn list_state(record: &OrderRecord, cid: ClientOrderId) -> OrderState {
134 let broker_id = BrokerOrderId::new(record.order_id.clone());
135 let executed = record.executed_quantity.unwrap_or(Decimal::ZERO);
136 match record.order_status {
137 Some(1) => OrderState::Cancelled { cid },
138 Some(2) => OrderState::Filled { cid },
139 Some(3) if executed > Decimal::ZERO => OrderState::PartiallyFilled {
140 cid,
141 filled: Qty::new(executed),
142 left: Qty::new(record.remained_quantity.unwrap_or_else(|| {
143 (record.order_quantity.unwrap_or(Decimal::ZERO) - executed).max(Decimal::ZERO)
144 })),
145 },
146 Some(3) => OrderState::Acked { cid, broker_id },
147 _ => OrderState::Unknown {
148 cid,
149 since: brokapi_runtime::now(),
150 },
151 }
152}
153
154pub fn map_stop_record(
158 record: &OrderRecord,
159 cid: Option<ClientOrderId>,
160 now: Instant,
161) -> Vec<StopOrderSnapshot> {
162 let (Some(ticker), Some(class_code)) = (record.ticker.as_deref(), record.class_code.as_deref())
163 else {
164 return Vec::new();
165 };
166 let Some(side) = map_side_int(record.side) else {
167 return Vec::new();
168 };
169 let state = match record.order_status {
170 Some(3) => StopOrderState::Active,
171 Some(2) => StopOrderState::Triggered,
173 Some(1) => StopOrderState::Cancelled,
174 other => StopOrderState::Other(format!("orderStatus={other:?}")),
175 };
176 let limit = |market_flag: Option<i32>| {
177 (market_flag == Some(2))
178 .then_some(record.price)
179 .flatten()
180 .filter(|p| *p > Decimal::ZERO)
181 .map(Price::new)
182 };
183 let mut legs: Vec<(StopOrderKind, Decimal, Option<Price>)> = Vec::new();
184 match record.order_type {
185 Some(4) => legs.push((
187 StopOrderKind::StopLimit,
188 record.stop_price.unwrap_or(Decimal::ZERO),
189 record.price.filter(|p| *p > Decimal::ZERO).map(Price::new),
190 )),
191 Some(6) => legs.push((
192 StopOrderKind::StopLoss,
193 record.stop_price.unwrap_or(Decimal::ZERO),
194 limit(record.market_stop_loss),
195 )),
196 Some(5) | Some(11) => legs.push((
197 StopOrderKind::TakeProfit,
198 record.take_price.unwrap_or(Decimal::ZERO),
199 limit(record.market_take_profit),
200 )),
201 Some(7) => {
202 legs.push((
203 StopOrderKind::StopLoss,
204 record.stop_price.unwrap_or(Decimal::ZERO),
205 limit(record.market_stop_loss),
206 ));
207 legs.push((
208 StopOrderKind::TakeProfit,
209 record.take_price.unwrap_or(Decimal::ZERO),
210 limit(record.market_take_profit),
211 ));
212 }
213 _ => return Vec::new(),
214 }
215 legs.into_iter()
216 .map(|(kind, stop_price, limit_price)| StopOrderSnapshot {
217 cid,
218 broker_id: BrokerOrderId::new(record.order_id.clone()),
219 instrument: instrument_id_ticker(ticker, class_code),
220 side,
221 kind,
222 qty: Qty::new(record.order_quantity.unwrap_or(Decimal::ZERO)),
223 stop_price: Price::new(stop_price),
224 limit_price,
225 expiry: StopExpiry::GoodTillCancel,
227 state: state.clone(),
228 ts: timestamps(now, None),
229 })
230 .collect()
231}
232
233pub fn map_state_list(
237 known_cid: Option<ClientOrderId>,
238 resp: &OrderStateListResponse,
239 now: Instant,
240) -> Option<OrderSnapshot> {
241 let data = resp.data.iter().max_by_key(|d| d.transaction_time)?;
243 let cid = known_cid
244 .or_else(|| parse_cid(&resp.client_order_id))
245 .or_else(|| resp.original_client_order_id.as_deref().and_then(parse_cid))?;
246 Some(map_execution_data(cid, data, now))
247}
248
249pub fn rejection_reason(snap: &OrderSnapshot) -> Option<&str> {
251 match &snap.state {
252 OrderState::Rejected { reason, .. } => Some(reason.as_str()),
253 _ => None,
254 }
255}
256
257fn map_operation_ack(
258 resp: &OrderOperationResponse,
259 now: Instant,
260 state: OrderState,
261) -> OrderSnapshot {
262 OrderSnapshot::new(
263 resp.client_order_id,
264 instrument_id_ticker("", ""),
265 state,
266 Side::Buy,
267 OrderType::Limit,
268 Qty::ZERO,
269 timestamps(now, None),
270 )
271 .with_broker_id(Some(BrokerOrderId::new(
272 resp.client_order_id.hyphenated().to_string(),
273 )))
274}
275
276fn map_ws_execution_data(
277 cid: ClientOrderId,
278 data: &OrderExecutionData,
279 now: Instant,
280 original_cid: &str,
281) -> OrderSnapshot {
282 let instrument = instrument_id_ticker(&data.ticker, &data.class_code);
283 let state = map_bcs_status(data, cid);
284 let side = map_bcs_side(data.side);
285 let order_type = data
286 .order_type
287 .map(map_bcs_order_type)
288 .unwrap_or(OrderType::Limit);
289 let qty = Qty::new(
290 data.order_quantity
291 .unwrap_or(data.last_quantity + data.remained_quantity),
292 );
293 let filled = Qty::new(data.executed_quantity.unwrap_or(data.last_quantity));
294 let broker_ts = data.transaction_time.map(super::unix_nanos);
295 let _original = parse_cid(original_cid);
296
297 OrderSnapshot::new(
298 cid,
299 instrument,
300 state,
301 side,
302 order_type,
303 qty,
304 timestamps(now, broker_ts),
305 )
306 .with_broker_id(
307 data.order_id
308 .as_ref()
309 .map(|id| BrokerOrderId::new(id.clone())),
310 )
311 .with_filled(filled)
312 .with_price(Some(Price::new(data.price)))
313}
314
315fn map_execution_data(
316 cid: ClientOrderId,
317 data: &RestOrderExecutionData,
318 now: Instant,
319) -> OrderSnapshot {
320 let ticker = data.ticker.as_deref().unwrap_or("");
321 let class_code = data.class_code.as_deref().unwrap_or("");
322 let instrument = instrument_id_ticker(ticker, class_code);
323 let state = map_rest_status(data, cid);
324 let side = data.side.as_deref().map(map_side_str).unwrap_or(Side::Buy);
325 let order_type = data
326 .order_type
327 .as_deref()
328 .map(map_order_type_str)
329 .unwrap_or(OrderType::Limit);
330 let qty = Qty::new(data.order_quantity.unwrap_or(Decimal::ZERO));
331 let filled = Qty::new(data.executed_quantity.unwrap_or(Decimal::ZERO));
332
333 OrderSnapshot::new(
334 cid,
335 instrument,
336 state,
337 side,
338 order_type,
339 qty,
340 timestamps(now, data.transaction_time.map(super::unix_nanos)),
341 )
342 .with_broker_id(
343 data.order_id
344 .as_ref()
345 .filter(|id| !id.is_empty())
346 .map(|id| BrokerOrderId::new(id.clone())),
347 )
348 .with_filled(filled)
349 .with_price(data.price.filter(|p| *p > Decimal::ZERO).map(Price::new))
350}
351
352fn parse_cid(raw: &str) -> Option<ClientOrderId> {
353 Uuid::parse_str(raw).ok()
354}
355
356fn map_bcs_status(data: &OrderExecutionData, cid: ClientOrderId) -> OrderState {
357 let broker_id = BrokerOrderId::new(
358 data.order_id
359 .clone()
360 .filter(|id| !id.is_empty())
361 .unwrap_or_else(|| cid.hyphenated().to_string()),
362 );
363 match data.order_status {
364 OrderStatus::New
365 | OrderStatus::PendingConfirmation
366 | OrderStatus::Replaced
367 | OrderStatus::PendingReplace => OrderState::Acked { cid, broker_id },
368 OrderStatus::PartiallyFilled => {
369 let filled = data.executed_quantity.unwrap_or(data.last_quantity);
370 OrderState::PartiallyFilled {
371 cid,
372 filled: Qty::new(filled),
373 left: Qty::new(data.remained_quantity),
374 }
375 }
376 OrderStatus::Filled => OrderState::Filled { cid },
377 OrderStatus::Cancelled => OrderState::Cancelled { cid },
378 OrderStatus::PendingCancel => OrderState::Unknown {
381 cid,
382 since: brokapi_runtime::now(),
383 },
384 OrderStatus::Rejected => OrderState::Rejected {
385 cid,
386 reason: "rejected".into(),
387 },
388 OrderStatus::Unknown => OrderState::Unknown {
389 cid,
390 since: brokapi_runtime::now(),
391 },
392 }
393}
394
395fn map_rest_status(data: &RestOrderExecutionData, cid: ClientOrderId) -> OrderState {
400 let broker_id = BrokerOrderId::new(
401 data.order_id
402 .clone()
403 .filter(|id| !id.is_empty())
404 .unwrap_or_else(|| cid.hyphenated().to_string()),
405 );
406 match data.order_status.as_deref() {
407 Some("0") | Some("NEW") | Some("5") | Some("9") | Some("10") => {
408 OrderState::Acked { cid, broker_id }
409 }
410 Some("1") => {
411 let filled = data.executed_quantity.unwrap_or(Decimal::ZERO);
412 OrderState::PartiallyFilled {
413 cid,
414 filled: Qty::new(filled),
415 left: Qty::new(data.remained_quantity.unwrap_or_else(|| {
416 (data.order_quantity.unwrap_or(Decimal::ZERO) - filled).max(Decimal::ZERO)
417 })),
418 }
419 }
420 Some("2") => OrderState::Filled { cid },
421 Some("4") => OrderState::Cancelled { cid },
422 Some("6") => OrderState::Unknown {
423 cid,
424 since: brokapi_runtime::now(),
425 },
426 Some("8") => OrderState::Rejected {
427 cid,
428 reason: data
429 .reject_reason
430 .clone()
431 .filter(|r| !r.is_empty())
432 .unwrap_or_else(|| "rejected".into()),
433 },
434 _ => OrderState::Unknown {
435 cid,
436 since: brokapi_runtime::now(),
437 },
438 }
439}
440
441fn map_bcs_side(side: OrderSide) -> Side {
442 match side {
443 OrderSide::Buy => Side::Buy,
444 OrderSide::Sell => Side::Sell,
445 OrderSide::Unknown => Side::Buy,
446 }
447}
448
449fn map_bcs_order_type(t: BcsOrderType) -> OrderType {
450 match t {
451 BcsOrderType::Market => OrderType::Market,
452 BcsOrderType::Limit => OrderType::Limit,
453 BcsOrderType::Unknown => OrderType::Limit,
454 }
455}
456
457fn map_side_int(v: Option<i32>) -> Option<Side> {
458 match v {
459 Some(1) => Some(Side::Buy),
460 Some(2) => Some(Side::Sell),
461 _ => None,
462 }
463}
464
465fn map_order_type_int(v: Option<i32>) -> Option<OrderType> {
468 match v {
469 Some(1) => Some(OrderType::Market),
470 Some(2) | Some(3) | Some(10) => Some(OrderType::Limit),
471 _ => None,
472 }
473}
474
475fn map_side_str(raw: &str) -> Side {
476 match raw {
477 "1" | "Buy" | "BUY" => Side::Buy,
478 _ => Side::Sell,
479 }
480}
481
482fn map_order_type_str(raw: &str) -> OrderType {
483 match raw {
484 "1" | "Market" => OrderType::Market,
485 _ => OrderType::Limit,
486 }
487}
488
489pub fn map_order_ack(cid: ClientOrderId, broker_id: BrokerOrderId, now: Instant) -> OrderAck {
492 OrderAck {
493 cid,
494 broker_id,
495 ts: timestamps(now, None),
496 }
497}