1use std::str::FromStr;
101
102use anyhow::Context;
103use nautilus_model::{
104 enums::{OrderSide, OrderStatus, OrderType, TimeInForce},
105 identifiers::{InstrumentId, Symbol, Venue},
106 orders::{Order, any::OrderAny},
107 types::{AccountBalance, Currency, MarginBalance, Money},
108};
109use rust_decimal::Decimal;
110use serde::{Deserialize, Deserializer, Serializer};
111use serde_json::Value;
112
113use crate::http::models::{
114 AssetId, Cloid, CrossMarginSummary, HyperliquidExchangeResponse,
115 HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams, HyperliquidExecOrderKind,
116 HyperliquidExecPlaceOrderRequest, HyperliquidExecTif, HyperliquidExecTpSl,
117 HyperliquidExecTriggerParams,
118};
119
120pub fn serialize_decimal_as_str<S>(decimal: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
122where
123 S: Serializer,
124{
125 serializer.serialize_str(&decimal.normalize().to_string())
126}
127
128pub fn deserialize_decimal_from_str<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
130where
131 D: Deserializer<'de>,
132{
133 let s = String::deserialize(deserializer)?;
134 Decimal::from_str(&s).map_err(serde::de::Error::custom)
135}
136
137pub fn serialize_optional_decimal_as_str<S>(
139 decimal: &Option<Decimal>,
140 serializer: S,
141) -> Result<S::Ok, S::Error>
142where
143 S: Serializer,
144{
145 match decimal {
146 Some(d) => serializer.serialize_str(&d.normalize().to_string()),
147 None => serializer.serialize_none(),
148 }
149}
150
151pub fn deserialize_optional_decimal_from_str<'de, D>(
153 deserializer: D,
154) -> Result<Option<Decimal>, D::Error>
155where
156 D: Deserializer<'de>,
157{
158 let opt = Option::<String>::deserialize(deserializer)?;
159 match opt {
160 Some(s) => {
161 let decimal = Decimal::from_str(&s).map_err(serde::de::Error::custom)?;
162 Ok(Some(decimal))
163 }
164 None => Ok(None),
165 }
166}
167
168pub fn serialize_vec_decimal_as_str<S>(
170 decimals: &Vec<Decimal>,
171 serializer: S,
172) -> Result<S::Ok, S::Error>
173where
174 S: Serializer,
175{
176 use serde::ser::SerializeSeq;
177 let mut seq = serializer.serialize_seq(Some(decimals.len()))?;
178 for decimal in decimals {
179 seq.serialize_element(&decimal.normalize().to_string())?;
180 }
181 seq.end()
182}
183
184pub fn deserialize_vec_decimal_from_str<'de, D>(deserializer: D) -> Result<Vec<Decimal>, D::Error>
186where
187 D: Deserializer<'de>,
188{
189 let strings = Vec::<String>::deserialize(deserializer)?;
190 strings
191 .into_iter()
192 .map(|s| Decimal::from_str(&s).map_err(serde::de::Error::custom))
193 .collect()
194}
195
196#[inline]
202pub fn round_down_to_tick(price: Decimal, tick_size: Decimal) -> Decimal {
203 if tick_size.is_zero() {
204 return price;
205 }
206 (price / tick_size).floor() * tick_size
207}
208
209#[inline]
211pub fn round_down_to_step(qty: Decimal, step_size: Decimal) -> Decimal {
212 if step_size.is_zero() {
213 return qty;
214 }
215 (qty / step_size).floor() * step_size
216}
217
218#[inline]
220pub fn ensure_min_notional(
221 price: Decimal,
222 qty: Decimal,
223 min_notional: Decimal,
224) -> Result<(), String> {
225 let notional = price * qty;
226 if notional < min_notional {
227 Err(format!(
228 "Notional value {} is less than minimum required {}",
229 notional, min_notional
230 ))
231 } else {
232 Ok(())
233 }
234}
235
236pub fn normalize_price(price: Decimal, decimals: u8) -> Decimal {
238 let scale = Decimal::from(10_u64.pow(decimals as u32));
239 (price * scale).floor() / scale
240}
241
242pub fn normalize_quantity(qty: Decimal, decimals: u8) -> Decimal {
244 let scale = Decimal::from(10_u64.pow(decimals as u32));
245 (qty * scale).floor() / scale
246}
247
248pub fn normalize_order(
250 price: Decimal,
251 qty: Decimal,
252 tick_size: Decimal,
253 step_size: Decimal,
254 min_notional: Decimal,
255 price_decimals: u8,
256 size_decimals: u8,
257) -> Result<(Decimal, Decimal), String> {
258 let normalized_price = normalize_price(price, price_decimals);
260 let normalized_qty = normalize_quantity(qty, size_decimals);
261
262 let final_price = round_down_to_tick(normalized_price, tick_size);
264 let final_qty = round_down_to_step(normalized_qty, step_size);
265
266 ensure_min_notional(final_price, final_qty, min_notional)?;
268
269 Ok((final_price, final_qty))
270}
271
272pub fn time_in_force_to_hyperliquid_tif(
282 tif: TimeInForce,
283 is_post_only: bool,
284) -> anyhow::Result<HyperliquidExecTif> {
285 match (tif, is_post_only) {
286 (_, true) => Ok(HyperliquidExecTif::Alo), (TimeInForce::Gtc, false) => Ok(HyperliquidExecTif::Gtc),
288 (TimeInForce::Ioc, false) => Ok(HyperliquidExecTif::Ioc),
289 (TimeInForce::Fok, false) => Ok(HyperliquidExecTif::Ioc), _ => anyhow::bail!("Unsupported time in force for Hyperliquid: {tif:?}"),
291 }
292}
293
294pub fn extract_asset_id_from_symbol(symbol: &str) -> anyhow::Result<AssetId> {
303 let base = if let Some(base) = symbol.strip_suffix("-PERP") {
305 base.strip_suffix("-USD")
307 .ok_or_else(|| anyhow::anyhow!("Cannot extract asset from symbol: {symbol}"))?
308 } else if let Some(base) = symbol.strip_suffix("-USD") {
309 base
311 } else {
312 anyhow::bail!("Cannot extract asset ID from symbol: {symbol}")
313 };
314
315 Ok(match base {
322 "SOL" => 0, "APT" => 1, "ATOM" => 2, "BTC" => 3, "ETH" => 4, "MATIC" => 5, "BNB" => 6, "AVAX" => 7, "DYDX" => 9, "APE" => 10, "OP" => 11, "kPEPE" => 12, "ARB" => 13, "kSHIB" => 29, "WIF" => 78, "DOGE" => 173, _ => {
339 anyhow::bail!("Asset ID mapping not found for symbol: {symbol}")
341 }
342 })
343}
344
345fn determine_tpsl_type(
358 order_type: OrderType,
359 order_side: OrderSide,
360 trigger_price: Decimal,
361 current_price: Option<Decimal>,
362) -> HyperliquidExecTpSl {
363 match order_type {
364 OrderType::StopMarket | OrderType::StopLimit => HyperliquidExecTpSl::Sl,
366
367 OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidExecTpSl::Tp,
369
370 _ => {
372 if let Some(current) = current_price {
373 match order_side {
374 OrderSide::Buy => {
375 if trigger_price > current {
377 HyperliquidExecTpSl::Sl
378 } else {
379 HyperliquidExecTpSl::Tp
380 }
381 }
382 OrderSide::Sell => {
383 if trigger_price < current {
385 HyperliquidExecTpSl::Sl
386 } else {
387 HyperliquidExecTpSl::Tp
388 }
389 }
390 _ => HyperliquidExecTpSl::Sl, }
392 } else {
393 HyperliquidExecTpSl::Sl
395 }
396 }
397 }
398}
399
400pub fn order_to_hyperliquid_request(
424 order: &OrderAny,
425) -> anyhow::Result<HyperliquidExecPlaceOrderRequest> {
426 let instrument_id = order.instrument_id();
427 let symbol = instrument_id.symbol.as_str();
428 let asset = extract_asset_id_from_symbol(symbol)
429 .with_context(|| format!("Failed to extract asset ID from symbol: {}", symbol))?;
430
431 let is_buy = matches!(order.order_side(), OrderSide::Buy);
432 let reduce_only = order.is_reduce_only();
433 let order_side = order.order_side();
434 let order_type = order.order_type();
435
436 let price_decimal = match order.price() {
438 Some(price) => Decimal::from_str_exact(&price.to_string())
439 .with_context(|| format!("Failed to convert price to decimal: {}", price))?,
440 None => {
441 if matches!(
444 order_type,
445 OrderType::Market | OrderType::StopMarket | OrderType::MarketIfTouched
446 ) {
447 Decimal::ZERO
448 } else {
449 anyhow::bail!("Limit orders require a price")
450 }
451 }
452 };
453
454 let size_decimal =
456 Decimal::from_str_exact(&order.quantity().to_string()).with_context(|| {
457 format!(
458 "Failed to convert quantity to decimal: {}",
459 order.quantity()
460 )
461 })?;
462
463 let kind = match order_type {
465 OrderType::Market => {
466 HyperliquidExecOrderKind::Limit {
468 limit: HyperliquidExecLimitParams {
469 tif: HyperliquidExecTif::Ioc,
470 },
471 }
472 }
473 OrderType::Limit => {
474 let tif =
475 time_in_force_to_hyperliquid_tif(order.time_in_force(), order.is_post_only())?;
476 HyperliquidExecOrderKind::Limit {
477 limit: HyperliquidExecLimitParams { tif },
478 }
479 }
480 OrderType::StopMarket => {
481 if let Some(trigger_price) = order.trigger_price() {
482 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
483 .with_context(|| {
484 format!(
485 "Failed to convert trigger price to decimal: {}",
486 trigger_price
487 )
488 })?;
489
490 let tpsl = determine_tpsl_type(
492 order_type,
493 order_side,
494 trigger_price_decimal,
495 None, );
497
498 HyperliquidExecOrderKind::Trigger {
499 trigger: HyperliquidExecTriggerParams {
500 is_market: true,
501 trigger_px: trigger_price_decimal,
502 tpsl,
503 },
504 }
505 } else {
506 anyhow::bail!("Stop market orders require a trigger price")
507 }
508 }
509 OrderType::StopLimit => {
510 if let Some(trigger_price) = order.trigger_price() {
511 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
512 .with_context(|| {
513 format!(
514 "Failed to convert trigger price to decimal: {}",
515 trigger_price
516 )
517 })?;
518
519 let tpsl = determine_tpsl_type(order_type, order_side, trigger_price_decimal, None);
521
522 HyperliquidExecOrderKind::Trigger {
523 trigger: HyperliquidExecTriggerParams {
524 is_market: false,
525 trigger_px: trigger_price_decimal,
526 tpsl,
527 },
528 }
529 } else {
530 anyhow::bail!("Stop limit orders require a trigger price")
531 }
532 }
533 OrderType::MarketIfTouched => {
534 if let Some(trigger_price) = order.trigger_price() {
537 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
538 .with_context(|| {
539 format!(
540 "Failed to convert trigger price to decimal: {}",
541 trigger_price
542 )
543 })?;
544
545 HyperliquidExecOrderKind::Trigger {
546 trigger: HyperliquidExecTriggerParams {
547 is_market: true,
548 trigger_px: trigger_price_decimal,
549 tpsl: HyperliquidExecTpSl::Tp, },
551 }
552 } else {
553 anyhow::bail!("Market-if-touched orders require a trigger price")
554 }
555 }
556 OrderType::LimitIfTouched => {
557 if let Some(trigger_price) = order.trigger_price() {
560 let trigger_price_decimal = Decimal::from_str_exact(&trigger_price.to_string())
561 .with_context(|| {
562 format!(
563 "Failed to convert trigger price to decimal: {}",
564 trigger_price
565 )
566 })?;
567
568 HyperliquidExecOrderKind::Trigger {
569 trigger: HyperliquidExecTriggerParams {
570 is_market: false,
571 trigger_px: trigger_price_decimal,
572 tpsl: HyperliquidExecTpSl::Tp, },
574 }
575 } else {
576 anyhow::bail!("Limit-if-touched orders require a trigger price")
577 }
578 }
579 _ => anyhow::bail!("Unsupported order type for Hyperliquid: {:?}", order_type),
580 };
581
582 let cloid = match Cloid::from_hex(order.client_order_id()) {
584 Ok(cloid) => Some(cloid),
585 Err(e) => {
586 anyhow::bail!(
587 "Failed to convert client order ID '{}' to CLOID: {}",
588 order.client_order_id(),
589 e
590 )
591 }
592 };
593
594 Ok(HyperliquidExecPlaceOrderRequest {
595 asset,
596 is_buy,
597 price: price_decimal,
598 size: size_decimal,
599 reduce_only,
600 kind,
601 cloid,
602 })
603}
604
605pub fn orders_to_hyperliquid_requests(
607 orders: &[&OrderAny],
608) -> anyhow::Result<Vec<HyperliquidExecPlaceOrderRequest>> {
609 orders
610 .iter()
611 .map(|order| order_to_hyperliquid_request(order))
612 .collect()
613}
614
615pub fn orders_to_hyperliquid_action_value(orders: &[&OrderAny]) -> anyhow::Result<Value> {
617 let requests = orders_to_hyperliquid_requests(orders)?;
618 serde_json::to_value(requests).context("Failed to serialize orders to JSON")
619}
620
621pub fn order_any_to_hyperliquid_request(
623 order: &OrderAny,
624) -> anyhow::Result<HyperliquidExecPlaceOrderRequest> {
625 order_to_hyperliquid_request(order)
626}
627
628pub fn client_order_id_to_cancel_request(
634 client_order_id: &str,
635 symbol: &str,
636) -> anyhow::Result<HyperliquidExecCancelByCloidRequest> {
637 let asset = extract_asset_id_from_symbol(symbol)
638 .with_context(|| format!("Failed to extract asset ID from symbol: {}", symbol))?;
639
640 let cloid = Cloid::from_hex(client_order_id).map_err(|e| {
641 anyhow::anyhow!(
642 "Failed to convert client order ID '{}' to CLOID: {}",
643 client_order_id,
644 e
645 )
646 })?;
647
648 Ok(HyperliquidExecCancelByCloidRequest { asset, cloid })
649}
650
651pub fn is_response_successful(response: &HyperliquidExchangeResponse) -> bool {
653 matches!(response, HyperliquidExchangeResponse::Status { status, .. } if status == "ok")
654}
655
656pub fn extract_error_message(response: &HyperliquidExchangeResponse) -> String {
658 match response {
659 HyperliquidExchangeResponse::Status { status, response } => {
660 if status == "ok" {
661 "Operation successful".to_string()
662 } else {
663 if let Some(error_msg) = response.get("error").and_then(|v| v.as_str()) {
665 error_msg.to_string()
666 } else {
667 format!("Request failed with status: {}", status)
668 }
669 }
670 }
671 HyperliquidExchangeResponse::Error { error } => error.clone(),
672 }
673}
674
675pub fn is_conditional_order_data(trigger_px: Option<&str>, tpsl: Option<&str>) -> bool {
686 trigger_px.is_some() && tpsl.is_some()
687}
688
689pub fn parse_trigger_order_type(is_market: bool, tpsl: &str) -> OrderType {
700 match (is_market, tpsl) {
701 (true, "sl") => OrderType::StopMarket,
702 (false, "sl") => OrderType::StopLimit,
703 (true, "tp") => OrderType::MarketIfTouched,
704 (false, "tp") => OrderType::LimitIfTouched,
705 _ => OrderType::StopMarket, }
707}
708
709pub fn parse_order_status_with_trigger(
720 status: &str,
721 trigger_activated: Option<bool>,
722) -> (OrderStatus, Option<String>) {
723 use crate::common::enums::hyperliquid_status_to_order_status;
724
725 let base_status = hyperliquid_status_to_order_status(status);
726
727 if let Some(activated) = trigger_activated {
729 let trigger_status = if activated {
730 Some("activated".to_string())
731 } else {
732 Some("pending".to_string())
733 };
734 (base_status, trigger_status)
735 } else {
736 (base_status, None)
737 }
738}
739
740pub fn format_trailing_stop_info(
752 offset: &str,
753 offset_type: &str,
754 callback_price: Option<&str>,
755) -> String {
756 let offset_desc = match offset_type {
757 "percentage" => format!("{}%", offset),
758 "basisPoints" => format!("{} bps", offset),
759 "price" => offset.to_string(),
760 _ => offset.to_string(),
761 };
762
763 if let Some(callback) = callback_price {
764 format!(
765 "Trailing stop: {} offset, callback at {}",
766 offset_desc, callback
767 )
768 } else {
769 format!("Trailing stop: {} offset", offset_desc)
770 }
771}
772
773pub fn validate_conditional_order_params(
789 trigger_px: Option<&str>,
790 tpsl: Option<&str>,
791 is_market: Option<bool>,
792) -> anyhow::Result<()> {
793 if trigger_px.is_none() {
794 anyhow::bail!("Conditional order missing trigger price");
795 }
796
797 if tpsl.is_none() {
798 anyhow::bail!("Conditional order missing tpsl indicator");
799 }
800
801 let tpsl_value = tpsl.expect("tpsl should be Some at this point");
802 if tpsl_value != "tp" && tpsl_value != "sl" {
803 anyhow::bail!("Invalid tpsl value: {}", tpsl_value);
804 }
805
806 if is_market.is_none() {
807 anyhow::bail!("Conditional order missing is_market flag");
808 }
809
810 Ok(())
811}
812
813pub fn parse_trigger_price(trigger_px: &str) -> anyhow::Result<Decimal> {
823 Decimal::from_str_exact(trigger_px)
824 .with_context(|| format!("Failed to parse trigger price: {}", trigger_px))
825}
826
827pub fn parse_account_balances_and_margins(
833 cross_margin_summary: &CrossMarginSummary,
834) -> anyhow::Result<(Vec<AccountBalance>, Vec<MarginBalance>)> {
835 let mut balances = Vec::new();
836 let mut margins = Vec::new();
837
838 let currency = Currency::USD(); let total_value = cross_margin_summary
843 .account_value
844 .to_string()
845 .parse::<f64>()?;
846
847 let withdrawable = cross_margin_summary
849 .withdrawable
850 .to_string()
851 .parse::<f64>()?;
852
853 let margin_used = cross_margin_summary
855 .total_margin_used
856 .to_string()
857 .parse::<f64>()?;
858
859 let total = Money::new(total_value, currency);
861 let locked = Money::new(margin_used, currency);
862 let free = Money::new(withdrawable, currency);
863
864 let balance = AccountBalance::new(total, locked, free);
865 balances.push(balance);
866
867 if margin_used > 0.0 {
872 let margin_instrument_id =
873 InstrumentId::new(Symbol::new("ACCOUNT"), Venue::new("HYPERLIQUID"));
874
875 let initial_margin = Money::new(margin_used, currency);
876 let maintenance_margin = Money::new(margin_used, currency);
877
878 let margin_balance =
879 MarginBalance::new(initial_margin, maintenance_margin, margin_instrument_id);
880
881 margins.push(margin_balance);
882 }
883
884 Ok((balances, margins))
885}
886
887#[cfg(test)]
892mod tests {
893 use rstest::rstest;
894 use serde::{Deserialize, Serialize};
895
896 use super::*;
897
898 #[derive(Serialize, Deserialize)]
899 struct TestStruct {
900 #[serde(
901 serialize_with = "serialize_decimal_as_str",
902 deserialize_with = "deserialize_decimal_from_str"
903 )]
904 value: Decimal,
905 #[serde(
906 serialize_with = "serialize_optional_decimal_as_str",
907 deserialize_with = "deserialize_optional_decimal_from_str"
908 )]
909 optional_value: Option<Decimal>,
910 }
911
912 #[rstest]
913 fn test_decimal_serialization_roundtrip() {
914 let original = TestStruct {
915 value: Decimal::from_str("123.456789012345678901234567890").unwrap(),
916 optional_value: Some(Decimal::from_str("0.000000001").unwrap()),
917 };
918
919 let json = serde_json::to_string(&original).unwrap();
920 println!("Serialized: {}", json);
921
922 assert!(json.contains("\"123.45678901234567890123456789\""));
924 assert!(json.contains("\"0.000000001\""));
925
926 let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
927 assert_eq!(original.value, deserialized.value);
928 assert_eq!(original.optional_value, deserialized.optional_value);
929 }
930
931 #[rstest]
932 fn test_decimal_precision_preservation() {
933 let test_cases = [
934 "0",
935 "1",
936 "0.1",
937 "0.01",
938 "0.001",
939 "123.456789012345678901234567890",
940 "999999999999999999.999999999999999999",
941 ];
942
943 for case in test_cases {
944 let decimal = Decimal::from_str(case).unwrap();
945 let test_struct = TestStruct {
946 value: decimal,
947 optional_value: Some(decimal),
948 };
949
950 let json = serde_json::to_string(&test_struct).unwrap();
951 let parsed: TestStruct = serde_json::from_str(&json).unwrap();
952
953 assert_eq!(decimal, parsed.value, "Failed for case: {}", case);
954 assert_eq!(
955 Some(decimal),
956 parsed.optional_value,
957 "Failed for case: {}",
958 case
959 );
960 }
961 }
962
963 #[rstest]
964 fn test_optional_none_handling() {
965 let test_struct = TestStruct {
966 value: Decimal::from_str("42.0").unwrap(),
967 optional_value: None,
968 };
969
970 let json = serde_json::to_string(&test_struct).unwrap();
971 assert!(json.contains("null"));
972
973 let parsed: TestStruct = serde_json::from_str(&json).unwrap();
974 assert_eq!(test_struct.value, parsed.value);
975 assert_eq!(None, parsed.optional_value);
976 }
977
978 #[rstest]
979 fn test_round_down_to_tick() {
980 use rust_decimal_macros::dec;
981
982 assert_eq!(round_down_to_tick(dec!(100.07), dec!(0.05)), dec!(100.05));
983 assert_eq!(round_down_to_tick(dec!(100.03), dec!(0.05)), dec!(100.00));
984 assert_eq!(round_down_to_tick(dec!(100.05), dec!(0.05)), dec!(100.05));
985
986 assert_eq!(round_down_to_tick(dec!(100.07), dec!(0)), dec!(100.07));
988 }
989
990 #[rstest]
991 fn test_round_down_to_step() {
992 use rust_decimal_macros::dec;
993
994 assert_eq!(
995 round_down_to_step(dec!(0.12349), dec!(0.0001)),
996 dec!(0.1234)
997 );
998 assert_eq!(round_down_to_step(dec!(1.5555), dec!(0.1)), dec!(1.5));
999 assert_eq!(round_down_to_step(dec!(1.0001), dec!(0.0001)), dec!(1.0001));
1000
1001 assert_eq!(round_down_to_step(dec!(0.12349), dec!(0)), dec!(0.12349));
1003 }
1004
1005 #[rstest]
1006 fn test_min_notional_validation() {
1007 use rust_decimal_macros::dec;
1008
1009 assert!(ensure_min_notional(dec!(100), dec!(0.1), dec!(10)).is_ok());
1011 assert!(ensure_min_notional(dec!(100), dec!(0.11), dec!(10)).is_ok());
1012
1013 assert!(ensure_min_notional(dec!(100), dec!(0.05), dec!(10)).is_err());
1015 assert!(ensure_min_notional(dec!(1), dec!(5), dec!(10)).is_err());
1016
1017 assert!(ensure_min_notional(dec!(100), dec!(0.1), dec!(10)).is_ok());
1019 }
1020
1021 #[rstest]
1022 fn test_normalize_price() {
1023 use rust_decimal_macros::dec;
1024
1025 assert_eq!(normalize_price(dec!(100.12345), 2), dec!(100.12));
1026 assert_eq!(normalize_price(dec!(100.19999), 2), dec!(100.19));
1027 assert_eq!(normalize_price(dec!(100.999), 0), dec!(100));
1028 assert_eq!(normalize_price(dec!(100.12345), 4), dec!(100.1234));
1029 }
1030
1031 #[rstest]
1032 fn test_normalize_quantity() {
1033 use rust_decimal_macros::dec;
1034
1035 assert_eq!(normalize_quantity(dec!(1.12345), 3), dec!(1.123));
1036 assert_eq!(normalize_quantity(dec!(1.99999), 3), dec!(1.999));
1037 assert_eq!(normalize_quantity(dec!(1.999), 0), dec!(1));
1038 assert_eq!(normalize_quantity(dec!(1.12345), 5), dec!(1.12345));
1039 }
1040
1041 #[rstest]
1042 fn test_normalize_order_complete() {
1043 use rust_decimal_macros::dec;
1044
1045 let result = normalize_order(
1046 dec!(100.12345), dec!(0.123456), dec!(0.01), dec!(0.0001), dec!(10), 2, 4, );
1054
1055 assert!(result.is_ok());
1056 let (price, qty) = result.unwrap();
1057 assert_eq!(price, dec!(100.12)); assert_eq!(qty, dec!(0.1234)); }
1060
1061 #[rstest]
1062 fn test_normalize_order_min_notional_fail() {
1063 use rust_decimal_macros::dec;
1064
1065 let result = normalize_order(
1066 dec!(100.12345), dec!(0.05), dec!(0.01), dec!(0.0001), dec!(10), 2, 4, );
1074
1075 assert!(result.is_err());
1076 assert!(result.unwrap_err().contains("Notional value"));
1077 }
1078
1079 #[rstest]
1080 fn test_edge_cases() {
1081 use rust_decimal_macros::dec;
1082
1083 assert_eq!(
1085 round_down_to_tick(dec!(0.000001), dec!(0.000001)),
1086 dec!(0.000001)
1087 );
1088
1089 assert_eq!(round_down_to_tick(dec!(999999.99), dec!(1.0)), dec!(999999));
1091
1092 assert_eq!(
1094 round_down_to_tick(dec!(100.009999), dec!(0.01)),
1095 dec!(100.00)
1096 );
1097 }
1098
1099 #[rstest]
1104 fn test_is_conditional_order_data() {
1105 assert!(is_conditional_order_data(Some("50000.0"), Some("sl")));
1107
1108 assert!(!is_conditional_order_data(Some("50000.0"), None));
1110
1111 assert!(!is_conditional_order_data(None, Some("tp")));
1113
1114 assert!(!is_conditional_order_data(None, None));
1116 }
1117
1118 #[rstest]
1119 fn test_parse_trigger_order_type() {
1120 assert_eq!(parse_trigger_order_type(true, "sl"), OrderType::StopMarket);
1122
1123 assert_eq!(parse_trigger_order_type(false, "sl"), OrderType::StopLimit);
1125
1126 assert_eq!(
1128 parse_trigger_order_type(true, "tp"),
1129 OrderType::MarketIfTouched
1130 );
1131
1132 assert_eq!(
1134 parse_trigger_order_type(false, "tp"),
1135 OrderType::LimitIfTouched
1136 );
1137 }
1138
1139 #[rstest]
1140 fn test_parse_order_status_with_trigger() {
1141 let (status, trigger_status) = parse_order_status_with_trigger("open", Some(true));
1143 assert_eq!(status, OrderStatus::Accepted);
1144 assert_eq!(trigger_status, Some("activated".to_string()));
1145
1146 let (status, trigger_status) = parse_order_status_with_trigger("open", Some(false));
1148 assert_eq!(status, OrderStatus::Accepted);
1149 assert_eq!(trigger_status, Some("pending".to_string()));
1150
1151 let (status, trigger_status) = parse_order_status_with_trigger("open", None);
1153 assert_eq!(status, OrderStatus::Accepted);
1154 assert_eq!(trigger_status, None);
1155 }
1156
1157 #[rstest]
1158 fn test_format_trailing_stop_info() {
1159 let info = format_trailing_stop_info("100.0", "price", Some("50000.0"));
1161 assert!(info.contains("100.0"));
1162 assert!(info.contains("callback at 50000.0"));
1163
1164 let info = format_trailing_stop_info("5.0", "percentage", None);
1166 assert!(info.contains("5.0%"));
1167 assert!(info.contains("Trailing stop"));
1168
1169 let info = format_trailing_stop_info("250", "basisPoints", Some("49000.0"));
1171 assert!(info.contains("250 bps"));
1172 assert!(info.contains("49000.0"));
1173 }
1174
1175 #[rstest]
1176 fn test_parse_trigger_price() {
1177 use rust_decimal_macros::dec;
1178
1179 let result = parse_trigger_price("50000.0");
1181 assert!(result.is_ok());
1182 assert_eq!(result.unwrap(), dec!(50000.0));
1183
1184 let result = parse_trigger_price("49000");
1186 assert!(result.is_ok());
1187 assert_eq!(result.unwrap(), dec!(49000));
1188
1189 let result = parse_trigger_price("invalid");
1191 assert!(result.is_err());
1192
1193 let result = parse_trigger_price("");
1195 assert!(result.is_err());
1196 }
1197}