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, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
688)]
689#[repr(u8)]
690pub enum LighterCancelAllTimeInForce {
691 Immediate = 0,
692 Scheduled = 1,
693 AbortScheduled = 2,
694}
695
696#[derive(
698 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
699)]
700#[repr(u8)]
701pub enum LighterAssetMarginMode {
702 Disabled = 0,
703 Enabled = 1,
704}
705
706#[derive(
708 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
709)]
710#[repr(u8)]
711pub enum LighterAssetRouteType {
712 Perps = 0,
713 Spot = 1,
714}
715
716#[derive(
718 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
719)]
720#[repr(u8)]
721pub enum LighterPositionMarginMode {
722 Cross = 0,
723 Isolated = 1,
724}
725
726#[derive(
728 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
729)]
730#[repr(u8)]
731pub enum LighterMarginUpdateDirection {
732 RemoveFromIsolated = 0,
733 AddToIsolated = 1,
734}
735
736#[derive(Copy, Clone, Debug, PartialEq, Eq)]
745pub enum LighterAccountTier {
746 Standard,
747 Premium,
748 Plus,
749 Builder,
750 Unknown(u8),
751}
752
753impl LighterAccountTier {
754 #[must_use]
756 pub const fn from_code(code: u8) -> Self {
757 match code {
758 0 => Self::Standard,
759 1 => Self::Premium,
760 2 => Self::Plus,
761 3 => Self::Builder,
762 other => Self::Unknown(other),
763 }
764 }
765
766 #[must_use]
773 pub const fn documented_rest_quota_per_min(self) -> Option<u32> {
774 match self {
775 Self::Standard => Some(60),
776 Self::Premium => Some(24_000),
777 Self::Plus => Some(24_000),
778 Self::Builder => Some(240_000),
779 Self::Unknown(_) => None,
780 }
781 }
782}
783
784impl Display for LighterAccountTier {
785 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
786 match self {
787 Self::Standard => f.write_str("Standard"),
788 Self::Premium => f.write_str("Premium"),
789 Self::Plus => f.write_str("Plus"),
790 Self::Builder => f.write_str("Builder"),
791 Self::Unknown(code) => write!(f, "Unknown({code})"),
792 }
793 }
794}
795
796pub fn is_ask_from_order_side(side: OrderSide) -> bool {
808 match side {
809 OrderSide::Buy => false,
810 OrderSide::Sell => true,
811 }
812}
813
814#[must_use]
816pub fn order_side_from_is_ask(is_ask: bool) -> OrderSide {
817 if is_ask {
818 OrderSide::Sell
819 } else {
820 OrderSide::Buy
821 }
822}
823
824#[derive(
829 Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
830)]
831#[repr(u8)]
832pub enum LighterTxType {
833 Empty = 0,
834 L1Deposit = 1,
835 L1ChangePubKey = 2,
836 L1CreateMarket = 3,
837 L1UpdateMarket = 4,
838 L1CancelAllOrders = 5,
839 L1Withdraw = 6,
840 L1CreateOrder = 7,
841 ChangePubKey = 8,
842 CreateSubAccount = 9,
843 CreatePublicPool = 10,
844 UpdatePublicPool = 11,
845 Transfer = 12,
846 Withdraw = 13,
847 CreateOrder = 14,
848 CancelOrder = 15,
849 CancelAllOrders = 16,
850 ModifyOrder = 17,
851 MintShares = 18,
852 BurnShares = 19,
853 UpdateLeverage = 20,
854 InternalClaimOrder = 21,
855 InternalCancelOrder = 22,
856 InternalDeleverage = 23,
857 InternalExitPosition = 24,
858 InternalCancelAllOrders = 25,
859 InternalLiquidatePosition = 26,
860 InternalCreateOrder = 27,
861 CreateGroupedOrders = 28,
862 UpdateMargin = 29,
863 L1BurnShares = 30,
864 L1RegisterAsset = 31,
865 L1UpdateAsset = 32,
866 CreateStakingPool = 33,
867 StakeAssets = 35,
868 UnstakeAssets = 36,
869 L1UnstakeAssets = 37,
870 L1SetSystemConfig = 38,
871 ForceBurnShares = 40,
872 UpdateAccountConfig = 41,
873 StrategyTransfer = 43,
874 UpdateMarketConfig = 44,
875 ApproveIntegrator = 45,
876}
877
878#[cfg(test)]
879mod tests {
880 use std::str::FromStr;
881
882 use nautilus_model::{
883 data::{BarSpecification, BarType},
884 identifiers::InstrumentId,
885 };
886 use rstest::rstest;
887 use serde_json;
888
889 use super::*;
890
891 #[rstest]
892 fn test_environment_default_is_mainnet() {
893 assert_eq!(LighterEnvironment::default(), LighterEnvironment::Mainnet);
894 }
895
896 #[rstest]
897 fn test_product_type_serde_uses_wire_values() {
898 assert_eq!(
899 serde_json::to_string(&LighterProductType::Perp).unwrap(),
900 r#""perp""#,
901 );
902 assert_eq!(
903 serde_json::from_str::<LighterProductType>(r#""spot""#).unwrap(),
904 LighterProductType::Spot,
905 );
906 }
907
908 #[rstest]
909 fn test_market_status_serde() {
910 assert_eq!(
911 serde_json::from_str::<LighterMarketStatus>(r#""active""#).unwrap(),
912 LighterMarketStatus::Active,
913 );
914 assert_eq!(
915 serde_json::to_string(&LighterMarketStatus::Inactive).unwrap(),
916 r#""inactive""#,
917 );
918 }
919
920 #[rstest]
921 fn test_string_order_enums_serde() {
922 assert_eq!(
923 serde_json::from_str::<LighterOrderKind>(r#""take-profit-limit""#).unwrap(),
924 LighterOrderKind::TakeProfitLimit,
925 );
926 assert_eq!(
927 serde_json::from_str::<LighterOrderKind>(r#""twap-sub""#).unwrap(),
928 LighterOrderKind::TwapSub,
929 );
930 assert_eq!(
931 serde_json::from_str::<LighterOrderKind>(r#""liquidation""#).unwrap(),
932 LighterOrderKind::Liquidation,
933 );
934 assert_eq!(
935 serde_json::from_str::<LighterOrderTimeInForce>(r#""good-till-time""#).unwrap(),
936 LighterOrderTimeInForce::GoodTillTime,
937 );
938 assert_eq!(
939 serde_json::from_str::<LighterOrderTimeInForce>(r#""Unknown""#).unwrap(),
940 LighterOrderTimeInForce::Unknown,
941 );
942 assert_eq!(
943 serde_json::from_str::<LighterOrderTimeInForce>(r#""unknown""#).unwrap(),
944 LighterOrderTimeInForce::Unknown,
945 );
946 assert_eq!(
947 serde_json::to_string(&LighterOrderTimeInForce::Unknown).unwrap(),
948 r#""Unknown""#,
949 );
950 assert_eq!(
951 serde_json::from_str::<LighterOrderStatus>(r#""canceled-not-enough-liquidity""#)
952 .unwrap(),
953 LighterOrderStatus::CanceledNotEnoughLiquidity,
954 );
955 assert_eq!(
956 serde_json::from_str::<LighterTriggerStatus>(r#""parent-order""#).unwrap(),
957 LighterTriggerStatus::ParentOrder,
958 );
959 assert_eq!(
960 serde_json::from_str::<LighterOrderSide>(r#""sell""#).unwrap(),
961 LighterOrderSide::Sell,
962 );
963 }
964
965 #[rstest]
966 fn test_trade_type_serde() {
967 assert_eq!(
968 serde_json::from_str::<LighterTradeType>(r#""market-settlement""#).unwrap(),
969 LighterTradeType::MarketSettlement,
970 );
971 }
972
973 #[rstest]
974 fn test_order_type_repr_serde() {
975 assert_eq!(
976 serde_json::to_string(&LighterOrderType::Limit).unwrap(),
977 "0",
978 );
979 assert_eq!(serde_json::to_string(&LighterOrderType::Twap).unwrap(), "6",);
980 assert_eq!(
981 serde_json::to_string(&LighterOrderType::TwapSub).unwrap(),
982 "7",
983 );
984 assert_eq!(
985 serde_json::to_string(&LighterOrderType::Liquidation).unwrap(),
986 "8",
987 );
988 let parsed: LighterOrderType = serde_json::from_str("3").unwrap();
989 assert_eq!(parsed, LighterOrderType::StopLossLimit);
990 }
991
992 #[rstest]
993 fn test_numeric_constant_enums_repr_serde() {
994 assert_eq!(
995 serde_json::to_string(&LighterGroupingType::OneTriggersOneCancelsTheOther).unwrap(),
996 "3",
997 );
998 assert_eq!(
999 serde_json::to_string(&LighterCancelAllTimeInForce::AbortScheduled).unwrap(),
1000 "2",
1001 );
1002 assert_eq!(
1003 serde_json::to_string(&LighterPositionMarginMode::Isolated).unwrap(),
1004 "1",
1005 );
1006 assert_eq!(
1007 serde_json::to_string(&LighterMarginUpdateDirection::AddToIsolated).unwrap(),
1008 "1",
1009 );
1010 }
1011
1012 #[rstest]
1013 fn test_time_in_force_repr_serde() {
1014 assert_eq!(
1015 serde_json::to_string(&LighterTimeInForce::ImmediateOrCancel).unwrap(),
1016 "0",
1017 );
1018 assert_eq!(
1019 serde_json::to_string(&LighterTimeInForce::PostOnly).unwrap(),
1020 "2",
1021 );
1022 }
1023
1024 #[rstest]
1025 #[case::one_minute(LighterCandleResolution::OneMinute, "1m")]
1026 #[case::five_minute(LighterCandleResolution::FiveMinute, "5m")]
1027 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, "15m")]
1028 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, "30m")]
1029 #[case::one_hour(LighterCandleResolution::OneHour, "1h")]
1030 #[case::four_hour(LighterCandleResolution::FourHour, "4h")]
1031 #[case::twelve_hour(LighterCandleResolution::TwelveHour, "12h")]
1032 #[case::one_day(LighterCandleResolution::OneDay, "1d")]
1033 #[case::one_week(LighterCandleResolution::OneWeek, "1w")]
1034 fn test_candle_resolution_string_round_trip(
1035 #[case] resolution: LighterCandleResolution,
1036 #[case] expected: &str,
1037 ) {
1038 assert_eq!(resolution.as_str(), expected);
1039 assert_eq!(resolution.to_string(), expected);
1040 assert_eq!(
1041 serde_json::to_string(&resolution).unwrap(),
1042 format!("\"{expected}\""),
1043 );
1044 assert_eq!(
1045 serde_json::from_str::<LighterCandleResolution>(&format!("\"{expected}\"")).unwrap(),
1046 resolution,
1047 );
1048 assert_eq!(
1049 LighterCandleResolution::from_str(expected).unwrap(),
1050 resolution
1051 );
1052 assert!(resolution.interval_millis() > 0);
1053 }
1054
1055 #[rstest]
1056 #[case::one_minute(1, BarAggregation::Minute, LighterCandleResolution::OneMinute)]
1057 #[case::five_minute(5, BarAggregation::Minute, LighterCandleResolution::FiveMinute)]
1058 #[case::fifteen_minute(15, BarAggregation::Minute, LighterCandleResolution::FifteenMinute)]
1059 #[case::thirty_minute(30, BarAggregation::Minute, LighterCandleResolution::ThirtyMinute)]
1060 #[case::one_hour(1, BarAggregation::Hour, LighterCandleResolution::OneHour)]
1061 #[case::four_hour(4, BarAggregation::Hour, LighterCandleResolution::FourHour)]
1062 #[case::twelve_hour(12, BarAggregation::Hour, LighterCandleResolution::TwelveHour)]
1063 #[case::one_day(1, BarAggregation::Day, LighterCandleResolution::OneDay)]
1064 #[case::one_week(1, BarAggregation::Week, LighterCandleResolution::OneWeek)]
1065 fn test_candle_resolution_from_bar_type(
1066 #[case] step: usize,
1067 #[case] aggregation: BarAggregation,
1068 #[case] expected: LighterCandleResolution,
1069 ) {
1070 let bar_type = lighter_bar_type(
1071 step,
1072 aggregation,
1073 PriceType::Last,
1074 AggregationSource::External,
1075 );
1076
1077 assert_eq!(
1078 LighterCandleResolution::try_from(&bar_type).unwrap(),
1079 expected
1080 );
1081 }
1082
1083 #[rstest]
1084 #[case::three_minute(3, BarAggregation::Minute, "minute step")]
1085 #[case::two_hour(2, BarAggregation::Hour, "hour step")]
1086 #[case::two_day(2, BarAggregation::Day, "day step")]
1087 #[case::two_week(2, BarAggregation::Week, "week step")]
1088 #[case::one_second(1, BarAggregation::Second, "aggregation")]
1089 fn test_candle_resolution_rejects_unsupported_bars(
1090 #[case] step: usize,
1091 #[case] aggregation: BarAggregation,
1092 #[case] expected: &str,
1093 ) {
1094 let bar_type = lighter_bar_type(
1095 step,
1096 aggregation,
1097 PriceType::Last,
1098 AggregationSource::External,
1099 );
1100
1101 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1102 assert!(err.to_string().contains(expected));
1103 }
1104
1105 #[rstest]
1106 fn test_candle_resolution_rejects_internal_bars() {
1107 let bar_type = lighter_bar_type(
1108 1,
1109 BarAggregation::Minute,
1110 PriceType::Last,
1111 AggregationSource::Internal,
1112 );
1113
1114 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1115 assert!(err.to_string().contains("EXTERNAL aggregation"));
1116 }
1117
1118 #[rstest]
1119 #[case::one_minute(LighterCandleResolution::OneMinute, 1, BarAggregation::Minute)]
1120 #[case::five_minute(LighterCandleResolution::FiveMinute, 5, BarAggregation::Minute)]
1121 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, 15, BarAggregation::Minute)]
1122 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, 30, BarAggregation::Minute)]
1123 #[case::one_hour(LighterCandleResolution::OneHour, 1, BarAggregation::Hour)]
1124 #[case::four_hour(LighterCandleResolution::FourHour, 4, BarAggregation::Hour)]
1125 #[case::twelve_hour(LighterCandleResolution::TwelveHour, 12, BarAggregation::Hour)]
1126 #[case::one_day(LighterCandleResolution::OneDay, 1, BarAggregation::Day)]
1127 #[case::one_week(LighterCandleResolution::OneWeek, 1, BarAggregation::Week)]
1128 fn test_candle_resolution_to_bar_spec(
1129 #[case] resolution: LighterCandleResolution,
1130 #[case] step: usize,
1131 #[case] aggregation: BarAggregation,
1132 ) {
1133 let spec = resolution.to_bar_spec();
1134 assert_eq!(spec.step.get(), step);
1135 assert_eq!(spec.aggregation, aggregation);
1136 assert_eq!(spec.price_type, PriceType::Last);
1137 }
1138
1139 #[rstest]
1140 #[case::one_minute(LighterCandleResolution::OneMinute, true)]
1141 #[case::five_minute(LighterCandleResolution::FiveMinute, true)]
1142 #[case::fifteen_minute(LighterCandleResolution::FifteenMinute, true)]
1143 #[case::thirty_minute(LighterCandleResolution::ThirtyMinute, true)]
1144 #[case::one_hour(LighterCandleResolution::OneHour, true)]
1145 #[case::four_hour(LighterCandleResolution::FourHour, true)]
1146 #[case::twelve_hour(LighterCandleResolution::TwelveHour, true)]
1147 #[case::one_day(LighterCandleResolution::OneDay, true)]
1148 #[case::one_week(LighterCandleResolution::OneWeek, false)]
1149 fn test_candle_resolution_is_ws_streamable(
1150 #[case] resolution: LighterCandleResolution,
1151 #[case] expected: bool,
1152 ) {
1153 assert_eq!(resolution.is_ws_streamable(), expected);
1154 }
1155
1156 #[rstest]
1157 fn test_candle_resolution_rejects_non_last_price_type() {
1158 let bar_type = lighter_bar_type(
1159 1,
1160 BarAggregation::Minute,
1161 PriceType::Mark,
1162 AggregationSource::External,
1163 );
1164
1165 let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
1166 assert!(err.to_string().contains("LAST price type"));
1167 }
1168
1169 #[rstest]
1170 #[case::limit(LighterOrderType::Limit, OrderType::Limit)]
1171 #[case::market(LighterOrderType::Market, OrderType::Market)]
1172 #[case::stop_loss(LighterOrderType::StopLoss, OrderType::StopMarket)]
1173 #[case::stop_loss_limit(LighterOrderType::StopLossLimit, OrderType::StopLimit)]
1174 #[case::take_profit(LighterOrderType::TakeProfit, OrderType::MarketIfTouched)]
1175 #[case::take_profit_limit(LighterOrderType::TakeProfitLimit, OrderType::LimitIfTouched)]
1176 fn test_order_type_round_trip(#[case] lighter: LighterOrderType, #[case] nautilus: OrderType) {
1177 assert_eq!(lighter.as_nautilus().unwrap(), nautilus);
1178 assert_eq!(LighterOrderType::try_from(nautilus).unwrap(), lighter);
1179 }
1180
1181 #[rstest]
1182 #[case::twap(LighterOrderType::Twap)]
1183 #[case::twap_sub(LighterOrderType::TwapSub)]
1184 #[case::liquidation(LighterOrderType::Liquidation)]
1185 fn test_order_type_internal_variants_have_no_nautilus_mapping(
1186 #[case] order_type: LighterOrderType,
1187 ) {
1188 let err = order_type.as_nautilus().unwrap_err();
1189 assert!(
1190 err.to_string()
1191 .contains("no Nautilus order-type equivalent")
1192 );
1193 }
1194
1195 #[rstest]
1196 #[case(OrderType::TrailingStopMarket)]
1197 #[case(OrderType::TrailingStopLimit)]
1198 #[case(OrderType::MarketToLimit)]
1199 fn test_order_type_unsupported_nautilus_variants_error(#[case] nautilus: OrderType) {
1200 let err = LighterOrderType::try_from(nautilus).unwrap_err();
1201 assert!(err.to_string().contains("no Lighter order-type equivalent"));
1202 }
1203
1204 #[rstest]
1205 fn test_is_ask_round_trip() {
1206 assert!(!is_ask_from_order_side(OrderSide::Buy));
1207 assert!(is_ask_from_order_side(OrderSide::Sell));
1208 assert_eq!(order_side_from_is_ask(false), OrderSide::Buy);
1209 assert_eq!(order_side_from_is_ask(true), OrderSide::Sell);
1210 }
1211
1212 #[rstest]
1213 fn test_tx_type_repr_serde() {
1214 assert_eq!(
1215 serde_json::to_string(&LighterTxType::CreateOrder).unwrap(),
1216 "14"
1217 );
1218 assert_eq!(
1219 serde_json::to_string(&LighterTxType::CancelAllOrders).unwrap(),
1220 "16",
1221 );
1222 assert_eq!(
1223 serde_json::to_string(&LighterTxType::ApproveIntegrator).unwrap(),
1224 "45",
1225 );
1226 assert_eq!(
1227 serde_json::to_string(&LighterTxType::CreateGroupedOrders).unwrap(),
1228 "28",
1229 );
1230 assert_eq!(serde_json::to_string(&LighterTxType::Empty).unwrap(), "0");
1231 assert_eq!(
1232 serde_json::to_string(&LighterTxType::L1RegisterAsset).unwrap(),
1233 "31",
1234 );
1235 assert_eq!(
1236 serde_json::to_string(&LighterTxType::ForceBurnShares).unwrap(),
1237 "40",
1238 );
1239 assert_eq!(
1240 serde_json::to_string(&LighterTxType::UpdateMarketConfig).unwrap(),
1241 "44",
1242 );
1243 }
1244
1245 fn lighter_bar_type(
1246 step: usize,
1247 aggregation: BarAggregation,
1248 price_type: PriceType,
1249 aggregation_source: AggregationSource,
1250 ) -> BarType {
1251 BarType::new(
1252 InstrumentId::from("BTC-PERP.LIGHTER"),
1253 BarSpecification::new(step, aggregation, price_type),
1254 aggregation_source,
1255 )
1256 }
1257
1258 #[rstest]
1259 #[case(0, LighterAccountTier::Standard)]
1260 #[case(1, LighterAccountTier::Premium)]
1261 #[case(2, LighterAccountTier::Plus)]
1262 #[case(3, LighterAccountTier::Builder)]
1263 #[case(4, LighterAccountTier::Unknown(4))]
1264 #[case(255, LighterAccountTier::Unknown(255))]
1265 fn test_account_tier_from_code(#[case] code: u8, #[case] expected: LighterAccountTier) {
1266 assert_eq!(LighterAccountTier::from_code(code), expected);
1267 }
1268
1269 #[rstest]
1270 #[case(LighterAccountTier::Standard, Some(60))]
1271 #[case(LighterAccountTier::Premium, Some(24_000))]
1272 #[case(LighterAccountTier::Plus, Some(24_000))]
1273 #[case(LighterAccountTier::Builder, Some(240_000))]
1274 #[case(LighterAccountTier::Unknown(9), None)]
1275 fn test_account_tier_documented_rest_quota(
1276 #[case] tier: LighterAccountTier,
1277 #[case] expected: Option<u32>,
1278 ) {
1279 assert_eq!(tier.documented_rest_quota_per_min(), expected);
1280 }
1281
1282 #[rstest]
1283 #[case(LighterAccountTier::Standard, "Standard")]
1284 #[case(LighterAccountTier::Premium, "Premium")]
1285 #[case(LighterAccountTier::Plus, "Plus")]
1286 #[case(LighterAccountTier::Builder, "Builder")]
1287 #[case(LighterAccountTier::Unknown(7), "Unknown(7)")]
1288 fn test_account_tier_display(#[case] tier: LighterAccountTier, #[case] expected: &str) {
1289 assert_eq!(tier.to_string(), expected);
1290 }
1291}