1use crate::HlError;
2use rust_decimal::Decimal;
3use serde::de::{self, MapAccess, Visitor};
4use serde::ser::SerializeMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Side {
12 Buy,
14 Sell,
16}
17
18impl Side {
19 pub fn is_buy(self) -> bool {
21 matches!(self, Side::Buy)
22 }
23
24 pub fn from_is_buy(is_buy: bool) -> Self {
26 if is_buy {
27 Side::Buy
28 } else {
29 Side::Sell
30 }
31 }
32}
33
34impl fmt::Display for Side {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Side::Buy => write!(f, "buy"),
38 Side::Sell => write!(f, "sell"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[non_exhaustive]
48pub enum Tif {
49 Gtc,
51 Ioc,
53 Alo,
55}
56
57impl fmt::Display for Tif {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Tif::Gtc => write!(f, "Gtc"),
61 Tif::Ioc => write!(f, "Ioc"),
62 Tif::Alo => write!(f, "Alo"),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum Tpsl {
73 Sl,
75 Tp,
77}
78
79impl fmt::Display for Tpsl {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Tpsl::Sl => write!(f, "sl"),
83 Tpsl::Tp => write!(f, "tp"),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "lowercase")]
91pub enum PositionSide {
92 Long,
94 Short,
96}
97
98impl fmt::Display for PositionSide {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 PositionSide::Long => write!(f, "long"),
102 PositionSide::Short => write!(f, "short"),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110#[non_exhaustive]
111pub enum OrderStatus {
112 Filled,
114 Partial,
116 Open,
118 Rejected,
120 TriggerSl,
122 TriggerTp,
124}
125
126impl fmt::Display for OrderStatus {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 OrderStatus::Filled => write!(f, "filled"),
130 OrderStatus::Partial => write!(f, "partial"),
131 OrderStatus::Open => write!(f, "open"),
132 OrderStatus::Rejected => write!(f, "rejected"),
133 OrderStatus::TriggerSl => write!(f, "trigger_sl"),
134 OrderStatus::TriggerTp => write!(f, "trigger_tp"),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
148#[non_exhaustive]
149pub enum Grouping {
150 #[default]
152 Na,
153 NormalTpsl,
155 PositionTpsl,
157}
158
159impl Grouping {
160 pub fn as_str(&self) -> &'static str {
162 match self {
163 Grouping::Na => "na",
164 Grouping::NormalTpsl => "normalTpsl",
165 Grouping::PositionTpsl => "positionTpsl",
166 }
167 }
168}
169
170impl fmt::Display for Grouping {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "{}", self.as_str())
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179#[non_exhaustive]
180pub struct OrderWire {
181 pub asset: u32,
183 pub is_buy: bool,
185 pub limit_px: String,
187 pub sz: String,
189 pub reduce_only: bool,
191 pub order_type: OrderTypeWire,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub cloid: Option<String>,
196}
197
198#[derive(Debug, Clone)]
204pub struct OrderWireBuilder {
205 asset: u32,
206 is_buy: bool,
207 limit_px: String,
208 sz: String,
209 reduce_only: bool,
210 order_type: OrderTypeWire,
211 cloid: Option<String>,
212}
213
214impl OrderWireBuilder {
215 pub fn tif(mut self, tif: Tif) -> Self {
219 if let OrderTypeWire::Limit(ref mut limit) = self.order_type {
220 limit.tif = tif;
221 }
222 self
223 }
224
225 pub fn cloid(mut self, cloid: impl Into<String>) -> Self {
227 self.cloid = Some(cloid.into());
228 self
229 }
230
231 pub fn reduce_only(mut self, reduce_only: bool) -> Self {
233 self.reduce_only = reduce_only;
234 self
235 }
236
237 pub fn build(self) -> Result<OrderWire, HlError> {
239 let px: Decimal = self
240 .limit_px
241 .parse()
242 .map_err(|_| HlError::Parse(format!("invalid price: {}", self.limit_px)))?;
243 if px <= Decimal::ZERO {
244 return Err(HlError::Parse(format!(
245 "price must be positive, got: {}",
246 self.limit_px
247 )));
248 }
249 let sz: Decimal = self
250 .sz
251 .parse()
252 .map_err(|_| HlError::Parse(format!("invalid size: {}", self.sz)))?;
253 if sz <= Decimal::ZERO {
254 return Err(HlError::Parse(format!(
255 "size must be positive, got: {}",
256 self.sz
257 )));
258 }
259 Ok(OrderWire {
260 asset: self.asset,
261 is_buy: self.is_buy,
262 limit_px: self.limit_px,
263 sz: self.sz,
264 reduce_only: self.reduce_only,
265 order_type: self.order_type,
266 cloid: self.cloid,
267 })
268 }
269}
270
271impl OrderWire {
272 pub fn limit_buy(asset: u32, limit_px: Decimal, sz: Decimal) -> OrderWireBuilder {
293 OrderWireBuilder {
294 asset,
295 is_buy: true,
296 limit_px: limit_px.normalize().to_string(),
297 sz: sz.normalize().to_string(),
298 reduce_only: false,
299 order_type: OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc }),
300 cloid: None,
301 }
302 }
303
304 pub fn limit_sell(asset: u32, limit_px: Decimal, sz: Decimal) -> OrderWireBuilder {
308 OrderWireBuilder {
309 asset,
310 is_buy: false,
311 limit_px: limit_px.normalize().to_string(),
312 sz: sz.normalize().to_string(),
313 reduce_only: false,
314 order_type: OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc }),
315 cloid: None,
316 }
317 }
318
319 pub fn trigger_buy(
324 asset: u32,
325 trigger_px: Decimal,
326 sz: Decimal,
327 tpsl: Tpsl,
328 ) -> OrderWireBuilder {
329 let trigger_px_str = trigger_px.normalize().to_string();
330 OrderWireBuilder {
331 asset,
332 is_buy: true,
333 limit_px: trigger_px_str.clone(),
334 sz: sz.normalize().to_string(),
335 reduce_only: true,
336 order_type: OrderTypeWire::Trigger(TriggerOrderType {
337 trigger_px: trigger_px_str,
338 is_market: true,
339 tpsl,
340 }),
341 cloid: None,
342 }
343 }
344
345 pub fn trigger_sell(
350 asset: u32,
351 trigger_px: Decimal,
352 sz: Decimal,
353 tpsl: Tpsl,
354 ) -> OrderWireBuilder {
355 let trigger_px_str = trigger_px.normalize().to_string();
356 OrderWireBuilder {
357 asset,
358 is_buy: false,
359 limit_px: trigger_px_str.clone(),
360 sz: sz.normalize().to_string(),
361 reduce_only: true,
362 order_type: OrderTypeWire::Trigger(TriggerOrderType {
363 trigger_px: trigger_px_str,
364 is_market: true,
365 tpsl,
366 }),
367 cloid: None,
368 }
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
378#[non_exhaustive]
379pub enum OrderTypeWire {
380 Limit(LimitOrderType),
382 Trigger(TriggerOrderType),
384}
385
386impl OrderTypeWire {
387 pub fn is_limit(&self) -> bool {
389 matches!(self, OrderTypeWire::Limit(_))
390 }
391
392 pub fn is_trigger(&self) -> bool {
394 matches!(self, OrderTypeWire::Trigger(_))
395 }
396}
397
398impl Serialize for OrderTypeWire {
399 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
400 where
401 S: Serializer,
402 {
403 let mut map = serializer.serialize_map(Some(1))?;
404 match self {
405 OrderTypeWire::Limit(limit) => {
406 map.serialize_entry("limit", limit)?;
407 }
408 OrderTypeWire::Trigger(trigger) => {
409 map.serialize_entry("trigger", trigger)?;
410 }
411 }
412 map.end()
413 }
414}
415
416impl<'de> Deserialize<'de> for OrderTypeWire {
417 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
418 where
419 D: Deserializer<'de>,
420 {
421 struct OrderTypeWireVisitor;
422
423 impl<'de> Visitor<'de> for OrderTypeWireVisitor {
424 type Value = OrderTypeWire;
425
426 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
427 formatter.write_str("a map with either a \"limit\" or \"trigger\" key")
428 }
429
430 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
431 where
432 A: MapAccess<'de>,
433 {
434 let key: String = map
435 .next_key()?
436 .ok_or_else(|| de::Error::custom("empty order type object"))?;
437 match key.as_str() {
438 "limit" => {
439 let limit: LimitOrderType = map.next_value()?;
440 Ok(OrderTypeWire::Limit(limit))
441 }
442 "trigger" => {
443 let trigger: TriggerOrderType = map.next_value()?;
444 Ok(OrderTypeWire::Trigger(trigger))
445 }
446 other => Err(de::Error::unknown_field(other, &["limit", "trigger"])),
447 }
448 }
449 }
450
451 deserializer.deserialize_map(OrderTypeWireVisitor)
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct LimitOrderType {
458 pub tif: Tif,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(rename_all = "camelCase")]
465pub struct TriggerOrderType {
466 pub trigger_px: String,
468 pub is_market: bool,
470 pub tpsl: Tpsl,
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
476#[non_exhaustive]
477pub struct CancelRequest {
478 pub asset: u32,
480 pub oid: u64,
482}
483
484impl CancelRequest {
485 pub fn new(asset: u32, oid: u64) -> Self {
487 Self { asset, oid }
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
493#[non_exhaustive]
494pub struct CancelByCloidRequest {
495 pub asset: u32,
497 pub cloid: String,
499}
500
501impl CancelByCloidRequest {
502 pub fn new(asset: u32, cloid: impl Into<String>) -> Self {
504 Self {
505 asset,
506 cloid: cloid.into(),
507 }
508 }
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
513#[non_exhaustive]
514pub struct ModifyRequest {
515 pub oid: u64,
517 pub order: OrderWire,
519}
520
521impl ModifyRequest {
522 pub fn new(oid: u64, order: OrderWire) -> Self {
524 Self { oid, order }
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use std::str::FromStr;
532
533 #[test]
536 fn order_type_wire_limit_serialization() {
537 let ot = OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc });
538 let json = serde_json::to_string(&ot).unwrap();
539 assert_eq!(json, r#"{"limit":{"tif":"Gtc"}}"#);
540 }
541
542 #[test]
543 fn order_type_wire_trigger_serialization() {
544 let ot = OrderTypeWire::Trigger(TriggerOrderType {
545 trigger_px: "99.0".into(),
546 is_market: true,
547 tpsl: Tpsl::Sl,
548 });
549 let json = serde_json::to_string(&ot).unwrap();
550 assert_eq!(
551 json,
552 r#"{"trigger":{"triggerPx":"99.0","isMarket":true,"tpsl":"sl"}}"#
553 );
554 }
555
556 #[test]
557 fn order_type_wire_limit_roundtrip() {
558 let original = OrderTypeWire::Limit(LimitOrderType { tif: Tif::Ioc });
559 let json = serde_json::to_string(&original).unwrap();
560 let parsed: OrderTypeWire = serde_json::from_str(&json).unwrap();
561 assert_eq!(parsed, original);
562 }
563
564 #[test]
565 fn order_type_wire_trigger_roundtrip() {
566 let original = OrderTypeWire::Trigger(TriggerOrderType {
567 trigger_px: "50.5".into(),
568 is_market: false,
569 tpsl: Tpsl::Tp,
570 });
571 let json = serde_json::to_string(&original).unwrap();
572 let parsed: OrderTypeWire = serde_json::from_str(&json).unwrap();
573 assert_eq!(parsed, original);
574 }
575
576 #[test]
577 fn order_type_wire_is_limit_and_is_trigger() {
578 let limit = OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc });
579 assert!(limit.is_limit());
580 assert!(!limit.is_trigger());
581
582 let trigger = OrderTypeWire::Trigger(TriggerOrderType {
583 trigger_px: "1.0".into(),
584 is_market: true,
585 tpsl: Tpsl::Sl,
586 });
587 assert!(trigger.is_trigger());
588 assert!(!trigger.is_limit());
589 }
590
591 #[test]
592 fn order_type_wire_invalid_key_fails() {
593 let json = r#"{"unknown":{"tif":"Gtc"}}"#;
594 assert!(serde_json::from_str::<OrderTypeWire>(json).is_err());
595 }
596
597 #[test]
598 fn order_type_wire_empty_object_fails() {
599 let json = r#"{}"#;
600 assert!(serde_json::from_str::<OrderTypeWire>(json).is_err());
601 }
602
603 #[test]
606 fn builder_limit_buy_defaults() {
607 let order =
608 OrderWire::limit_buy(1, Decimal::from(90000), Decimal::from_str("0.001").unwrap())
609 .build()
610 .unwrap();
611 assert_eq!(order.asset, 1);
612 assert!(order.is_buy);
613 assert_eq!(order.limit_px, "90000");
614 assert_eq!(order.sz, "0.001");
615 assert!(!order.reduce_only);
616 assert!(order.order_type.is_limit());
617 assert!(order.cloid.is_none());
618 if let OrderTypeWire::Limit(ref l) = order.order_type {
619 assert_eq!(l.tif, Tif::Gtc);
620 }
621 }
622
623 #[test]
624 fn builder_limit_sell_with_options() {
625 let order = OrderWire::limit_sell(5, Decimal::from(3000), Decimal::from(2))
626 .tif(Tif::Ioc)
627 .cloid("my-order-1")
628 .reduce_only(true)
629 .build()
630 .unwrap();
631 assert_eq!(order.asset, 5);
632 assert!(!order.is_buy);
633 assert_eq!(order.limit_px, "3000");
634 assert_eq!(order.sz, "2");
635 assert!(order.reduce_only);
636 assert_eq!(order.cloid.as_deref(), Some("my-order-1"));
637 if let OrderTypeWire::Limit(ref l) = order.order_type {
638 assert_eq!(l.tif, Tif::Ioc);
639 } else {
640 panic!("expected limit order type");
641 }
642 }
643
644 #[test]
645 fn builder_trigger_buy() {
646 let order = OrderWire::trigger_buy(0, Decimal::from(99), Decimal::from(10), Tpsl::Sl)
647 .cloid("trigger-1")
648 .build()
649 .unwrap();
650 assert_eq!(order.asset, 0);
651 assert!(order.is_buy);
652 assert!(order.reduce_only);
653 assert!(order.order_type.is_trigger());
654 if let OrderTypeWire::Trigger(ref t) = order.order_type {
655 assert_eq!(t.trigger_px, "99");
656 assert!(t.is_market);
657 assert_eq!(t.tpsl, Tpsl::Sl);
658 } else {
659 panic!("expected trigger order type");
660 }
661 }
662
663 #[test]
664 fn builder_trigger_sell() {
665 let order = OrderWire::trigger_sell(2, Decimal::from(150), Decimal::from(5), Tpsl::Tp)
666 .reduce_only(false)
667 .build()
668 .unwrap();
669 assert_eq!(order.asset, 2);
670 assert!(!order.is_buy);
671 assert!(!order.reduce_only); assert!(order.order_type.is_trigger());
673 if let OrderTypeWire::Trigger(ref t) = order.order_type {
674 assert_eq!(t.trigger_px, "150");
675 assert_eq!(t.tpsl, Tpsl::Tp);
676 } else {
677 panic!("expected trigger order type");
678 }
679 }
680
681 #[test]
682 fn builder_tif_noop_on_trigger() {
683 let order = OrderWire::trigger_buy(0, Decimal::from(99), Decimal::ONE, Tpsl::Sl)
685 .tif(Tif::Ioc)
686 .build()
687 .unwrap();
688 assert!(order.order_type.is_trigger());
689 }
690
691 #[test]
692 fn build_validates_positive_price() {
693 let result = OrderWire::limit_buy(0, Decimal::ZERO, Decimal::ONE).build();
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn build_validates_positive_size() {
699 let result = OrderWire::limit_buy(0, Decimal::ONE, Decimal::ZERO).build();
700 assert!(result.is_err());
701 }
702
703 #[test]
704 fn build_validates_negative_price() {
705 let result = OrderWire::limit_buy(0, Decimal::from(-1), Decimal::ONE).build();
706 assert!(result.is_err());
707 }
708
709 #[test]
710 fn build_validates_negative_size() {
711 let result = OrderWire::limit_buy(0, Decimal::ONE, Decimal::from(-1)).build();
712 assert!(result.is_err());
713 }
714
715 #[test]
716 fn build_success() {
717 let result =
718 OrderWire::limit_buy(0, Decimal::from(90000), Decimal::from_str("0.001").unwrap())
719 .build();
720 assert!(result.is_ok());
721 }
722
723 #[test]
724 fn side_from_is_buy() {
725 assert_eq!(Side::from_is_buy(true), Side::Buy);
726 assert_eq!(Side::from_is_buy(false), Side::Sell);
727 }
728
729 #[test]
732 fn order_wire_limit_serde_roundtrip() {
733 let order =
734 OrderWire::limit_buy(1, Decimal::from(50000), Decimal::from_str("0.1").unwrap())
735 .cloid("test-cloid")
736 .build()
737 .unwrap();
738 let json = serde_json::to_string(&order).unwrap();
739 let parsed: OrderWire = serde_json::from_str(&json).unwrap();
740 assert_eq!(parsed.asset, 1);
741 assert!(parsed.is_buy);
742 assert_eq!(parsed.limit_px, "50000");
743 assert_eq!(parsed.sz, "0.1");
744 assert!(!parsed.reduce_only);
745 assert_eq!(parsed.cloid.as_deref(), Some("test-cloid"));
746 assert!(parsed.order_type.is_limit());
747 }
748
749 #[test]
750 fn order_wire_trigger_serde_roundtrip() {
751 let order = OrderWire::trigger_buy(0, Decimal::from(100), Decimal::from(10), Tpsl::Tp)
752 .build()
753 .unwrap();
754 let json = serde_json::to_string(&order).unwrap();
755 let parsed: OrderWire = serde_json::from_str(&json).unwrap();
756 let trigger = match parsed.order_type {
757 OrderTypeWire::Trigger(t) => t,
758 _ => panic!("expected trigger"),
759 };
760 assert_eq!(trigger.trigger_px, "100");
761 assert!(trigger.is_market);
762 assert_eq!(trigger.tpsl, Tpsl::Tp);
763 }
764
765 #[test]
766 fn order_wire_camel_case_serialization() {
767 let order = OrderWire::limit_buy(0, Decimal::ONE, Decimal::ONE)
768 .build()
769 .unwrap();
770 let json = serde_json::to_string(&order).unwrap();
771 assert!(json.contains("isBuy"));
772 assert!(json.contains("limitPx"));
773 assert!(json.contains("reduceOnly"));
774 assert!(json.contains("orderType"));
775 assert!(!json.contains("cloid"));
777 }
778
779 #[test]
780 fn order_wire_with_cloid_roundtrip() {
781 let order =
782 OrderWire::limit_sell(5, Decimal::from_str("3000.5").unwrap(), Decimal::from(2))
783 .reduce_only(true)
784 .cloid("my-order-123")
785 .build()
786 .unwrap();
787 let json = serde_json::to_string(&order).unwrap();
788 let parsed: OrderWire = serde_json::from_str(&json).unwrap();
789 assert_eq!(parsed.cloid.as_deref(), Some("my-order-123"));
790 assert!(parsed.reduce_only);
791 assert!(!parsed.is_buy);
792 }
793
794 #[test]
797 fn wire_format_limit_matches_hyperliquid() {
798 let ot = OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc });
800 let json = serde_json::to_value(&ot).unwrap();
801 assert!(json.get("limit").is_some());
802 assert_eq!(json["limit"]["tif"], "Gtc");
803 }
804
805 #[test]
806 fn wire_format_trigger_matches_hyperliquid() {
807 let ot = OrderTypeWire::Trigger(TriggerOrderType {
809 trigger_px: "99.0".into(),
810 is_market: true,
811 tpsl: Tpsl::Sl,
812 });
813 let json = serde_json::to_value(&ot).unwrap();
814 assert!(json.get("trigger").is_some());
815 assert_eq!(json["trigger"]["triggerPx"], "99.0");
816 assert_eq!(json["trigger"]["isMarket"], true);
817 assert_eq!(json["trigger"]["tpsl"], "sl");
818 }
819
820 #[test]
821 fn deserialize_from_hyperliquid_limit_json() {
822 let json = r#"{"limit":{"tif":"Gtc"}}"#;
824 let ot: OrderTypeWire = serde_json::from_str(json).unwrap();
825 assert_eq!(ot, OrderTypeWire::Limit(LimitOrderType { tif: Tif::Gtc }));
826 }
827
828 #[test]
829 fn deserialize_from_hyperliquid_trigger_json() {
830 let json = r#"{"trigger":{"triggerPx":"99.0","isMarket":true,"tpsl":"sl"}}"#;
831 let ot: OrderTypeWire = serde_json::from_str(json).unwrap();
832 assert_eq!(
833 ot,
834 OrderTypeWire::Trigger(TriggerOrderType {
835 trigger_px: "99.0".into(),
836 is_market: true,
837 tpsl: Tpsl::Sl,
838 })
839 );
840 }
841
842 #[test]
845 fn tif_serde_wire_format() {
846 assert_eq!(serde_json::to_string(&Tif::Gtc).unwrap(), "\"Gtc\"");
847 assert_eq!(serde_json::to_string(&Tif::Ioc).unwrap(), "\"Ioc\"");
848 assert_eq!(serde_json::to_string(&Tif::Alo).unwrap(), "\"Alo\"");
849
850 assert_eq!(serde_json::from_str::<Tif>("\"Gtc\"").unwrap(), Tif::Gtc);
851 assert_eq!(serde_json::from_str::<Tif>("\"Ioc\"").unwrap(), Tif::Ioc);
852 assert_eq!(serde_json::from_str::<Tif>("\"Alo\"").unwrap(), Tif::Alo);
853 }
854
855 #[test]
856 fn tpsl_serde_wire_format() {
857 assert_eq!(serde_json::to_string(&Tpsl::Sl).unwrap(), "\"sl\"");
858 assert_eq!(serde_json::to_string(&Tpsl::Tp).unwrap(), "\"tp\"");
859
860 assert_eq!(serde_json::from_str::<Tpsl>("\"sl\"").unwrap(), Tpsl::Sl);
861 assert_eq!(serde_json::from_str::<Tpsl>("\"tp\"").unwrap(), Tpsl::Tp);
862 }
863
864 #[test]
865 fn side_serde_wire_format() {
866 assert_eq!(serde_json::to_string(&Side::Buy).unwrap(), "\"buy\"");
867 assert_eq!(serde_json::to_string(&Side::Sell).unwrap(), "\"sell\"");
868
869 assert_eq!(serde_json::from_str::<Side>("\"buy\"").unwrap(), Side::Buy);
870 assert_eq!(
871 serde_json::from_str::<Side>("\"sell\"").unwrap(),
872 Side::Sell
873 );
874 }
875
876 #[test]
877 fn side_is_buy() {
878 assert!(Side::Buy.is_buy());
879 assert!(!Side::Sell.is_buy());
880 }
881
882 #[test]
883 fn position_side_serde_wire_format() {
884 assert_eq!(
885 serde_json::to_string(&PositionSide::Long).unwrap(),
886 "\"long\""
887 );
888 assert_eq!(
889 serde_json::to_string(&PositionSide::Short).unwrap(),
890 "\"short\""
891 );
892
893 assert_eq!(
894 serde_json::from_str::<PositionSide>("\"long\"").unwrap(),
895 PositionSide::Long
896 );
897 assert_eq!(
898 serde_json::from_str::<PositionSide>("\"short\"").unwrap(),
899 PositionSide::Short
900 );
901 }
902
903 #[test]
904 fn order_status_serde_wire_format() {
905 assert_eq!(
906 serde_json::to_string(&OrderStatus::Filled).unwrap(),
907 "\"filled\""
908 );
909 assert_eq!(
910 serde_json::to_string(&OrderStatus::Partial).unwrap(),
911 "\"partial\""
912 );
913 assert_eq!(
914 serde_json::to_string(&OrderStatus::Open).unwrap(),
915 "\"open\""
916 );
917 assert_eq!(
918 serde_json::to_string(&OrderStatus::TriggerSl).unwrap(),
919 "\"trigger_sl\""
920 );
921 assert_eq!(
922 serde_json::to_string(&OrderStatus::TriggerTp).unwrap(),
923 "\"trigger_tp\""
924 );
925
926 assert_eq!(
927 serde_json::from_str::<OrderStatus>("\"filled\"").unwrap(),
928 OrderStatus::Filled
929 );
930 assert_eq!(
931 serde_json::from_str::<OrderStatus>("\"trigger_sl\"").unwrap(),
932 OrderStatus::TriggerSl
933 );
934 }
935
936 #[test]
937 fn display_impls() {
938 assert_eq!(Side::Buy.to_string(), "buy");
939 assert_eq!(Side::Sell.to_string(), "sell");
940 assert_eq!(Tif::Gtc.to_string(), "Gtc");
941 assert_eq!(Tpsl::Sl.to_string(), "sl");
942 assert_eq!(Tpsl::Tp.to_string(), "tp");
943 assert_eq!(PositionSide::Long.to_string(), "long");
944 assert_eq!(PositionSide::Short.to_string(), "short");
945 assert_eq!(OrderStatus::Filled.to_string(), "filled");
946 assert_eq!(OrderStatus::TriggerSl.to_string(), "trigger_sl");
947 }
948
949 #[test]
950 fn invalid_side_deserialization_fails() {
951 assert!(serde_json::from_str::<Side>("\"BUY\"").is_err());
952 assert!(serde_json::from_str::<Side>("\"Buy\"").is_err());
953 }
954
955 #[test]
956 fn invalid_tif_deserialization_fails() {
957 assert!(serde_json::from_str::<Tif>("\"gtc\"").is_err());
958 assert!(serde_json::from_str::<Tif>("\"GTC\"").is_err());
959 }
960
961 #[test]
962 fn grouping_as_str_matches_wire() {
963 assert_eq!(Grouping::Na.as_str(), "na");
964 assert_eq!(Grouping::NormalTpsl.as_str(), "normalTpsl");
965 assert_eq!(Grouping::PositionTpsl.as_str(), "positionTpsl");
966 assert_eq!(Grouping::default(), Grouping::Na);
967 assert_eq!(Grouping::NormalTpsl.to_string(), "normalTpsl");
968 }
969}