1use crate::wire::{
9 DepthMarketData, ErrorField, InputOrderAction, InstrumentField, InvestorPositionField,
10 OrderField, TradeField, TradingAccountField,
11};
12use ek_data::account::AccountData;
13use ek_data::constant::{Asset, Direction, Exchange, InstrumentType, Offset, OrderType, Status};
14use ek_data::contract::ContractData;
15use ek_data::order::OrderData;
16use ek_data::tick::TickData;
17use ek_data::time::DtNs;
18use ek_data::trade::TradeData;
19use std::time::{SystemTime, UNIX_EPOCH};
20
21pub const MAX_FLOAT: f64 = f64::MAX;
26
27const OST_ALL_TRADED: &str = "0";
30const OST_PART_TRADED_Q: &str = "1";
31const OST_NO_TRADE_Q: &str = "3";
32const OST_PART_TRADED_NQ: &str = "2";
33const OST_NO_TRADE_NQ: &str = "4";
34const OST_CANCELED: &str = "5";
35const OST_UNKNOWN: &str = "a";
36
37const D_BUY: &str = "0";
40const D_SELL: &str = "1";
41#[allow(dead_code)]
44const PD_NET: &str = "1";
45const PD_LONG: &str = "2";
46const PD_SHORT: &str = "3";
47
48const OFEN_OPEN: &str = "0";
51const OFEN_CLOSE: &str = "1";
52const OFEN_CLOSE_TODAY: &str = "3";
53const OFEN_CLOSE_YESTERDAY: &str = "4";
54
55const OPT_ANY_PRICE: &str = "1";
57const OPT_LIMIT_PRICE: &str = "2";
58
59const TC_IOC: &str = "1";
61const TC_GFD: &str = "3";
62
63const VC_AV: &str = "1";
65const VC_CV: &str = "3";
66
67const PC_FUTURES: &str = "1";
70const PC_OPTIONS: &str = "2";
71const PC_COMBINATION: &str = "3";
79const PC_SPOT_OPTION: &str = "6";
80const PC_TAS: &str = "7";
82
83#[derive(Debug, Clone, Default, PartialEq, Eq)]
95pub struct ProductClasses(pub Vec<String>);
96
97impl ProductClasses {
98 pub fn default_classes() -> Vec<String> {
100 vec![PC_FUTURES.to_string()]
101 }
102
103 pub fn normalize(&self) -> Vec<String> {
106 let mut out: Vec<String> = self
107 .0
108 .iter()
109 .map(|c| c.trim().to_string())
110 .filter(|c| c.chars().count() == 1 && !c.is_empty())
111 .collect();
112 out.sort();
113 out.dedup();
114 if out.is_empty() {
115 return Self::default_classes();
116 }
117 out
118 }
119
120 pub fn accepts(&self, product_class: &str) -> bool {
122 self.normalize().iter().any(|c| c == product_class)
123 }
124}
125
126pub fn exchange_of(exchange_id: &str) -> Option<Exchange> {
131 match exchange_id {
132 "CFFEX" => Some(Exchange::CFFEX),
133 "SHFE" => Some(Exchange::SHFE),
134 "CZCE" => Some(Exchange::CZCE),
135 "DCE" => Some(Exchange::DCE),
136 "INE" => Some(Exchange::INE),
137 "GFEX" => Some(Exchange::GFEX),
138 _ => None,
139 }
140}
141
142pub fn asset_from(product_class: &str) -> Option<Asset> {
152 match product_class {
153 PC_FUTURES | PC_COMBINATION | PC_TAS => Some(Asset::FUTURES),
154 PC_OPTIONS | PC_SPOT_OPTION => Some(Asset::OPTION),
155 _ => None,
156 }
157}
158
159pub fn instrument_type_from(product_class: &str) -> Option<InstrumentType> {
163 match product_class {
164 PC_COMBINATION => Some(InstrumentType::SPREAD),
165 PC_TAS => Some(InstrumentType::FUTURES_CONTRACT),
167 _ => None,
168 }
169}
170
171pub fn status_of(code: &str) -> Option<Status> {
174 match code {
175 OST_NO_TRADE_Q => Some(Status::NOTTRADED),
176 OST_PART_TRADED_Q => Some(Status::PARTTRADED),
177 OST_ALL_TRADED => Some(Status::ALLTRADED),
178 OST_PART_TRADED_NQ => Some(Status::PARTTRADED),
179 OST_NO_TRADE_NQ => Some(Status::NOTTRADED),
180 OST_CANCELED => Some(Status::CANCELLED),
181 OST_UNKNOWN => Some(Status::SUBMITTING),
182 _ => None,
183 }
184}
185
186pub fn direction_from(code: &str) -> Option<Direction> {
188 match code {
189 D_BUY | PD_LONG => Some(Direction::LONG),
190 D_SELL | PD_SHORT => Some(Direction::SHORT),
191 _ => None,
192 }
193}
194
195pub fn offset_from(code: &str) -> Option<Offset> {
197 match code {
198 OFEN_OPEN => Some(Offset::OPEN),
199 OFEN_CLOSE => Some(Offset::CLOSE),
200 OFEN_CLOSE_TODAY => Some(Offset::CLOSETODAY),
201 OFEN_CLOSE_YESTERDAY => Some(Offset::CLOSEYESTERDAY),
202 _ => None,
203 }
204}
205
206pub fn order_type_from(price_type: &str, time_cond: &str, volume_cond: &str) -> Option<OrderType> {
209 match (price_type, time_cond, volume_cond) {
210 (OPT_LIMIT_PRICE, TC_GFD, VC_AV) => Some(OrderType::LIMIT),
211 (OPT_ANY_PRICE, TC_GFD, VC_AV) => Some(OrderType::MARKET),
212 (OPT_LIMIT_PRICE, TC_IOC, VC_AV) => Some(OrderType::FAK),
213 (OPT_LIMIT_PRICE, TC_IOC, VC_CV) => Some(OrderType::FOK),
214 _ => None,
215 }
216}
217
218pub fn direction_to(d: Direction) -> Option<&'static str> {
221 match d {
222 Direction::LONG => Some(D_BUY),
223 Direction::SHORT => Some(D_SELL),
224 _ => None,
225 }
226}
227
228pub fn offset_to(exchange: Exchange, o: Offset) -> Option<&'static str> {
230 let o = match o {
233 Offset::CLOSETODAY | Offset::CLOSEYESTERDAY
234 if exchange != Exchange::SHFE && exchange != Exchange::INE =>
235 {
236 Offset::CLOSE
237 }
238 other => other,
239 };
240 match o {
241 Offset::OPEN => Some(OFEN_OPEN),
242 Offset::CLOSE => Some(OFEN_CLOSE),
243 Offset::CLOSETODAY => Some(OFEN_CLOSE_TODAY),
244 Offset::CLOSEYESTERDAY => Some(OFEN_CLOSE_YESTERDAY),
245 _ => None,
246 }
247}
248
249pub fn order_type_to(t: OrderType) -> Option<(&'static str, &'static str, &'static str)> {
251 match t {
252 OrderType::LIMIT => Some((OPT_LIMIT_PRICE, TC_GFD, VC_AV)),
253 OrderType::MARKET => Some((OPT_ANY_PRICE, TC_GFD, VC_AV)),
254 OrderType::FAK => Some((OPT_LIMIT_PRICE, TC_IOC, VC_AV)),
255 OrderType::FOK => Some((OPT_LIMIT_PRICE, TC_IOC, VC_CV)),
256 _ => None,
257 }
258}
259
260pub fn adjust_price(price: f64) -> f64 {
264 if price == MAX_FLOAT { 0.0 } else { price }
265}
266
267fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
274 let y = if m <= 2 { y - 1 } else { y };
275 let era = if y >= 0 { y } else { y - 399 } / 400;
276 let yoe = y - era * 400;
277 let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
278 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
279 era * 146097 + doe - 719468
280}
281
282pub fn ctp_datetime(date: &str, time: &str, millis: i64) -> Option<DtNs> {
288 if date.len() != 8 || time.len() != 8 || !date.is_ascii() || !time.is_ascii() {
293 return None;
294 }
295 let digits_ok = |s: &str| s.bytes().all(|b: u8| b.is_ascii_digit());
296 if !(digits_ok(date)
297 && digits_ok(&time[..2])
298 && digits_ok(&time[3..5])
299 && digits_ok(&time[6..8]))
300 {
301 return None;
302 }
303 let (y, m, d) = (
304 date[0..4].parse::<i64>().ok()?,
305 date[4..6].parse::<i64>().ok()?,
306 date[6..8].parse::<i64>().ok()?,
307 );
308 let (h, mi, s) = (
309 time[0..2].parse::<i64>().ok()?,
310 time[3..5].parse::<i64>().ok()?,
311 time[6..8].parse::<i64>().ok()?,
312 );
313 if !((1..=12).contains(&m) && h < 24 && mi < 60 && s < 60) {
314 return None;
315 }
316 let secs = days_from_civil(y, m, d)
324 .checked_mul(86_400)?
325 .checked_add(h * 3_600 + mi * 60 + s - 8 * 3_600)?;
326 secs.checked_mul(1_000_000_000)?
327 .checked_add(millis.checked_mul(1_000_000)?)
328}
329
330#[must_use]
338pub fn now_epoch_ms() -> i64 {
339 ek_data::time::now_nanos() / 1_000_000
340}
341
342pub fn shanghai_now_date() -> String {
344 let secs = SystemTime::now()
345 .duration_since(UNIX_EPOCH)
346 .map(|d| d.as_secs() as i64)
347 .unwrap_or(0)
348 + 8 * 3_600;
349 let days = secs.div_euclid(86_400);
350 let z = days + 719_468;
352 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
353 let doe = z - era * 146_097;
354 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
355 let y = yoe + era * 400;
356 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
357 let mp = (5 * doy + 2) / 153;
358 let d = doy - (153 * mp + 2) / 5 + 1;
359 let m = if mp < 10 { mp + 3 } else { mp - 9 };
360 let y = if m <= 2 { y + 1 } else { y };
361 format!("{y:04}{m:02}{d:02}")
362}
363
364pub fn error_text(msg: &str, e: &ErrorField) -> String {
368 format!("{},代码:{},信息:{}", msg, e.error_id, e.error_msg)
369}
370
371fn yyyymmdd_i32(s: &str) -> Option<i32> {
388 if s.len() != 8 || !s.bytes().all(|b| b.is_ascii_digit()) {
389 return None;
390 }
391 match s.parse::<i32>() {
392 Ok(0) => None,
393 Ok(v) => Some(v),
394 Err(_) => None,
395 }
396}
397
398pub fn tick_from(
417 dm: &DepthMarketData,
418 contract: Option<&ContractData>,
419 gw: &str,
420 current_date: &str,
421) -> TickData {
422 let exchange = match contract {
423 Some(c) => c.exchange,
424 None => exchange_of(&dm.exchange_id).unwrap_or(Exchange::UNKNOWN),
426 };
427 let date_str = if dm.action_day.is_empty() || exchange == Exchange::DCE {
428 current_date.to_string()
429 } else {
430 dm.action_day.clone()
431 };
432 let dt = ctp_datetime(&date_str, &dm.update_time, dm.update_millisec);
434 let trading_day = yyyymmdd_i32(&dm.trading_day);
435 let action_day = yyyymmdd_i32(&date_str);
436 let mut tick = TickData {
437 gateway_name: gw.to_string(),
438 symbol: dm.instrument_id.clone(),
439 exchange,
440 datetime: dt.unwrap_or_default(),
441 datetime_invalid: dt.is_none(),
443 trading_day,
444 action_day,
445 update_time: dm.update_time.clone(),
446 update_millisec: dm.update_millisec,
447 name: contract.map(|c| c.name.clone()).unwrap_or_default(),
449 volume: dm.volume,
450 turnover: dm.turnover,
451 open_interest: dm.open_interest,
452 last_price: adjust_price(dm.last_price),
453 limit_up: adjust_price(dm.upper_limit_price),
454 limit_down: adjust_price(dm.lower_limit_price),
455 open_price: adjust_price(dm.open_price),
456 high_price: adjust_price(dm.highest_price),
457 low_price: adjust_price(dm.lowest_price),
458 pre_close: adjust_price(dm.pre_close_price),
459 bid_price_1: adjust_price(dm.bid_price1),
460 ask_price_1: adjust_price(dm.ask_price1),
461 bid_volume_1: dm.bid_volume1,
462 ask_volume_1: dm.ask_volume1,
463 bid_price_2: 0.0,
466 bid_price_3: 0.0,
467 bid_price_4: 0.0,
468 bid_price_5: 0.0,
469 ask_price_2: 0.0,
470 ask_price_3: 0.0,
471 ask_price_4: 0.0,
472 ask_price_5: 0.0,
473 bid_volume_2: 0.0,
474 bid_volume_3: 0.0,
475 bid_volume_4: 0.0,
476 bid_volume_5: 0.0,
477 ask_volume_2: 0.0,
478 ask_volume_3: 0.0,
479 ask_volume_4: 0.0,
480 ask_volume_5: 0.0,
481 last_volume: 0.0,
484 localtime: None,
486 pct_change: 0.0,
488 };
489 if dm.bid_volume2 != 0.0 || dm.ask_volume2 != 0.0 {
490 tick.bid_price_2 = adjust_price(dm.bid_price2);
491 tick.bid_price_3 = adjust_price(dm.bid_price3);
492 tick.bid_price_4 = adjust_price(dm.bid_price4);
493 tick.bid_price_5 = adjust_price(dm.bid_price5);
494 tick.ask_price_2 = adjust_price(dm.ask_price2);
495 tick.ask_price_3 = adjust_price(dm.ask_price3);
496 tick.ask_price_4 = adjust_price(dm.ask_price4);
497 tick.ask_price_5 = adjust_price(dm.ask_price5);
498 tick.bid_volume_2 = dm.bid_volume2;
499 tick.bid_volume_3 = dm.bid_volume3;
500 tick.bid_volume_4 = dm.bid_volume4;
501 tick.bid_volume_5 = dm.bid_volume5;
502 tick.ask_volume_2 = dm.ask_volume2;
503 tick.ask_volume_3 = dm.ask_volume3;
504 tick.ask_volume_4 = dm.ask_volume4;
505 tick.ask_volume_5 = dm.ask_volume5;
506 }
507 tick
508}
509
510pub fn contract_from(inf: &InstrumentField, gw: &str) -> ContractData {
517 ContractData {
518 gateway_name: gw.to_string(),
519 symbol: inf.instrument_id.clone(),
520 name: inf.instrument_name.clone(),
521 exchange: exchange_of(&inf.exchange_id).unwrap_or(Exchange::UNKNOWN),
522 asset: asset_from(&inf.product_class).unwrap_or(Asset::UNKNOWN),
523 instrument_type: instrument_type_from(&inf.product_class),
525 size: inf.volume_multiple,
526 price_tick: inf.price_tick,
527 max_limit_order_volume: Some(inf.max_limit_order_volume),
528 min_limit_order_volume: Some(inf.min_limit_order_volume),
529 max_market_order_volume: Some(inf.max_market_order_volume),
530 min_market_order_volume: Some(inf.min_market_order_volume),
531 delivery_year: Some(inf.delivery_year as i32),
532 delivery_month: Some(inf.delivery_month as i32),
533 product_id: Some(inf.product_id.clone()),
534 is_trading: Some(inf.is_trading == 1),
536 open_date: (!inf.open_date.is_empty()).then(|| inf.open_date.clone()),
539 up_stop_price: None,
544 down_stop_price: None,
545 pre_close: None,
548 float_volume: None,
551 total_volume: None,
552 instrument_status: None,
555 }
556}
557
558pub fn order_from(of: &OrderField, exchange: Exchange, gw: &str) -> OrderData {
571 let orderid = format!("{}_{}_{}", of.front_id, of.session_id, of.order_ref);
573 OrderData {
574 gateway_name: gw.to_string(),
575 symbol: of.instrument_id.clone(),
576 exchange,
577 orderid,
578 order_type: order_type_from(
579 &of.order_price_type,
580 &of.time_condition,
581 &of.volume_condition,
582 )
583 .unwrap_or(OrderType::UNKNOWN),
584 direction: direction_from(&of.direction),
585 offset: offset_from(&of.comb_offset_flag).unwrap_or(Offset::NONE),
586 price: adjust_price(of.limit_price),
587 volume: of.volume_total_original,
588 traded: of.volume_traded,
589 status: status_of(&of.order_status).unwrap_or(Status::UNKNOWN),
590 datetime: ctp_datetime(&of.insert_date, &of.insert_time, 0),
593 reference: String::new(),
600 name: String::new(),
602 status_msg: String::new(),
605 traded_price: 0.0,
608 }
609}
610
611pub fn trade_from(tf: &TradeField, orderid: &str, exchange: Exchange, gw: &str) -> TradeData {
621 TradeData {
622 gateway_name: gw.to_string(),
623 symbol: tf.instrument_id.clone(),
624 exchange,
625 orderid: orderid.to_string(),
626 tradeid: tf.trade_id.clone(),
627 direction: direction_from(&tf.direction),
628 offset: offset_from(&tf.offset_flag).unwrap_or(Offset::NONE),
629 price: tf.price,
630 volume: tf.volume,
631 datetime: ctp_datetime(&tf.trade_date, &tf.trade_time, 0),
632 commission: 0.0,
637 slippage: 0.0,
638 trade_pnl: 0.0,
639 name: String::new(),
641 traded_amount: 0.0,
642 }
643}
644
645pub fn account_from(ta: &TradingAccountField, gw: &str) -> AccountData {
651 AccountData {
652 gateway_name: gw.to_string(),
653 accountid: ta.account_id.clone(),
654 balance: ta.balance,
655 frozen: ta.frozen_margin + ta.frozen_cash + ta.frozen_commission,
656 }
657}
658
659pub fn accumulate_position(
662 row: &InvestorPositionField,
663 pos: &mut ek_data::PositionData,
664 exchange: Exchange,
665 contract_size: f64,
666) {
667 if exchange == Exchange::SHFE || exchange == Exchange::INE {
675 if row.yd_position != 0.0 && row.today_position == 0.0 {
676 pos.yd_volume = row.position;
677 }
678 } else {
679 pos.yd_volume = row.position - row.today_position;
680 }
681 let cost = pos.price * pos.volume * contract_size;
684 pos.volume += row.position;
685 pos.pnl += row.position_profit;
686 if pos.volume != 0.0 && contract_size != 0.0 {
687 pos.price = (cost + row.position_cost) / (pos.volume * contract_size);
688 }
689 if pos.direction == Direction::LONG {
691 pos.frozen += row.short_frozen;
692 } else {
693 pos.frozen += row.long_frozen;
694 }
695}
696
697pub fn new_position_data(
699 symbol: &str,
700 exchange: Exchange,
701 direction: Option<Direction>,
702 gw: &str,
703) -> Option<ek_data::PositionData> {
704 Some(ek_data::PositionData {
705 gateway_name: gw.to_string(),
706 symbol: symbol.to_string(),
707 exchange,
708 direction: direction?,
709 ..ek_data::PositionData::default()
710 })
711}
712
713pub fn cancel_action_from(
721 orderid: &str,
722 symbol: &str,
723 exchange: Exchange,
724 broker: &str,
725 user: &str,
726) -> Option<InputOrderAction> {
727 let mut parts = orderid.split('_');
728 let (front, session, ore) = (parts.next()?, parts.next()?, parts.next()?);
729 if parts.next().is_some() {
730 return None;
731 }
732 let (front_id, session_id): (i64, i64) = (front.parse().ok()?, session.parse().ok()?);
733 Some(InputOrderAction {
734 instrument_id: symbol.to_string(),
735 exchange_id: exchange.as_str().to_string(),
736 order_ref: ore.to_string(),
737 front_id,
738 session_id,
739 action_flag: "0".to_string(),
741 broker_id: broker.to_string(),
742 investor_id: user.to_string(),
743 })
744}
745
746pub fn instrument_id(raw: &str) -> Option<String> {
756 let s = raw.trim();
757 if s.is_empty() {
758 return None;
759 }
760 match s.split_once('.') {
761 None => Some(s.to_string()),
762 Some((head, rest)) => {
763 let rest = rest.trim();
764 if head.is_empty() || rest.is_empty() {
765 return None;
766 }
767 exchange_of(&head.trim().to_ascii_uppercase()).map(|_| rest.to_string())
769 }
770 }
771}
772
773pub fn position_direction(code: &str, position: f64) -> Option<Direction> {
780 match code {
781 PD_NET => match position.partial_cmp(&0.0)? {
782 std::cmp::Ordering::Greater => Some(Direction::LONG),
783 std::cmp::Ordering::Less => Some(Direction::SHORT),
784 std::cmp::Ordering::Equal => None,
785 },
786 PD_LONG => Some(Direction::LONG),
787 PD_SHORT => Some(Direction::SHORT),
788 _ => None,
789 }
790}
791#[cfg(test)]
792mod tests {
793 use super::*;
794
795 #[test]
800 fn contract_carries_open_date_from_wire() {
801 let inf = InstrumentField {
802 instrument_id: "rb2611".into(),
803 exchange_id: "SHFE".into(),
804 instrument_name: "螺纹钢2611".into(),
805 product_class: "1".into(),
806 volume_multiple: 10.0,
807 price_tick: 1.0,
808 max_limit_order_volume: 500.0,
809 min_limit_order_volume: 1.0,
810 max_market_order_volume: 500.0,
811 min_market_order_volume: 1.0,
812 delivery_year: 2026,
813 delivery_month: 11,
814 product_id: "rb".into(),
815 is_trading: 1,
816 open_date: "20260115".into(),
817 };
818 let c = contract_from(&inf, "ctp");
819 assert_eq!(c.open_date.as_deref(), Some("20260115"), "上市日必须透传");
820 }
821
822 #[test]
825 fn contract_open_date_empty_maps_to_none() {
826 let inf = InstrumentField {
827 instrument_id: "x".into(),
828 exchange_id: "SHFE".into(),
829 instrument_name: "x".into(),
830 product_class: "1".into(),
831 volume_multiple: 10.0,
832 price_tick: 1.0,
833 max_limit_order_volume: 1.0,
834 min_limit_order_volume: 1.0,
835 max_market_order_volume: 1.0,
836 min_market_order_volume: 1.0,
837 delivery_year: 2026,
838 delivery_month: 1,
839 product_id: "x".into(),
840 is_trading: 1,
841 open_date: String::new(),
842 };
843 let c = contract_from(&inf, "ctp");
844 assert_eq!(c.open_date, None);
845 }
846
847 #[test]
848 fn yyyymmdd_i32_accepts_real_dates_and_rejects_garbage() {
849 assert_eq!(yyyymmdd_i32("20261015"), Some(20261015));
850 assert_eq!(yyyymmdd_i32("00000000"), None);
852 assert_eq!(yyyymmdd_i32("2026101"), None);
854 assert_eq!(yyyymmdd_i32("202610150"), None);
855 assert_eq!(yyyymmdd_i32("2026101a"), None);
856 assert_eq!(yyyymmdd_i32(""), None);
857 }
858
859 #[test]
862 fn instrument_id_accepts_bare_and_en_symbol_and_rejects_garbage() {
863 assert_eq!(instrument_id("ag2612").as_deref(), Some("ag2612"));
865 assert_eq!(instrument_id(" cu2612 ").as_deref(), Some("cu2612"));
866 assert_eq!(instrument_id("SHFE.ag2612").as_deref(), Some("ag2612"));
868 assert_eq!(instrument_id("INE.sc2612").as_deref(), Some("sc2612"));
869 assert_eq!(instrument_id("CFFEX.IF2412").as_deref(), Some("IF2412"));
870 assert_eq!(instrument_id(""), None);
872 assert_eq!(instrument_id(" "), None);
873 assert_eq!(instrument_id("SHFE."), None);
874 assert_eq!(instrument_id(".ag2612"), None);
875 assert_eq!(instrument_id("XX.ag2612"), None);
876 assert_eq!(instrument_id("shfe.ag2612").as_deref(), Some("ag2612"));
878 assert_eq!(instrument_id("Shfe. ag2612 ").as_deref(), Some("ag2612"));
879 }
880
881 #[test]
882 fn offset_to_folds_close_today_by_exchange() {
883 for ex in [Exchange::SHFE, Exchange::INE] {
885 assert_eq!(offset_to(ex, Offset::CLOSETODAY), Some(OFEN_CLOSE_TODAY));
886 assert_eq!(
887 offset_to(ex, Offset::CLOSEYESTERDAY),
888 Some(OFEN_CLOSE_YESTERDAY)
889 );
890 assert_eq!(offset_to(ex, Offset::OPEN), Some(OFEN_OPEN));
891 assert_eq!(offset_to(ex, Offset::CLOSE), Some(OFEN_CLOSE));
892 }
893 for ex in [
895 Exchange::CFFEX,
896 Exchange::CZCE,
897 Exchange::DCE,
898 Exchange::GFEX,
899 ] {
900 assert_eq!(
901 offset_to(ex, Offset::CLOSETODAY),
902 Some(OFEN_CLOSE),
903 "{ex:?}"
904 );
905 assert_eq!(
906 offset_to(ex, Offset::CLOSEYESTERDAY),
907 Some(OFEN_CLOSE),
908 "{ex:?}"
909 );
910 assert_eq!(offset_to(ex, Offset::OPEN), Some(OFEN_OPEN));
911 assert_eq!(offset_to(ex, Offset::CLOSE), Some(OFEN_CLOSE));
912 }
913 }
914
915 #[test]
916 fn status_of_covers_part_and_no_trade_not_queueing() {
917 assert_eq!(status_of("2"), Some(Status::PARTTRADED));
918 assert_eq!(status_of("4"), Some(Status::NOTTRADED));
919 assert_eq!(status_of("0"), Some(Status::ALLTRADED));
921 assert_eq!(status_of("1"), Some(Status::PARTTRADED));
922 assert_eq!(status_of("3"), Some(Status::NOTTRADED));
923 assert_eq!(status_of("5"), Some(Status::CANCELLED));
924 assert_eq!(status_of("a"), Some(Status::SUBMITTING));
925 assert_eq!(status_of("z"), None);
926 }
927
928 #[test]
929 fn position_direction_uses_sign_for_net_positions() {
930 assert_eq!(position_direction("1", 3.0), Some(Direction::LONG));
932 assert_eq!(position_direction("1", -2.0), Some(Direction::SHORT));
933 assert_eq!(position_direction("1", 0.0), None);
934 assert_eq!(position_direction("2", 5.0), Some(Direction::LONG));
936 assert_eq!(position_direction("3", 5.0), Some(Direction::SHORT));
937 assert_eq!(position_direction("9", 1.0), None);
938 }
939
940 #[test]
941 fn order_from_normalizes_market_order_sentinel_price() {
942 let of = OrderField {
943 instrument_id: "ag2612".into(),
944 exchange_id: "SHFE".into(),
945 order_ref: "1".into(),
946 front_id: 1,
947 session_id: 2,
948 order_status: "3".into(),
949 order_price_type: "1".into(),
950 time_condition: "3".into(),
951 volume_condition: "1".into(),
952 insert_date: "20261030".into(),
953 insert_time: "00:15:00".into(),
954 direction: "0".into(),
955 comb_offset_flag: "0".into(),
956 limit_price: MAX_FLOAT,
957 volume_total_original: 1.0,
958 volume_traded: 0.0,
959 ..OrderField::default()
960 };
961 let order = order_from(&of, Exchange::SHFE, "ctp");
963 assert_eq!(order.price, 0.0);
964 assert_eq!(order.symbol, "ag2612");
965 assert_eq!(order.status, Status::NOTTRADED);
966 }
967}