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