1use std::fmt::Display;
19
20use nautilus_model::{
21 data::{BarSpecification, BarType},
22 enums::{AggregationSource, BarAggregation, OrderSide, OrderType, PriceType},
23};
24use serde::{Deserialize, Serialize};
25use serde_repr::{Deserialize_repr, Serialize_repr};
26use strum::{AsRefStr, Display, EnumIter, EnumString};
27
28#[derive(
30 Copy,
31 Clone,
32 Debug,
33 Default,
34 Display,
35 PartialEq,
36 Eq,
37 Hash,
38 AsRefStr,
39 EnumIter,
40 EnumString,
41 Serialize,
42 Deserialize,
43)]
44#[serde(rename_all = "lowercase")]
45#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
46#[cfg_attr(
47 feature = "python",
48 pyo3::pyclass(
49 eq,
50 eq_int,
51 module = "nautilus_trader.adapters.lighter",
52 from_py_object,
53 rename_all = "SCREAMING_SNAKE_CASE",
54 )
55)]
56#[cfg_attr(
57 feature = "python",
58 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.lighter")
59)]
60pub enum LighterDeployment {
61 #[default]
63 Lighter,
64 Robinhood,
66}
67
68#[derive(
70 Copy,
71 Clone,
72 Debug,
73 Default,
74 Display,
75 PartialEq,
76 Eq,
77 Hash,
78 AsRefStr,
79 EnumIter,
80 EnumString,
81 Serialize,
82 Deserialize,
83)]
84#[serde(rename_all = "lowercase")]
85#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
86#[cfg_attr(
87 feature = "python",
88 pyo3::pyclass(
89 eq,
90 eq_int,
91 module = "nautilus_trader.adapters.lighter",
92 from_py_object,
93 rename_all = "SCREAMING_SNAKE_CASE",
94 )
95)]
96#[cfg_attr(
97 feature = "python",
98 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.lighter")
99)]
100pub enum LighterEnvironment {
101 #[default]
103 Mainnet,
104 Testnet,
106}
107
108#[derive(
110 Copy,
111 Clone,
112 Debug,
113 Display,
114 PartialEq,
115 Eq,
116 Hash,
117 AsRefStr,
118 EnumIter,
119 EnumString,
120 Serialize,
121 Deserialize,
122)]
123#[serde(rename_all = "lowercase")]
124#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
125pub enum LighterProductType {
126 Perp,
128 Spot,
130}
131
132#[derive(
134 Copy,
135 Clone,
136 Debug,
137 Display,
138 PartialEq,
139 Eq,
140 Hash,
141 AsRefStr,
142 EnumIter,
143 EnumString,
144 Serialize,
145 Deserialize,
146)]
147#[strum(ascii_case_insensitive)]
148pub enum LighterCandleResolution {
149 #[serde(rename = "1m")]
151 #[strum(serialize = "1m")]
152 OneMinute,
153 #[serde(rename = "5m")]
155 #[strum(serialize = "5m")]
156 FiveMinute,
157 #[serde(rename = "15m")]
159 #[strum(serialize = "15m")]
160 FifteenMinute,
161 #[serde(rename = "30m")]
163 #[strum(serialize = "30m")]
164 ThirtyMinute,
165 #[serde(rename = "1h")]
167 #[strum(serialize = "1h")]
168 OneHour,
169 #[serde(rename = "4h")]
171 #[strum(serialize = "4h")]
172 FourHour,
173 #[serde(rename = "12h")]
175 #[strum(serialize = "12h")]
176 TwelveHour,
177 #[serde(rename = "1d")]
179 #[strum(serialize = "1d")]
180 OneDay,
181 #[serde(rename = "1w")]
183 #[strum(serialize = "1w")]
184 OneWeek,
185}
186
187impl LighterCandleResolution {
188 #[must_use]
190 pub const fn as_str(self) -> &'static str {
191 match self {
192 Self::OneMinute => "1m",
193 Self::FiveMinute => "5m",
194 Self::FifteenMinute => "15m",
195 Self::ThirtyMinute => "30m",
196 Self::OneHour => "1h",
197 Self::FourHour => "4h",
198 Self::TwelveHour => "12h",
199 Self::OneDay => "1d",
200 Self::OneWeek => "1w",
201 }
202 }
203
204 #[must_use]
206 pub const fn interval_millis(self) -> i64 {
207 match self {
208 Self::OneMinute => 60_000,
209 Self::FiveMinute => 5 * 60_000,
210 Self::FifteenMinute => 15 * 60_000,
211 Self::ThirtyMinute => 30 * 60_000,
212 Self::OneHour => 60 * 60_000,
213 Self::FourHour => 4 * 60 * 60_000,
214 Self::TwelveHour => 12 * 60 * 60_000,
215 Self::OneDay => 24 * 60 * 60_000,
216 Self::OneWeek => 7 * 24 * 60 * 60_000,
217 }
218 }
219
220 #[must_use]
222 pub fn to_bar_spec(self) -> BarSpecification {
223 let (step, aggregation) = match self {
224 Self::OneMinute => (1, BarAggregation::Minute),
225 Self::FiveMinute => (5, BarAggregation::Minute),
226 Self::FifteenMinute => (15, BarAggregation::Minute),
227 Self::ThirtyMinute => (30, BarAggregation::Minute),
228 Self::OneHour => (1, BarAggregation::Hour),
229 Self::FourHour => (4, BarAggregation::Hour),
230 Self::TwelveHour => (12, BarAggregation::Hour),
231 Self::OneDay => (1, BarAggregation::Day),
232 Self::OneWeek => (1, BarAggregation::Week),
233 };
234 BarSpecification::new(step, aggregation, PriceType::Last)
235 }
236
237 #[must_use]
241 pub const fn is_ws_streamable(self) -> bool {
242 !matches!(self, Self::OneWeek)
243 }
244}
245
246impl TryFrom<&BarType> for LighterCandleResolution {
247 type Error = anyhow::Error;
248
249 fn try_from(value: &BarType) -> Result<Self, Self::Error> {
250 anyhow::ensure!(
251 value.aggregation_source() == AggregationSource::External,
252 "Lighter candles only support EXTERNAL aggregation",
253 );
254
255 let spec = value.spec();
256 anyhow::ensure!(
257 spec.price_type == PriceType::Last,
258 "Lighter candles only support LAST price type",
259 );
260
261 let step = spec.step.get();
262 match spec.aggregation {
263 BarAggregation::Minute => match step {
264 1 => Ok(Self::OneMinute),
265 5 => Ok(Self::FiveMinute),
266 15 => Ok(Self::FifteenMinute),
267 30 => Ok(Self::ThirtyMinute),
268 _ => anyhow::bail!("unsupported Lighter candle minute step: {step}"),
269 },
270 BarAggregation::Hour => match step {
271 1 => Ok(Self::OneHour),
272 4 => Ok(Self::FourHour),
273 12 => Ok(Self::TwelveHour),
274 _ => anyhow::bail!("unsupported Lighter candle hour step: {step}"),
275 },
276 BarAggregation::Day => match step {
277 1 => Ok(Self::OneDay),
278 _ => anyhow::bail!("unsupported Lighter candle day step: {step}"),
279 },
280 BarAggregation::Week => match step {
281 1 => Ok(Self::OneWeek),
282 _ => anyhow::bail!("unsupported Lighter candle week step: {step}"),
283 },
284 other => anyhow::bail!("unsupported Lighter candle aggregation: {other}"),
285 }
286 }
287}
288
289#[derive(
291 Copy,
292 Clone,
293 Debug,
294 Default,
295 Display,
296 PartialEq,
297 Eq,
298 Hash,
299 AsRefStr,
300 EnumIter,
301 EnumString,
302 Serialize,
303 Deserialize,
304)]
305#[strum(ascii_case_insensitive)]
306pub enum LighterFundingResolution {
307 #[default]
309 #[serde(rename = "1h")]
310 #[strum(serialize = "1h")]
311 OneHour,
312 #[serde(rename = "1d")]
314 #[strum(serialize = "1d")]
315 OneDay,
316}
317
318impl LighterFundingResolution {
319 #[must_use]
321 pub const fn interval_minutes(self) -> u16 {
322 match self {
323 Self::OneHour => 60,
324 Self::OneDay => 24 * 60,
325 }
326 }
327
328 #[must_use]
330 pub const fn interval_millis(self) -> i64 {
331 match self {
332 Self::OneHour => 60 * 60_000,
333 Self::OneDay => 24 * 60 * 60_000,
334 }
335 }
336}
337
338#[derive(
340 Copy,
341 Clone,
342 Debug,
343 Default,
344 Display,
345 PartialEq,
346 Eq,
347 Hash,
348 AsRefStr,
349 EnumIter,
350 EnumString,
351 Serialize,
352 Deserialize,
353)]
354#[serde(rename_all = "lowercase")]
355#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
356pub enum LighterOrderBookFilter {
357 #[default]
359 All,
360 Perp,
362 Spot,
364}
365
366#[derive(
368 Copy,
369 Clone,
370 Debug,
371 Display,
372 PartialEq,
373 Eq,
374 Hash,
375 AsRefStr,
376 EnumIter,
377 EnumString,
378 Serialize,
379 Deserialize,
380)]
381#[serde(rename_all = "kebab-case")]
382#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
383pub enum LighterMarketStatus {
384 Inactive,
386 Active,
388}
389
390#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)]
392#[repr(i64)]
393pub enum LighterTxStatus {
394 Failed = 0,
395 Pending = 1,
396 Executed = 2,
397 PendingFinal = 3,
398}
399
400#[derive(
402 Copy,
403 Clone,
404 Debug,
405 Display,
406 PartialEq,
407 Eq,
408 Hash,
409 AsRefStr,
410 EnumIter,
411 EnumString,
412 Serialize,
413 Deserialize,
414)]
415#[serde(rename_all = "kebab-case")]
416#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
417pub enum LighterOrderKind {
418 Limit,
419 Market,
420 StopLoss,
421 StopLossLimit,
422 TakeProfit,
423 TakeProfitLimit,
424 Twap,
425 TwapSub,
426 Liquidation,
427}
428
429#[derive(
431 Copy,
432 Clone,
433 Debug,
434 Display,
435 PartialEq,
436 Eq,
437 Hash,
438 AsRefStr,
439 EnumIter,
440 EnumString,
441 Serialize,
442 Deserialize,
443)]
444#[serde(rename_all = "kebab-case")]
445#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
446pub enum LighterOrderTimeInForce {
447 GoodTillTime,
448 ImmediateOrCancel,
449 PostOnly,
450 #[serde(alias = "unknown")]
451 #[serde(rename = "Unknown")]
452 #[strum(serialize = "Unknown")]
453 Unknown,
454}
455
456#[derive(
458 Copy,
459 Clone,
460 Debug,
461 Display,
462 PartialEq,
463 Eq,
464 Hash,
465 AsRefStr,
466 EnumIter,
467 EnumString,
468 Serialize,
469 Deserialize,
470)]
471#[serde(rename_all = "kebab-case")]
472#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
473pub enum LighterOrderStatus {
474 InProgress,
475 Pending,
476 Open,
477 Filled,
478 Canceled,
479 CanceledPostOnly,
480 CanceledReduceOnly,
481 CanceledPositionNotAllowed,
482 CanceledMarginNotAllowed,
483 CanceledTooMuchSlippage,
484 CanceledNotEnoughLiquidity,
485 CanceledSelfTrade,
486 CanceledExpired,
487 CanceledOco,
488 CanceledChild,
489 CanceledLiquidation,
490 CanceledInvalidBalance,
491}
492
493impl LighterOrderStatus {
494 #[must_use]
501 pub fn as_cancel_reason(self) -> Option<&'static str> {
502 match self {
503 Self::CanceledPostOnly => Some("post-only"),
504 Self::CanceledReduceOnly => Some("reduce-only"),
505 Self::CanceledPositionNotAllowed => Some("position-not-allowed"),
506 Self::CanceledMarginNotAllowed => Some("margin-not-allowed"),
507 Self::CanceledTooMuchSlippage => Some("too-much-slippage"),
508 Self::CanceledNotEnoughLiquidity => Some("not-enough-liquidity"),
509 Self::CanceledSelfTrade => Some("self-trade"),
510 Self::CanceledOco => Some("oco"),
511 Self::CanceledChild => Some("child"),
512 Self::CanceledLiquidation => Some("liquidation"),
513 Self::CanceledInvalidBalance => Some("invalid-balance"),
514 _ => None,
515 }
516 }
517
518 pub(crate) fn is_terminal(self) -> bool {
519 !matches!(self, Self::InProgress | Self::Pending | Self::Open)
520 }
521}
522
523#[derive(
525 Copy,
526 Clone,
527 Debug,
528 Display,
529 PartialEq,
530 Eq,
531 Hash,
532 AsRefStr,
533 EnumIter,
534 EnumString,
535 Serialize,
536 Deserialize,
537)]
538#[serde(rename_all = "lowercase")]
539#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
540pub enum LighterOrderSide {
541 Buy,
542 Sell,
543}
544
545#[derive(
547 Copy,
548 Clone,
549 Debug,
550 Display,
551 PartialEq,
552 Eq,
553 Hash,
554 AsRefStr,
555 EnumIter,
556 EnumString,
557 Serialize,
558 Deserialize,
559)]
560#[serde(rename_all = "kebab-case")]
561#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
562pub enum LighterTriggerStatus {
563 Na,
564 Ready,
565 MarkPrice,
566 Twap,
567 ParentOrder,
568}
569
570#[derive(
572 Copy,
573 Clone,
574 Debug,
575 Display,
576 PartialEq,
577 Eq,
578 Hash,
579 AsRefStr,
580 EnumIter,
581 EnumString,
582 Serialize,
583 Deserialize,
584)]
585#[serde(rename_all = "kebab-case")]
586#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
587pub enum LighterTradeType {
588 Trade,
589 Liquidation,
590 Deleverage,
591 MarketSettlement,
592}
593
594#[derive(
598 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
599)]
600#[repr(u8)]
601pub enum LighterOrderType {
602 Limit = 0,
603 Market = 1,
604 StopLoss = 2,
605 StopLossLimit = 3,
606 TakeProfit = 4,
607 TakeProfitLimit = 5,
608 Twap = 6,
609 TwapSub = 7,
610 Liquidation = 8,
611}
612
613impl LighterOrderType {
614 pub fn as_nautilus(self) -> anyhow::Result<OrderType> {
627 match self {
628 Self::Limit => Ok(OrderType::Limit),
629 Self::Market => Ok(OrderType::Market),
630 Self::StopLoss => Ok(OrderType::StopMarket),
631 Self::StopLossLimit => Ok(OrderType::StopLimit),
632 Self::TakeProfit => Ok(OrderType::MarketIfTouched),
633 Self::TakeProfitLimit => Ok(OrderType::LimitIfTouched),
634 Self::Twap | Self::TwapSub | Self::Liquidation => Err(anyhow::anyhow!(
635 "Lighter `{self:?}` has no Nautilus order-type equivalent",
636 )),
637 }
638 }
639}
640
641impl TryFrom<OrderType> for LighterOrderType {
642 type Error = anyhow::Error;
643
644 fn try_from(value: OrderType) -> Result<Self, Self::Error> {
645 match value {
646 OrderType::Limit => Ok(Self::Limit),
647 OrderType::Market => Ok(Self::Market),
648 OrderType::StopMarket => Ok(Self::StopLoss),
649 OrderType::StopLimit => Ok(Self::StopLossLimit),
650 OrderType::MarketIfTouched => Ok(Self::TakeProfit),
651 OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
652 other => Err(anyhow::anyhow!(
653 "Nautilus `{other:?}` has no Lighter order-type equivalent",
654 )),
655 }
656 }
657}
658
659#[derive(
661 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
662)]
663#[repr(u8)]
664pub enum LighterTimeInForce {
665 ImmediateOrCancel = 0,
667 GoodTillTime = 1,
669 PostOnly = 2,
671}
672
673#[derive(
675 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
676)]
677#[repr(u8)]
678pub enum LighterGroupingType {
679 None = 0,
680 OneTriggersTheOther = 1,
681 OneCancelsTheOther = 2,
682 OneTriggersOneCancelsTheOther = 3,
683}
684
685#[derive(
687 Copy,
688 Clone,
689 Debug,
690 Default,
691 Display,
692 PartialEq,
693 Eq,
694 Hash,
695 AsRefStr,
696 EnumString,
697 Serialize_repr,
698 Deserialize_repr,
699)]
700#[repr(u8)]
701#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
702pub enum LighterSelfTradeBehaviorMode {
703 #[default]
704 ExpireMaker = 0,
705 ExpireTaker = 1,
706 CancelBoth = 2,
707 Reduce = 3,
708}
709
710#[derive(
712 Copy,
713 Clone,
714 Debug,
715 Default,
716 Display,
717 PartialEq,
718 Eq,
719 Hash,
720 AsRefStr,
721 EnumString,
722 Serialize_repr,
723 Deserialize_repr,
724)]
725#[repr(u8)]
726#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
727pub enum LighterSelfTradeEqualityMode {
728 #[default]
729 AccountIndex = 0,
730 MasterAccountIndex = 1,
731}
732
733#[derive(
735 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
736)]
737#[repr(u8)]
738pub enum LighterCancelAllTimeInForce {
739 Immediate = 0,
740 Scheduled = 1,
741 AbortScheduled = 2,
742}
743
744#[derive(
746 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
747)]
748#[repr(u8)]
749pub enum LighterAssetMarginMode {
750 Disabled = 0,
751 Enabled = 1,
752}
753
754#[derive(
756 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
757)]
758#[repr(u8)]
759pub enum LighterAssetRouteType {
760 Perps = 0,
761 Spot = 1,
762}
763
764#[derive(
766 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
767)]
768#[repr(u8)]
769pub enum LighterPositionMarginMode {
770 Cross = 0,
771 Isolated = 1,
772}
773
774#[derive(
776 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
777)]
778#[repr(u8)]
779pub enum LighterMarginUpdateDirection {
780 RemoveFromIsolated = 0,
781 AddToIsolated = 1,
782}
783
784#[derive(Copy, Clone, Debug, PartialEq, Eq)]
793pub enum LighterAccountTier {
794 Standard,
795 Premium,
796 Plus,
797 Builder,
798 Unknown(u8),
799}
800
801impl LighterAccountTier {
802 #[must_use]
804 pub const fn from_code(code: u8) -> Self {
805 match code {
806 0 => Self::Standard,
807 1 => Self::Premium,
808 2 => Self::Plus,
809 3 => Self::Builder,
810 other => Self::Unknown(other),
811 }
812 }
813
814 #[must_use]
821 pub const fn documented_rest_quota_per_min(self) -> Option<u32> {
822 match self {
823 Self::Standard => Some(60),
824 Self::Premium => Some(24_000),
825 Self::Plus => Some(24_000),
826 Self::Builder => Some(240_000),
827 Self::Unknown(_) => None,
828 }
829 }
830}
831
832impl Display for LighterAccountTier {
833 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
834 match self {
835 Self::Standard => f.write_str("Standard"),
836 Self::Premium => f.write_str("Premium"),
837 Self::Plus => f.write_str("Plus"),
838 Self::Builder => f.write_str("Builder"),
839 Self::Unknown(code) => write!(f, "Unknown({code})"),
840 }
841 }
842}
843
844pub fn is_ask_from_order_side(side: OrderSide) -> bool {
856 match side {
857 OrderSide::Buy => false,
858 OrderSide::Sell => true,
859 }
860}
861
862#[must_use]
864pub fn order_side_from_is_ask(is_ask: bool) -> OrderSide {
865 if is_ask {
866 OrderSide::Sell
867 } else {
868 OrderSide::Buy
869 }
870}
871
872#[derive(
877 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
878)]
879#[repr(u8)]
880pub enum LighterTxType {
881 Empty = 0,
882 L1Deposit = 1,
883 L1ChangePubKey = 2,
884 L1CreateMarket = 3,
885 L1UpdateMarket = 4,
886 L1CancelAllOrders = 5,
887 L1Withdraw = 6,
888 L1CreateOrder = 7,
889 ChangePubKey = 8,
890 CreateSubAccount = 9,
891 CreatePublicPool = 10,
892 UpdatePublicPool = 11,
893 Transfer = 12,
894 Withdraw = 13,
895 CreateOrder = 14,
896 CancelOrder = 15,
897 CancelAllOrders = 16,
898 ModifyOrder = 17,
899 MintShares = 18,
900 BurnShares = 19,
901 UpdateLeverage = 20,
902 InternalClaimOrder = 21,
903 InternalCancelOrder = 22,
904 InternalDeleverage = 23,
905 InternalExitPosition = 24,
906 InternalCancelAllOrders = 25,
907 InternalLiquidatePosition = 26,
908 InternalCreateOrder = 27,
909 CreateGroupedOrders = 28,
910 UpdateMargin = 29,
911 L1BurnShares = 30,
912 L1RegisterAsset = 31,
913 L1UpdateAsset = 32,
914 CreateStakingPool = 33,
915 StakeAssets = 35,
916 UnstakeAssets = 36,
917 L1UnstakeAssets = 37,
918 L1SetSystemConfig = 38,
919 ForceBurnShares = 40,
920 UpdateAccountConfig = 41,
921 StrategyTransfer = 43,
922 UpdateMarketConfig = 44,
923 ApproveIntegrator = 45,
924}
925
926#[cfg(test)]
927mod tests {
928 use std::str::FromStr;
929
930 use nautilus_model::{
931 data::{BarSpecification, BarType},
932 identifiers::InstrumentId,
933 };
934 use rstest::rstest;
935 use serde_json;
936
937 use super::*;
938
939 #[rstest]
940 fn test_environment_default_is_mainnet() {
941 assert_eq!(LighterEnvironment::default(), LighterEnvironment::Mainnet);
942 }
943
944 #[rstest]
945 fn test_product_type_serde_uses_wire_values() {
946 assert_eq!(
947 serde_json::to_string(&LighterProductType::Perp).unwrap(),
948 r#""perp""#,
949 );
950 assert_eq!(
951 serde_json::from_str::<LighterProductType>(r#""spot""#).unwrap(),
952 LighterProductType::Spot,
953 );
954 }
955
956 #[rstest]
957 fn test_market_status_serde() {
958 assert_eq!(
959 serde_json::from_str::<LighterMarketStatus>(r#""active""#).unwrap(),
960 LighterMarketStatus::Active,
961 );
962 assert_eq!(
963 serde_json::to_string(&LighterMarketStatus::Inactive).unwrap(),
964 r#""inactive""#,
965 );
966 }
967
968 #[rstest]
969 fn test_string_order_enums_serde() {
970 assert_eq!(
971 serde_json::from_str::<LighterOrderKind>(r#""take-profit-limit""#).unwrap(),
972 LighterOrderKind::TakeProfitLimit,
973 );
974 assert_eq!(
975 serde_json::from_str::<LighterOrderKind>(r#""twap-sub""#).unwrap(),
976 LighterOrderKind::TwapSub,
977 );
978 assert_eq!(
979 serde_json::from_str::<LighterOrderKind>(r#""liquidation""#).unwrap(),
980 LighterOrderKind::Liquidation,
981 );
982 assert_eq!(
983 serde_json::from_str::<LighterOrderTimeInForce>(r#""good-till-time""#).unwrap(),
984 LighterOrderTimeInForce::GoodTillTime,
985 );
986 assert_eq!(
987 serde_json::from_str::<LighterOrderTimeInForce>(r#""Unknown""#).unwrap(),
988 LighterOrderTimeInForce::Unknown,
989 );
990 assert_eq!(
991 serde_json::from_str::<LighterOrderTimeInForce>(r#""unknown""#).unwrap(),
992 LighterOrderTimeInForce::Unknown,
993 );
994 assert_eq!(
995 serde_json::to_string(&LighterOrderTimeInForce::Unknown).unwrap(),
996 r#""Unknown""#,
997 );
998 assert_eq!(
999 serde_json::from_str::<LighterOrderStatus>(r#""canceled-not-enough-liquidity""#)
1000 .unwrap(),
1001 LighterOrderStatus::CanceledNotEnoughLiquidity,
1002 );
1003 assert_eq!(
1004 serde_json::from_str::<LighterTriggerStatus>(r#""parent-order""#).unwrap(),
1005 LighterTriggerStatus::ParentOrder,
1006 );
1007 assert_eq!(
1008 serde_json::from_str::<LighterOrderSide>(r#""sell""#).unwrap(),
1009 LighterOrderSide::Sell,
1010 );
1011 }
1012
1013 #[rstest]
1014 fn test_trade_type_serde() {
1015 assert_eq!(
1016 serde_json::from_str::<LighterTradeType>(r#""market-settlement""#).unwrap(),
1017 LighterTradeType::MarketSettlement,
1018 );
1019 }
1020
1021 #[rstest]
1022 fn test_order_type_repr_serde() {
1023 assert_eq!(
1024 serde_json::to_string(&LighterOrderType::Limit).unwrap(),
1025 "0",
1026 );
1027 assert_eq!(serde_json::to_string(&LighterOrderType::Twap).unwrap(), "6",);
1028 assert_eq!(
1029 serde_json::to_string(&LighterOrderType::TwapSub).unwrap(),
1030 "7",
1031 );
1032 assert_eq!(
1033 serde_json::to_string(&LighterOrderType::Liquidation).unwrap(),
1034 "8",
1035 );
1036 let parsed: LighterOrderType = serde_json::from_str("3").unwrap();
1037 assert_eq!(parsed, LighterOrderType::StopLossLimit);
1038 }
1039
1040 #[rstest]
1041 fn test_numeric_constant_enums_repr_serde() {
1042 assert_eq!(
1043 serde_json::to_string(&LighterGroupingType::OneTriggersOneCancelsTheOther).unwrap(),
1044 "3",
1045 );
1046 assert_eq!(
1047 serde_json::to_string(&LighterCancelAllTimeInForce::AbortScheduled).unwrap(),
1048 "2",
1049 );
1050 assert_eq!(
1051 serde_json::to_string(&LighterPositionMarginMode::Isolated).unwrap(),
1052 "1",
1053 );
1054 assert_eq!(
1055 serde_json::to_string(&LighterMarginUpdateDirection::AddToIsolated).unwrap(),
1056 "1",
1057 );
1058 }
1059
1060 #[rstest]
1061 fn test_time_in_force_repr_serde() {
1062 assert_eq!(
1063 serde_json::to_string(&LighterTimeInForce::ImmediateOrCancel).unwrap(),
1064 "0",
1065 );
1066 assert_eq!(
1067 serde_json::to_string(&LighterTimeInForce::PostOnly).unwrap(),
1068 "2",
1069 );
1070 }
1071
1072 #[rstest]
1073 #[case::one_minute(LighterCandleResolution::OneMinute, "1m")]
1074 #[case::five_minute(LighterCandleResolution::FiveMinute, "5m")]
1075 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, "15m")]
1076 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, "30m")]
1077 #[case::one_hour(LighterCandleResolution::OneHour, "1h")]
1078 #[case::four_hour(LighterCandleResolution::FourHour, "4h")]
1079 #[case::twelve_hour(LighterCandleResolution::TwelveHour, "12h")]
1080 #[case::one_day(LighterCandleResolution::OneDay, "1d")]
1081 #[case::one_week(LighterCandleResolution::OneWeek, "1w")]
1082 fn test_candle_resolution_string_round_trip(
1083 #[case] resolution: LighterCandleResolution,
1084 #[case] expected: &str,
1085 ) {
1086 assert_eq!(resolution.as_str(), expected);
1087 assert_eq!(resolution.to_string(), expected);
1088 assert_eq!(
1089 serde_json::to_string(&resolution).unwrap(),
1090 format!("\"{expected}\""),
1091 );
1092 assert_eq!(
1093 serde_json::from_str::<LighterCandleResolution>(&format!("\"{expected}\"")).unwrap(),
1094 resolution,
1095 );
1096 assert_eq!(
1097 LighterCandleResolution::from_str(expected).unwrap(),
1098 resolution
1099 );
1100 assert!(resolution.interval_millis() > 0);
1101 }
1102
1103 #[rstest]
1104 #[case::one_minute(1, BarAggregation::Minute, LighterCandleResolution::OneMinute)]
1105 #[case::five_minute(5, BarAggregation::Minute, LighterCandleResolution::FiveMinute)]
1106 #[case::fifteen_minute(15, BarAggregation::Minute, LighterCandleResolution::FifteenMinute)]
1107 #[case::thirty_minute(30, BarAggregation::Minute, LighterCandleResolution::ThirtyMinute)]
1108 #[case::one_hour(1, BarAggregation::Hour, LighterCandleResolution::OneHour)]
1109 #[case::four_hour(4, BarAggregation::Hour, LighterCandleResolution::FourHour)]
1110 #[case::twelve_hour(12, BarAggregation::Hour, LighterCandleResolution::TwelveHour)]
1111 #[case::one_day(1, BarAggregation::Day, LighterCandleResolution::OneDay)]
1112 #[case::one_week(1, BarAggregation::Week, LighterCandleResolution::OneWeek)]
1113 fn test_candle_resolution_from_bar_type(
1114 #[case] step: usize,
1115 #[case] aggregation: BarAggregation,
1116 #[case] expected: LighterCandleResolution,
1117 ) {
1118 let bar_type = lighter_bar_type(
1119 step,
1120 aggregation,
1121 PriceType::Last,
1122 AggregationSource::External,
1123 );
1124
1125 assert_eq!(
1126 LighterCandleResolution::try_from(&bar_type).unwrap(),
1127 expected
1128 );
1129 }
1130
1131 #[rstest]
1132 #[case::three_minute(3, BarAggregation::Minute, "minute step")]
1133 #[case::two_hour(2, BarAggregation::Hour, "hour step")]
1134 #[case::two_day(2, BarAggregation::Day, "day step")]
1135 #[case::two_week(2, BarAggregation::Week, "week step")]
1136 #[case::one_second(1, BarAggregation::Second, "aggregation")]
1137 fn test_candle_resolution_rejects_unsupported_bars(
1138 #[case] step: usize,
1139 #[case] aggregation: BarAggregation,
1140 #[case] expected: &str,
1141 ) {
1142 let bar_type = lighter_bar_type(
1143 step,
1144 aggregation,
1145 PriceType::Last,
1146 AggregationSource::External,
1147 );
1148
1149 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1150 assert!(err.to_string().contains(expected));
1151 }
1152
1153 #[rstest]
1154 fn test_candle_resolution_rejects_internal_bars() {
1155 let bar_type = lighter_bar_type(
1156 1,
1157 BarAggregation::Minute,
1158 PriceType::Last,
1159 AggregationSource::Internal,
1160 );
1161
1162 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1163 assert!(err.to_string().contains("EXTERNAL aggregation"));
1164 }
1165
1166 #[rstest]
1167 #[case::one_minute(LighterCandleResolution::OneMinute, 1, BarAggregation::Minute)]
1168 #[case::five_minute(LighterCandleResolution::FiveMinute, 5, BarAggregation::Minute)]
1169 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, 15, BarAggregation::Minute)]
1170 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, 30, BarAggregation::Minute)]
1171 #[case::one_hour(LighterCandleResolution::OneHour, 1, BarAggregation::Hour)]
1172 #[case::four_hour(LighterCandleResolution::FourHour, 4, BarAggregation::Hour)]
1173 #[case::twelve_hour(LighterCandleResolution::TwelveHour, 12, BarAggregation::Hour)]
1174 #[case::one_day(LighterCandleResolution::OneDay, 1, BarAggregation::Day)]
1175 #[case::one_week(LighterCandleResolution::OneWeek, 1, BarAggregation::Week)]
1176 fn test_candle_resolution_to_bar_spec(
1177 #[case] resolution: LighterCandleResolution,
1178 #[case] step: usize,
1179 #[case] aggregation: BarAggregation,
1180 ) {
1181 let spec = resolution.to_bar_spec();
1182 assert_eq!(spec.step.get(), step);
1183 assert_eq!(spec.aggregation, aggregation);
1184 assert_eq!(spec.price_type, PriceType::Last);
1185 }
1186
1187 #[rstest]
1188 #[case::one_minute(LighterCandleResolution::OneMinute, true)]
1189 #[case::five_minute(LighterCandleResolution::FiveMinute, true)]
1190 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, true)]
1191 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, true)]
1192 #[case::one_hour(LighterCandleResolution::OneHour, true)]
1193 #[case::four_hour(LighterCandleResolution::FourHour, true)]
1194 #[case::twelve_hour(LighterCandleResolution::TwelveHour, true)]
1195 #[case::one_day(LighterCandleResolution::OneDay, true)]
1196 #[case::one_week(LighterCandleResolution::OneWeek, false)]
1197 fn test_candle_resolution_is_ws_streamable(
1198 #[case] resolution: LighterCandleResolution,
1199 #[case] expected: bool,
1200 ) {
1201 assert_eq!(resolution.is_ws_streamable(), expected);
1202 }
1203
1204 #[rstest]
1205 fn test_candle_resolution_rejects_non_last_price_type() {
1206 let bar_type = lighter_bar_type(
1207 1,
1208 BarAggregation::Minute,
1209 PriceType::Mark,
1210 AggregationSource::External,
1211 );
1212
1213 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1214 assert!(err.to_string().contains("LAST price type"));
1215 }
1216
1217 #[rstest]
1218 #[case::limit(LighterOrderType::Limit, OrderType::Limit)]
1219 #[case::market(LighterOrderType::Market, OrderType::Market)]
1220 #[case::stop_loss(LighterOrderType::StopLoss, OrderType::StopMarket)]
1221 #[case::stop_loss_limit(LighterOrderType::StopLossLimit, OrderType::StopLimit)]
1222 #[case::take_profit(LighterOrderType::TakeProfit, OrderType::MarketIfTouched)]
1223 #[case::take_profit_limit(LighterOrderType::TakeProfitLimit, OrderType::LimitIfTouched)]
1224 fn test_order_type_round_trip(#[case] lighter: LighterOrderType, #[case] nautilus: OrderType) {
1225 assert_eq!(lighter.as_nautilus().unwrap(), nautilus);
1226 assert_eq!(LighterOrderType::try_from(nautilus).unwrap(), lighter);
1227 }
1228
1229 #[rstest]
1230 #[case::twap(LighterOrderType::Twap)]
1231 #[case::twap_sub(LighterOrderType::TwapSub)]
1232 #[case::liquidation(LighterOrderType::Liquidation)]
1233 fn test_order_type_internal_variants_have_no_nautilus_mapping(
1234 #[case] order_type: LighterOrderType,
1235 ) {
1236 let err = order_type.as_nautilus().unwrap_err();
1237 assert!(
1238 err.to_string()
1239 .contains("no Nautilus order-type equivalent")
1240 );
1241 }
1242
1243 #[rstest]
1244 #[case(OrderType::TrailingStopMarket)]
1245 #[case(OrderType::TrailingStopLimit)]
1246 #[case(OrderType::MarketToLimit)]
1247 fn test_order_type_unsupported_nautilus_variants_error(#[case] nautilus: OrderType) {
1248 let err = LighterOrderType::try_from(nautilus).unwrap_err();
1249 assert!(err.to_string().contains("no Lighter order-type equivalent"));
1250 }
1251
1252 #[rstest]
1253 fn test_is_ask_round_trip() {
1254 assert!(!is_ask_from_order_side(OrderSide::Buy));
1255 assert!(is_ask_from_order_side(OrderSide::Sell));
1256 assert_eq!(order_side_from_is_ask(false), OrderSide::Buy);
1257 assert_eq!(order_side_from_is_ask(true), OrderSide::Sell);
1258 }
1259
1260 #[rstest]
1261 fn test_tx_type_repr_serde() {
1262 assert_eq!(
1263 serde_json::to_string(&LighterTxType::CreateOrder).unwrap(),
1264 "14"
1265 );
1266 assert_eq!(
1267 serde_json::to_string(&LighterTxType::CancelAllOrders).unwrap(),
1268 "16",
1269 );
1270 assert_eq!(
1271 serde_json::to_string(&LighterTxType::ApproveIntegrator).unwrap(),
1272 "45",
1273 );
1274 assert_eq!(
1275 serde_json::to_string(&LighterTxType::CreateGroupedOrders).unwrap(),
1276 "28",
1277 );
1278 assert_eq!(serde_json::to_string(&LighterTxType::Empty).unwrap(), "0");
1279 assert_eq!(
1280 serde_json::to_string(&LighterTxType::L1RegisterAsset).unwrap(),
1281 "31",
1282 );
1283 assert_eq!(
1284 serde_json::to_string(&LighterTxType::ForceBurnShares).unwrap(),
1285 "40",
1286 );
1287 assert_eq!(
1288 serde_json::to_string(&LighterTxType::UpdateMarketConfig).unwrap(),
1289 "44",
1290 );
1291 }
1292
1293 fn lighter_bar_type(
1294 step: usize,
1295 aggregation: BarAggregation,
1296 price_type: PriceType,
1297 aggregation_source: AggregationSource,
1298 ) -> BarType {
1299 BarType::new(
1300 InstrumentId::from("BTC-PERP.LIGHTER"),
1301 BarSpecification::new(step, aggregation, price_type),
1302 aggregation_source,
1303 )
1304 }
1305
1306 #[rstest]
1307 #[case(0, LighterAccountTier::Standard)]
1308 #[case(1, LighterAccountTier::Premium)]
1309 #[case(2, LighterAccountTier::Plus)]
1310 #[case(3, LighterAccountTier::Builder)]
1311 #[case(4, LighterAccountTier::Unknown(4))]
1312 #[case(255, LighterAccountTier::Unknown(255))]
1313 fn test_account_tier_from_code(#[case] code: u8, #[case] expected: LighterAccountTier) {
1314 assert_eq!(LighterAccountTier::from_code(code), expected);
1315 }
1316
1317 #[rstest]
1318 #[case(LighterAccountTier::Standard, Some(60))]
1319 #[case(LighterAccountTier::Premium, Some(24_000))]
1320 #[case(LighterAccountTier::Plus, Some(24_000))]
1321 #[case(LighterAccountTier::Builder, Some(240_000))]
1322 #[case(LighterAccountTier::Unknown(9), None)]
1323 fn test_account_tier_documented_rest_quota(
1324 #[case] tier: LighterAccountTier,
1325 #[case] expected: Option<u32>,
1326 ) {
1327 assert_eq!(tier.documented_rest_quota_per_min(), expected);
1328 }
1329
1330 #[rstest]
1331 #[case(LighterAccountTier::Standard, "Standard")]
1332 #[case(LighterAccountTier::Premium, "Premium")]
1333 #[case(LighterAccountTier::Plus, "Plus")]
1334 #[case(LighterAccountTier::Builder, "Builder")]
1335 #[case(LighterAccountTier::Unknown(7), "Unknown(7)")]
1336 fn test_account_tier_display(#[case] tier: LighterAccountTier, #[case] expected: &str) {
1337 assert_eq!(tier.to_string(), expected);
1338 }
1339}