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