use std::fmt::Display;
use nautilus_model::{
data::{BarSpecification, BarType},
enums::{AggregationSource, BarAggregation, OrderSide, OrderType, PriceType},
};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use strum::{AsRefStr, Display, EnumIter, EnumString};
#[derive(
Copy,
Clone,
Debug,
Default,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
eq,
eq_int,
module = "nautilus_trader.core.nautilus_pyo3.lighter",
from_py_object,
rename_all = "SCREAMING_SNAKE_CASE",
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.lighter")
)]
pub enum LighterEnvironment {
#[default]
Mainnet,
Testnet,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LighterProductType {
Perp,
Spot,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[strum(ascii_case_insensitive)]
pub enum LighterCandleResolution {
#[serde(rename = "1m")]
#[strum(serialize = "1m")]
OneMinute,
#[serde(rename = "5m")]
#[strum(serialize = "5m")]
FiveMinute,
#[serde(rename = "15m")]
#[strum(serialize = "15m")]
FifteenMinute,
#[serde(rename = "30m")]
#[strum(serialize = "30m")]
ThirtyMinute,
#[serde(rename = "1h")]
#[strum(serialize = "1h")]
OneHour,
#[serde(rename = "4h")]
#[strum(serialize = "4h")]
FourHour,
#[serde(rename = "12h")]
#[strum(serialize = "12h")]
TwelveHour,
#[serde(rename = "1d")]
#[strum(serialize = "1d")]
OneDay,
#[serde(rename = "1w")]
#[strum(serialize = "1w")]
OneWeek,
}
impl LighterCandleResolution {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::OneMinute => "1m",
Self::FiveMinute => "5m",
Self::FifteenMinute => "15m",
Self::ThirtyMinute => "30m",
Self::OneHour => "1h",
Self::FourHour => "4h",
Self::TwelveHour => "12h",
Self::OneDay => "1d",
Self::OneWeek => "1w",
}
}
#[must_use]
pub const fn interval_millis(self) -> i64 {
match self {
Self::OneMinute => 60_000,
Self::FiveMinute => 5 * 60_000,
Self::FifteenMinute => 15 * 60_000,
Self::ThirtyMinute => 30 * 60_000,
Self::OneHour => 60 * 60_000,
Self::FourHour => 4 * 60 * 60_000,
Self::TwelveHour => 12 * 60 * 60_000,
Self::OneDay => 24 * 60 * 60_000,
Self::OneWeek => 7 * 24 * 60 * 60_000,
}
}
#[must_use]
pub fn to_bar_spec(self) -> BarSpecification {
let (step, aggregation) = match self {
Self::OneMinute => (1, BarAggregation::Minute),
Self::FiveMinute => (5, BarAggregation::Minute),
Self::FifteenMinute => (15, BarAggregation::Minute),
Self::ThirtyMinute => (30, BarAggregation::Minute),
Self::OneHour => (1, BarAggregation::Hour),
Self::FourHour => (4, BarAggregation::Hour),
Self::TwelveHour => (12, BarAggregation::Hour),
Self::OneDay => (1, BarAggregation::Day),
Self::OneWeek => (1, BarAggregation::Week),
};
BarSpecification::new(step, aggregation, PriceType::Last)
}
#[must_use]
pub const fn is_ws_streamable(self) -> bool {
!matches!(self, Self::OneWeek)
}
}
impl TryFrom<&BarType> for LighterCandleResolution {
type Error = anyhow::Error;
fn try_from(value: &BarType) -> Result<Self, Self::Error> {
anyhow::ensure!(
value.aggregation_source() == AggregationSource::External,
"Lighter candles only support EXTERNAL aggregation",
);
let spec = value.spec();
anyhow::ensure!(
spec.price_type == PriceType::Last,
"Lighter candles only support LAST price type",
);
let step = spec.step.get();
match spec.aggregation {
BarAggregation::Minute => match step {
1 => Ok(Self::OneMinute),
5 => Ok(Self::FiveMinute),
15 => Ok(Self::FifteenMinute),
30 => Ok(Self::ThirtyMinute),
_ => anyhow::bail!("unsupported Lighter candle minute step: {step}"),
},
BarAggregation::Hour => match step {
1 => Ok(Self::OneHour),
4 => Ok(Self::FourHour),
12 => Ok(Self::TwelveHour),
_ => anyhow::bail!("unsupported Lighter candle hour step: {step}"),
},
BarAggregation::Day => match step {
1 => Ok(Self::OneDay),
_ => anyhow::bail!("unsupported Lighter candle day step: {step}"),
},
BarAggregation::Week => match step {
1 => Ok(Self::OneWeek),
_ => anyhow::bail!("unsupported Lighter candle week step: {step}"),
},
other => anyhow::bail!("unsupported Lighter candle aggregation: {other}"),
}
}
}
#[derive(
Copy,
Clone,
Debug,
Default,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[strum(ascii_case_insensitive)]
pub enum LighterFundingResolution {
#[default]
#[serde(rename = "1h")]
#[strum(serialize = "1h")]
OneHour,
#[serde(rename = "1d")]
#[strum(serialize = "1d")]
OneDay,
}
impl LighterFundingResolution {
#[must_use]
pub const fn interval_minutes(self) -> u16 {
match self {
Self::OneHour => 60,
Self::OneDay => 24 * 60,
}
}
#[must_use]
pub const fn interval_millis(self) -> i64 {
match self {
Self::OneHour => 60 * 60_000,
Self::OneDay => 24 * 60 * 60_000,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Default,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LighterOrderBookFilter {
#[default]
All,
Perp,
Spot,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterMarketStatus {
Inactive,
Active,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterOrderKind {
Limit,
Market,
StopLoss,
StopLossLimit,
TakeProfit,
TakeProfitLimit,
Twap,
TwapSub,
Liquidation,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterOrderTimeInForce {
GoodTillTime,
ImmediateOrCancel,
PostOnly,
#[serde(alias = "unknown")]
#[serde(rename = "Unknown")]
#[strum(serialize = "Unknown")]
Unknown,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterOrderStatus {
InProgress,
Pending,
Open,
Filled,
Canceled,
CanceledPostOnly,
CanceledReduceOnly,
CanceledPositionNotAllowed,
CanceledMarginNotAllowed,
CanceledTooMuchSlippage,
CanceledNotEnoughLiquidity,
CanceledSelfTrade,
CanceledExpired,
CanceledOco,
CanceledChild,
CanceledLiquidation,
CanceledInvalidBalance,
}
impl LighterOrderStatus {
#[must_use]
pub fn as_cancel_reason(self) -> Option<&'static str> {
match self {
Self::CanceledPostOnly => Some("post-only"),
Self::CanceledReduceOnly => Some("reduce-only"),
Self::CanceledPositionNotAllowed => Some("position-not-allowed"),
Self::CanceledMarginNotAllowed => Some("margin-not-allowed"),
Self::CanceledTooMuchSlippage => Some("too-much-slippage"),
Self::CanceledNotEnoughLiquidity => Some("not-enough-liquidity"),
Self::CanceledSelfTrade => Some("self-trade"),
Self::CanceledOco => Some("oco"),
Self::CanceledChild => Some("child"),
Self::CanceledLiquidation => Some("liquidation"),
Self::CanceledInvalidBalance => Some("invalid-balance"),
_ => None,
}
}
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LighterOrderSide {
Buy,
Sell,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterTriggerStatus {
Na,
Ready,
MarkPrice,
Twap,
ParentOrder,
}
#[derive(
Copy,
Clone,
Debug,
Display,
PartialEq,
Eq,
Hash,
AsRefStr,
EnumIter,
EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
pub enum LighterTradeType {
Trade,
Liquidation,
Deleverage,
MarketSettlement,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterOrderType {
Limit = 0,
Market = 1,
StopLoss = 2,
StopLossLimit = 3,
TakeProfit = 4,
TakeProfitLimit = 5,
Twap = 6,
TwapSub = 7,
Liquidation = 8,
}
impl LighterOrderType {
pub fn as_nautilus(self) -> anyhow::Result<OrderType> {
match self {
Self::Limit => Ok(OrderType::Limit),
Self::Market => Ok(OrderType::Market),
Self::StopLoss => Ok(OrderType::StopMarket),
Self::StopLossLimit => Ok(OrderType::StopLimit),
Self::TakeProfit => Ok(OrderType::MarketIfTouched),
Self::TakeProfitLimit => Ok(OrderType::LimitIfTouched),
Self::Twap | Self::TwapSub | Self::Liquidation => Err(anyhow::anyhow!(
"Lighter `{self:?}` has no Nautilus order-type equivalent",
)),
}
}
}
impl TryFrom<OrderType> for LighterOrderType {
type Error = anyhow::Error;
fn try_from(value: OrderType) -> Result<Self, Self::Error> {
match value {
OrderType::Limit => Ok(Self::Limit),
OrderType::Market => Ok(Self::Market),
OrderType::StopMarket => Ok(Self::StopLoss),
OrderType::StopLimit => Ok(Self::StopLossLimit),
OrderType::MarketIfTouched => Ok(Self::TakeProfit),
OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
other => Err(anyhow::anyhow!(
"Nautilus `{other:?}` has no Lighter order-type equivalent",
)),
}
}
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterTimeInForce {
ImmediateOrCancel = 0,
GoodTillTime = 1,
PostOnly = 2,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterGroupingType {
None = 0,
OneTriggersTheOther = 1,
OneCancelsTheOther = 2,
OneTriggersOneCancelsTheOther = 3,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterCancelAllTimeInForce {
Immediate = 0,
Scheduled = 1,
AbortScheduled = 2,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterAssetMarginMode {
Disabled = 0,
Enabled = 1,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterAssetRouteType {
Perps = 0,
Spot = 1,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterPositionMarginMode {
Cross = 0,
Isolated = 1,
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterMarginUpdateDirection {
RemoveFromIsolated = 0,
AddToIsolated = 1,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LighterAccountTier {
Standard,
Premium,
Plus,
Builder,
Unknown(u8),
}
impl LighterAccountTier {
#[must_use]
pub const fn from_code(code: u8) -> Self {
match code {
0 => Self::Standard,
1 => Self::Premium,
2 => Self::Plus,
3 => Self::Builder,
other => Self::Unknown(other),
}
}
#[must_use]
pub const fn documented_rest_quota_per_min(self) -> Option<u32> {
match self {
Self::Standard => Some(60),
Self::Premium => Some(24_000),
Self::Plus => Some(120_000),
Self::Builder => Some(240_000),
Self::Unknown(_) => None,
}
}
}
impl Display for LighterAccountTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Standard => f.write_str("Standard"),
Self::Premium => f.write_str("Premium"),
Self::Plus => f.write_str("Plus"),
Self::Builder => f.write_str("Builder"),
Self::Unknown(code) => write!(f, "Unknown({code})"),
}
}
}
pub fn is_ask_from_order_side(side: OrderSide) -> anyhow::Result<bool> {
match side {
OrderSide::Buy => Ok(false),
OrderSide::Sell => Ok(true),
OrderSide::NoOrderSide => Err(anyhow::anyhow!("Lighter requires a specified order side")),
}
}
#[must_use]
pub fn order_side_from_is_ask(is_ask: bool) -> OrderSide {
if is_ask {
OrderSide::Sell
} else {
OrderSide::Buy
}
}
#[derive(
Copy, Clone, Debug, Display, PartialEq, Eq, Hash, AsRefStr, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum LighterTxType {
Empty = 0,
L1Deposit = 1,
L1ChangePubKey = 2,
L1CreateMarket = 3,
L1UpdateMarket = 4,
L1CancelAllOrders = 5,
L1Withdraw = 6,
L1CreateOrder = 7,
ChangePubKey = 8,
CreateSubAccount = 9,
CreatePublicPool = 10,
UpdatePublicPool = 11,
Transfer = 12,
Withdraw = 13,
CreateOrder = 14,
CancelOrder = 15,
CancelAllOrders = 16,
ModifyOrder = 17,
MintShares = 18,
BurnShares = 19,
UpdateLeverage = 20,
InternalClaimOrder = 21,
InternalCancelOrder = 22,
InternalDeleverage = 23,
InternalExitPosition = 24,
InternalCancelAllOrders = 25,
InternalLiquidatePosition = 26,
InternalCreateOrder = 27,
CreateGroupedOrders = 28,
UpdateMargin = 29,
L1BurnShares = 30,
L1RegisterAsset = 31,
L1UpdateAsset = 32,
CreateStakingPool = 33,
StakeAssets = 35,
UnstakeAssets = 36,
L1UnstakeAssets = 37,
L1SetSystemConfig = 38,
ForceBurnShares = 40,
UpdateAccountConfig = 41,
StrategyTransfer = 43,
UpdateMarketConfig = 44,
ApproveIntegrator = 45,
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use nautilus_model::{
data::{BarSpecification, BarType},
identifiers::InstrumentId,
};
use rstest::rstest;
use serde_json;
use super::*;
#[rstest]
fn test_environment_default_is_mainnet() {
assert_eq!(LighterEnvironment::default(), LighterEnvironment::Mainnet);
}
#[rstest]
fn test_product_type_serde_uses_wire_values() {
assert_eq!(
serde_json::to_string(&LighterProductType::Perp).unwrap(),
r#""perp""#,
);
assert_eq!(
serde_json::from_str::<LighterProductType>(r#""spot""#).unwrap(),
LighterProductType::Spot,
);
}
#[rstest]
fn test_market_status_serde() {
assert_eq!(
serde_json::from_str::<LighterMarketStatus>(r#""active""#).unwrap(),
LighterMarketStatus::Active,
);
assert_eq!(
serde_json::to_string(&LighterMarketStatus::Inactive).unwrap(),
r#""inactive""#,
);
}
#[rstest]
fn test_string_order_enums_serde() {
assert_eq!(
serde_json::from_str::<LighterOrderKind>(r#""take-profit-limit""#).unwrap(),
LighterOrderKind::TakeProfitLimit,
);
assert_eq!(
serde_json::from_str::<LighterOrderKind>(r#""twap-sub""#).unwrap(),
LighterOrderKind::TwapSub,
);
assert_eq!(
serde_json::from_str::<LighterOrderKind>(r#""liquidation""#).unwrap(),
LighterOrderKind::Liquidation,
);
assert_eq!(
serde_json::from_str::<LighterOrderTimeInForce>(r#""good-till-time""#).unwrap(),
LighterOrderTimeInForce::GoodTillTime,
);
assert_eq!(
serde_json::from_str::<LighterOrderTimeInForce>(r#""Unknown""#).unwrap(),
LighterOrderTimeInForce::Unknown,
);
assert_eq!(
serde_json::from_str::<LighterOrderTimeInForce>(r#""unknown""#).unwrap(),
LighterOrderTimeInForce::Unknown,
);
assert_eq!(
serde_json::to_string(&LighterOrderTimeInForce::Unknown).unwrap(),
r#""Unknown""#,
);
assert_eq!(
serde_json::from_str::<LighterOrderStatus>(r#""canceled-not-enough-liquidity""#)
.unwrap(),
LighterOrderStatus::CanceledNotEnoughLiquidity,
);
assert_eq!(
serde_json::from_str::<LighterTriggerStatus>(r#""parent-order""#).unwrap(),
LighterTriggerStatus::ParentOrder,
);
assert_eq!(
serde_json::from_str::<LighterOrderSide>(r#""sell""#).unwrap(),
LighterOrderSide::Sell,
);
}
#[rstest]
fn test_trade_type_serde() {
assert_eq!(
serde_json::from_str::<LighterTradeType>(r#""market-settlement""#).unwrap(),
LighterTradeType::MarketSettlement,
);
}
#[rstest]
fn test_order_type_repr_serde() {
assert_eq!(
serde_json::to_string(&LighterOrderType::Limit).unwrap(),
"0",
);
assert_eq!(serde_json::to_string(&LighterOrderType::Twap).unwrap(), "6",);
assert_eq!(
serde_json::to_string(&LighterOrderType::TwapSub).unwrap(),
"7",
);
assert_eq!(
serde_json::to_string(&LighterOrderType::Liquidation).unwrap(),
"8",
);
let parsed: LighterOrderType = serde_json::from_str("3").unwrap();
assert_eq!(parsed, LighterOrderType::StopLossLimit);
}
#[rstest]
fn test_numeric_constant_enums_repr_serde() {
assert_eq!(
serde_json::to_string(&LighterGroupingType::OneTriggersOneCancelsTheOther).unwrap(),
"3",
);
assert_eq!(
serde_json::to_string(&LighterCancelAllTimeInForce::AbortScheduled).unwrap(),
"2",
);
assert_eq!(
serde_json::to_string(&LighterPositionMarginMode::Isolated).unwrap(),
"1",
);
assert_eq!(
serde_json::to_string(&LighterMarginUpdateDirection::AddToIsolated).unwrap(),
"1",
);
}
#[rstest]
fn test_time_in_force_repr_serde() {
assert_eq!(
serde_json::to_string(&LighterTimeInForce::ImmediateOrCancel).unwrap(),
"0",
);
assert_eq!(
serde_json::to_string(&LighterTimeInForce::PostOnly).unwrap(),
"2",
);
}
#[rstest]
#[case::one_minute(LighterCandleResolution::OneMinute, "1m")]
#[case::five_minute(LighterCandleResolution::FiveMinute, "5m")]
#[case::fifteen_minute(LighterCandleResolution::FifteenMinute, "15m")]
#[case::thirty_minute(LighterCandleResolution::ThirtyMinute, "30m")]
#[case::one_hour(LighterCandleResolution::OneHour, "1h")]
#[case::four_hour(LighterCandleResolution::FourHour, "4h")]
#[case::twelve_hour(LighterCandleResolution::TwelveHour, "12h")]
#[case::one_day(LighterCandleResolution::OneDay, "1d")]
#[case::one_week(LighterCandleResolution::OneWeek, "1w")]
fn test_candle_resolution_string_round_trip(
#[case] resolution: LighterCandleResolution,
#[case] expected: &str,
) {
assert_eq!(resolution.as_str(), expected);
assert_eq!(resolution.to_string(), expected);
assert_eq!(
serde_json::to_string(&resolution).unwrap(),
format!("\"{expected}\""),
);
assert_eq!(
serde_json::from_str::<LighterCandleResolution>(&format!("\"{expected}\"")).unwrap(),
resolution,
);
assert_eq!(
LighterCandleResolution::from_str(expected).unwrap(),
resolution
);
assert!(resolution.interval_millis() > 0);
}
#[rstest]
#[case::one_minute(1, BarAggregation::Minute, LighterCandleResolution::OneMinute)]
#[case::five_minute(5, BarAggregation::Minute, LighterCandleResolution::FiveMinute)]
#[case::fifteen_minute(15, BarAggregation::Minute, LighterCandleResolution::FifteenMinute)]
#[case::thirty_minute(30, BarAggregation::Minute, LighterCandleResolution::ThirtyMinute)]
#[case::one_hour(1, BarAggregation::Hour, LighterCandleResolution::OneHour)]
#[case::four_hour(4, BarAggregation::Hour, LighterCandleResolution::FourHour)]
#[case::twelve_hour(12, BarAggregation::Hour, LighterCandleResolution::TwelveHour)]
#[case::one_day(1, BarAggregation::Day, LighterCandleResolution::OneDay)]
#[case::one_week(1, BarAggregation::Week, LighterCandleResolution::OneWeek)]
fn test_candle_resolution_from_bar_type(
#[case] step: usize,
#[case] aggregation: BarAggregation,
#[case] expected: LighterCandleResolution,
) {
let bar_type = lighter_bar_type(
step,
aggregation,
PriceType::Last,
AggregationSource::External,
);
assert_eq!(
LighterCandleResolution::try_from(&bar_type).unwrap(),
expected
);
}
#[rstest]
#[case::three_minute(3, BarAggregation::Minute, "minute step")]
#[case::two_hour(2, BarAggregation::Hour, "hour step")]
#[case::two_day(2, BarAggregation::Day, "day step")]
#[case::two_week(2, BarAggregation::Week, "week step")]
#[case::one_second(1, BarAggregation::Second, "aggregation")]
fn test_candle_resolution_rejects_unsupported_bars(
#[case] step: usize,
#[case] aggregation: BarAggregation,
#[case] expected: &str,
) {
let bar_type = lighter_bar_type(
step,
aggregation,
PriceType::Last,
AggregationSource::External,
);
let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
assert!(err.to_string().contains(expected));
}
#[rstest]
fn test_candle_resolution_rejects_internal_bars() {
let bar_type = lighter_bar_type(
1,
BarAggregation::Minute,
PriceType::Last,
AggregationSource::Internal,
);
let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
assert!(err.to_string().contains("EXTERNAL aggregation"));
}
#[rstest]
#[case::one_minute(LighterCandleResolution::OneMinute, 1, BarAggregation::Minute)]
#[case::five_minute(LighterCandleResolution::FiveMinute, 5, BarAggregation::Minute)]
#[case::fifteen_minute(LighterCandleResolution::FifteenMinute, 15, BarAggregation::Minute)]
#[case::thirty_minute(LighterCandleResolution::ThirtyMinute, 30, BarAggregation::Minute)]
#[case::one_hour(LighterCandleResolution::OneHour, 1, BarAggregation::Hour)]
#[case::four_hour(LighterCandleResolution::FourHour, 4, BarAggregation::Hour)]
#[case::twelve_hour(LighterCandleResolution::TwelveHour, 12, BarAggregation::Hour)]
#[case::one_day(LighterCandleResolution::OneDay, 1, BarAggregation::Day)]
#[case::one_week(LighterCandleResolution::OneWeek, 1, BarAggregation::Week)]
fn test_candle_resolution_to_bar_spec(
#[case] resolution: LighterCandleResolution,
#[case] step: usize,
#[case] aggregation: BarAggregation,
) {
let spec = resolution.to_bar_spec();
assert_eq!(spec.step.get(), step);
assert_eq!(spec.aggregation, aggregation);
assert_eq!(spec.price_type, PriceType::Last);
}
#[rstest]
#[case::one_minute(LighterCandleResolution::OneMinute, true)]
#[case::five_minute(LighterCandleResolution::FiveMinute, true)]
#[case::fifteen_minute(LighterCandleResolution::FifteenMinute, true)]
#[case::thirty_minute(LighterCandleResolution::ThirtyMinute, true)]
#[case::one_hour(LighterCandleResolution::OneHour, true)]
#[case::four_hour(LighterCandleResolution::FourHour, true)]
#[case::twelve_hour(LighterCandleResolution::TwelveHour, true)]
#[case::one_day(LighterCandleResolution::OneDay, true)]
#[case::one_week(LighterCandleResolution::OneWeek, false)]
fn test_candle_resolution_is_ws_streamable(
#[case] resolution: LighterCandleResolution,
#[case] expected: bool,
) {
assert_eq!(resolution.is_ws_streamable(), expected);
}
#[rstest]
fn test_candle_resolution_rejects_non_last_price_type() {
let bar_type = lighter_bar_type(
1,
BarAggregation::Minute,
PriceType::Mark,
AggregationSource::External,
);
let err = LighterCandleResolution::try_from(&bar_type).unwrap_err();
assert!(err.to_string().contains("LAST price type"));
}
#[rstest]
#[case::limit(LighterOrderType::Limit, OrderType::Limit)]
#[case::market(LighterOrderType::Market, OrderType::Market)]
#[case::stop_loss(LighterOrderType::StopLoss, OrderType::StopMarket)]
#[case::stop_loss_limit(LighterOrderType::StopLossLimit, OrderType::StopLimit)]
#[case::take_profit(LighterOrderType::TakeProfit, OrderType::MarketIfTouched)]
#[case::take_profit_limit(LighterOrderType::TakeProfitLimit, OrderType::LimitIfTouched)]
fn test_order_type_round_trip(#[case] lighter: LighterOrderType, #[case] nautilus: OrderType) {
assert_eq!(lighter.as_nautilus().unwrap(), nautilus);
assert_eq!(LighterOrderType::try_from(nautilus).unwrap(), lighter);
}
#[rstest]
#[case::twap(LighterOrderType::Twap)]
#[case::twap_sub(LighterOrderType::TwapSub)]
#[case::liquidation(LighterOrderType::Liquidation)]
fn test_order_type_internal_variants_have_no_nautilus_mapping(
#[case] order_type: LighterOrderType,
) {
let err = order_type.as_nautilus().unwrap_err();
assert!(
err.to_string()
.contains("no Nautilus order-type equivalent")
);
}
#[rstest]
#[case(OrderType::TrailingStopMarket)]
#[case(OrderType::TrailingStopLimit)]
#[case(OrderType::MarketToLimit)]
fn test_order_type_unsupported_nautilus_variants_error(#[case] nautilus: OrderType) {
let err = LighterOrderType::try_from(nautilus).unwrap_err();
assert!(err.to_string().contains("no Lighter order-type equivalent"));
}
#[rstest]
fn test_is_ask_round_trip() {
assert!(!is_ask_from_order_side(OrderSide::Buy).unwrap());
assert!(is_ask_from_order_side(OrderSide::Sell).unwrap());
assert_eq!(order_side_from_is_ask(false), OrderSide::Buy);
assert_eq!(order_side_from_is_ask(true), OrderSide::Sell);
}
#[rstest]
fn test_is_ask_rejects_unspecified_side() {
let err = is_ask_from_order_side(OrderSide::NoOrderSide).unwrap_err();
assert!(err.to_string().contains("specified order side"));
}
#[rstest]
fn test_tx_type_repr_serde() {
assert_eq!(
serde_json::to_string(&LighterTxType::CreateOrder).unwrap(),
"14"
);
assert_eq!(
serde_json::to_string(&LighterTxType::CancelAllOrders).unwrap(),
"16",
);
assert_eq!(
serde_json::to_string(&LighterTxType::ApproveIntegrator).unwrap(),
"45",
);
assert_eq!(
serde_json::to_string(&LighterTxType::CreateGroupedOrders).unwrap(),
"28",
);
assert_eq!(serde_json::to_string(&LighterTxType::Empty).unwrap(), "0");
assert_eq!(
serde_json::to_string(&LighterTxType::L1RegisterAsset).unwrap(),
"31",
);
assert_eq!(
serde_json::to_string(&LighterTxType::ForceBurnShares).unwrap(),
"40",
);
assert_eq!(
serde_json::to_string(&LighterTxType::UpdateMarketConfig).unwrap(),
"44",
);
}
fn lighter_bar_type(
step: usize,
aggregation: BarAggregation,
price_type: PriceType,
aggregation_source: AggregationSource,
) -> BarType {
BarType::new(
InstrumentId::from("BTC-PERP.LIGHTER"),
BarSpecification::new(step, aggregation, price_type),
aggregation_source,
)
}
#[rstest]
#[case(0, LighterAccountTier::Standard)]
#[case(1, LighterAccountTier::Premium)]
#[case(2, LighterAccountTier::Plus)]
#[case(3, LighterAccountTier::Builder)]
#[case(4, LighterAccountTier::Unknown(4))]
#[case(255, LighterAccountTier::Unknown(255))]
fn test_account_tier_from_code(#[case] code: u8, #[case] expected: LighterAccountTier) {
assert_eq!(LighterAccountTier::from_code(code), expected);
}
#[rstest]
#[case(LighterAccountTier::Standard, Some(60))]
#[case(LighterAccountTier::Premium, Some(24_000))]
#[case(LighterAccountTier::Plus, Some(120_000))]
#[case(LighterAccountTier::Builder, Some(240_000))]
#[case(LighterAccountTier::Unknown(9), None)]
fn test_account_tier_documented_rest_quota(
#[case] tier: LighterAccountTier,
#[case] expected: Option<u32>,
) {
assert_eq!(tier.documented_rest_quota_per_min(), expected);
}
#[rstest]
#[case(LighterAccountTier::Standard, "Standard")]
#[case(LighterAccountTier::Premium, "Premium")]
#[case(LighterAccountTier::Plus, "Plus")]
#[case(LighterAccountTier::Builder, "Builder")]
#[case(LighterAccountTier::Unknown(7), "Unknown(7)")]
fn test_account_tier_display(#[case] tier: LighterAccountTier, #[case] expected: &str) {
assert_eq!(tier.to_string(), expected);
}
}