From 21eb46d05d52763d07e855eb17ef234cf5335966 Mon Sep 17 00:00:00 2001 From: larry cao Date: Mon, 15 Jun 2026 21:36:12 +0800 Subject: [PATCH] refactor: isolate kline interval dialects --- docs/ARCHITECTURE.md | 2 +- src/core/types.rs | 42 ----------------- .../backpack/connector/market_data.rs | 44 ++++------------- src/exchanges/backpack/conversions.rs | 25 +++++++++- src/exchanges/backpack/mod.rs | 32 ++----------- src/exchanges/binance/codec.rs | 3 +- src/exchanges/binance/conversions.rs | 47 ++++++++++++++++++- src/exchanges/binance/rest.rs | 33 ++----------- src/exchanges/binance_perp/codec.rs | 3 +- src/exchanges/binance_perp/conversions.rs | 25 +++++++++- src/exchanges/binance_perp/rest.rs | 5 +- .../bybit_perp/connector/market_data.rs | 46 +++++------------- src/exchanges/bybit_perp/conversions.rs | 25 +++++++++- src/exchanges/hyperliquid/codec.rs | 3 +- src/exchanges/paradex/codec.rs | 7 ++- .../paradex/connector/market_data.rs | 7 ++- src/exchanges/paradex/conversions.rs | 25 +++++++++- src/exchanges/paradex/rest.rs | 33 ++----------- 18 files changed, 188 insertions(+), 219 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 36545d0..e897a7a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -107,7 +107,7 @@ Risks and debt: - Some legacy compatibility constructors keep old names or parameters while delegating to the new builders. Prefer the direct `build_connector*` functions in new code. - `ExchangeFactory` is for latency tests and demos. It now delegates Bybit and OKX creation through the exchange builders, but production code should still prefer exchange-specific builders when credentials, passphrases, or custom behavior matter. - `RestClientConfig::max_retries` applies to retryable GET and DELETE failures. POST requests are not automatically retried to avoid repeating order-style side effects. -- `KlineInterval` includes exchange-specific formatting helpers, so a small amount of exchange dialect knowledge leaks into the shared type layer. +- `KlineInterval` is a shared semantic interval type; exchange-specific interval formatting belongs in exchange modules. - `src/exchanges/okx/` is implemented, but `src/exchanges/okx_perp/` is an empty, unregistered directory. Treat OKX perpetual as not implemented. - Some tests depend on public exchange APIs and can fail because of network or upstream API changes. - Historical docs previously mixed old plans with current architecture. Current docs now keep those notes in `docs/archive/`. diff --git a/src/core/types.rs b/src/core/types.rs index ba4d2c8..1ec781e 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -370,48 +370,6 @@ pub enum KlineInterval { Months1, } -impl KlineInterval { - pub fn to_binance_format(&self) -> String { - match self { - Self::Minutes1 => "1m".to_string(), - Self::Minutes3 => "3m".to_string(), - Self::Minutes5 => "5m".to_string(), - Self::Minutes15 => "15m".to_string(), - Self::Minutes30 => "30m".to_string(), - Self::Hours1 => "1h".to_string(), - Self::Hours2 => "2h".to_string(), - Self::Hours4 => "4h".to_string(), - Self::Hours6 => "6h".to_string(), - Self::Hours8 => "8h".to_string(), - Self::Hours12 => "12h".to_string(), - Self::Days1 => "1d".to_string(), - Self::Days3 => "3d".to_string(), - Self::Weeks1 => "1w".to_string(), - Self::Months1 => "1M".to_string(), - } - } - - pub fn to_bybit_format(&self) -> String { - match self { - Self::Minutes1 => "1".to_string(), - Self::Minutes3 => "3".to_string(), - Self::Minutes5 => "5".to_string(), - Self::Minutes15 => "15".to_string(), - Self::Minutes30 => "30".to_string(), - Self::Hours1 => "60".to_string(), - Self::Hours2 => "120".to_string(), - Self::Hours4 => "240".to_string(), - Self::Hours6 => "360".to_string(), - Self::Hours8 => "480".to_string(), - Self::Hours12 => "720".to_string(), - Self::Days1 => "D".to_string(), - Self::Days3 => "3D".to_string(), - Self::Weeks1 => "W".to_string(), - Self::Months1 => "M".to_string(), - } - } -} - impl fmt::Display for KlineInterval { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let description = match self { diff --git a/src/exchanges/backpack/connector/market_data.rs b/src/exchanges/backpack/connector/market_data.rs index 3e75e7f..2cff66b 100644 --- a/src/exchanges/backpack/connector/market_data.rs +++ b/src/exchanges/backpack/connector/market_data.rs @@ -7,7 +7,9 @@ use crate::core::{ SubscriptionType, Symbol, WebSocketConfig, }, }; -use crate::exchanges::backpack::{codec::BackpackCodec, rest::BackpackRestClient}; +use crate::exchanges::backpack::{ + codec::BackpackCodec, conversions::kline_interval_to_backpack_string, rest::BackpackRestClient, +}; use async_trait::async_trait; use rust_decimal::Decimal; use tokio::sync::mpsc; @@ -173,10 +175,10 @@ impl> MarketDataSource for Ma start_time: Option, end_time: Option, ) -> Result, ExchangeError> { - let interval_str = interval.to_backpack_format(); + let interval_str = kline_interval_to_backpack_string(interval); let klines = self .rest - .get_klines(&symbol, &interval_str, start_time, end_time, limit) + .get_klines(&symbol, interval_str, start_time, end_time, limit) .await?; Ok(klines @@ -185,7 +187,7 @@ impl> MarketDataSource for Ma symbol: conversion::string_to_symbol(&symbol), open_time: k.start.parse::().unwrap_or(0), close_time: k.end.parse::().unwrap_or(0), - interval: interval_str.clone(), + interval: interval_str.to_string(), open_price: conversion::string_to_price(&k.open), high_price: conversion::string_to_price(&k.high), low_price: conversion::string_to_price(&k.low), @@ -266,10 +268,10 @@ impl MarketDataSource for MarketData { start_time: Option, end_time: Option, ) -> Result, ExchangeError> { - let interval_str = interval.to_backpack_format(); + let interval_str = kline_interval_to_backpack_string(interval); let klines = self .rest - .get_klines(&symbol, &interval_str, start_time, end_time, limit) + .get_klines(&symbol, interval_str, start_time, end_time, limit) .await?; Ok(klines @@ -278,7 +280,7 @@ impl MarketDataSource for MarketData { symbol: conversion::string_to_symbol(&symbol), open_time: k.start.parse::().unwrap_or(0), close_time: k.end.parse::().unwrap_or(0), - interval: interval_str.clone(), + interval: interval_str.to_string(), open_price: conversion::string_to_price(&k.open), high_price: conversion::string_to_price(&k.high), low_price: conversion::string_to_price(&k.low), @@ -291,34 +293,6 @@ impl MarketDataSource for MarketData { } } -/// Extension trait for `KlineInterval` to support Backpack format -pub trait BackpackKlineInterval { - fn to_backpack_format(&self) -> String; -} - -impl BackpackKlineInterval for KlineInterval { - fn to_backpack_format(&self) -> String { - match self { - Self::Minutes1 => "1m".to_string(), - Self::Minutes3 => "3m".to_string(), - Self::Minutes5 => "5m".to_string(), - Self::Minutes15 => "15m".to_string(), - Self::Minutes30 => "30m".to_string(), - Self::Hours1 => "1h".to_string(), - Self::Hours2 => "2h".to_string(), - Self::Hours4 => "4h".to_string(), - Self::Hours6 => "6h".to_string(), - Self::Hours8 => "8h".to_string(), - Self::Hours12 => "12h".to_string(), - Self::Days1 => "1d".to_string(), - Self::Days3 => "3d".to_string(), - Self::Weeks1 => "1w".to_string(), - Self::Months1 => "1M".to_string(), - // Seconds1 removed - not commonly supported - } - } -} - /// Convert `BackpackMessage` to `MarketDataType` fn convert_backpack_message_to_market_data( message: crate::exchanges::backpack::codec::BackpackMessage, diff --git a/src/exchanges/backpack/conversions.rs b/src/exchanges/backpack/conversions.rs index 3e28a02..ec87c03 100644 --- a/src/exchanges/backpack/conversions.rs +++ b/src/exchanges/backpack/conversions.rs @@ -1,6 +1,6 @@ use crate::core::types::{ - conversion, Balance, Kline, Market, MarketDataType, OrderBook, OrderBookEntry, Position, - PositionSide, Symbol, Ticker, Trade, + conversion, Balance, Kline, KlineInterval, Market, MarketDataType, OrderBook, OrderBookEntry, + Position, PositionSide, Symbol, Ticker, Trade, }; use crate::exchanges::backpack::types::{ BackpackBalance, BackpackMarket, BackpackOrderBook, BackpackPosition, BackpackRestKline, @@ -8,6 +8,27 @@ use crate::exchanges::backpack::types::{ BackpackWebSocketTicker, BackpackWebSocketTrade, }; +/// Convert kline interval to Backpack format +pub fn kline_interval_to_backpack_string(interval: KlineInterval) -> &'static str { + match interval { + KlineInterval::Minutes1 => "1m", + KlineInterval::Minutes3 => "3m", + KlineInterval::Minutes5 => "5m", + KlineInterval::Minutes15 => "15m", + KlineInterval::Minutes30 => "30m", + KlineInterval::Hours1 => "1h", + KlineInterval::Hours2 => "2h", + KlineInterval::Hours4 => "4h", + KlineInterval::Hours6 => "6h", + KlineInterval::Hours8 => "8h", + KlineInterval::Hours12 => "12h", + KlineInterval::Days1 => "1d", + KlineInterval::Days3 => "3d", + KlineInterval::Weeks1 => "1w", + KlineInterval::Months1 => "1M", + } +} + /// Convert Backpack market to core Market type pub fn convert_market(backpack_market: BackpackMarket) -> Market { Market { diff --git a/src/exchanges/backpack/mod.rs b/src/exchanges/backpack/mod.rs index a3b6815..ccf7a5b 100644 --- a/src/exchanges/backpack/mod.rs +++ b/src/exchanges/backpack/mod.rs @@ -7,6 +7,8 @@ pub mod builder; pub mod connector; pub mod rest; +use crate::exchanges::backpack::conversions::kline_interval_to_backpack_string; + // Re-export main components pub use builder::{ build_connector, @@ -48,7 +50,7 @@ pub fn create_backpack_stream_identifiers( crate::core::types::SubscriptionType::Klines { interval } => { streams.push(format!( "kline.{}.{}", - interval.to_backpack_format(), + kline_interval_to_backpack_string(*interval), symbol )); } @@ -58,31 +60,3 @@ pub fn create_backpack_stream_identifiers( streams } - -/// Helper extension trait for `KlineInterval` to support Backpack format -pub trait BackpackKlineInterval { - fn to_backpack_format(&self) -> &str; -} - -impl BackpackKlineInterval for crate::core::types::KlineInterval { - fn to_backpack_format(&self) -> &str { - match self { - Self::Minutes1 => "1m", - Self::Minutes3 => "3m", - Self::Minutes5 => "5m", - Self::Minutes15 => "15m", - Self::Minutes30 => "30m", - Self::Hours1 => "1h", - Self::Hours2 => "2h", - Self::Hours4 => "4h", - Self::Hours6 => "6h", - Self::Hours8 => "8h", - Self::Hours12 => "12h", - Self::Days1 => "1d", - Self::Days3 => "3d", - Self::Weeks1 => "1w", - Self::Months1 => "1M", - // Seconds1 removed - not commonly supported - } - } -} diff --git a/src/exchanges/binance/codec.rs b/src/exchanges/binance/codec.rs index 90b8959..9a5cb06 100644 --- a/src/exchanges/binance/codec.rs +++ b/src/exchanges/binance/codec.rs @@ -1,5 +1,6 @@ use crate::core::errors::ExchangeError; use crate::core::kernel::WsCodec; +use crate::exchanges::binance::conversions::kline_interval_to_binance_string; use serde_json::{json, Value}; use tokio_tungstenite::tungstenite::Message; @@ -191,7 +192,7 @@ pub fn create_binance_stream_identifiers( streams.push(format!( "{}@kline_{}", lower_symbol, - interval.to_binance_format() + kline_interval_to_binance_string(*interval) )); } } diff --git a/src/exchanges/binance/conversions.rs b/src/exchanges/binance/conversions.rs index ff43694..fd94ef8 100644 --- a/src/exchanges/binance/conversions.rs +++ b/src/exchanges/binance/conversions.rs @@ -1,7 +1,7 @@ use super::types as binance_types; use crate::core::types::{ - conversion, Kline, Market, MarketDataType, OrderBook, OrderBookEntry, OrderSide, OrderType, - Symbol, Ticker, TimeInForce, Trade, + conversion, Kline, KlineInterval, Market, MarketDataType, OrderBook, OrderBookEntry, OrderSide, + OrderType, Symbol, Ticker, TimeInForce, Trade, }; use serde_json::Value; @@ -80,6 +80,27 @@ pub fn convert_time_in_force(tif: &TimeInForce) -> String { } } +/// Convert kline interval to Binance format +pub fn kline_interval_to_binance_string(interval: KlineInterval) -> &'static str { + match interval { + KlineInterval::Minutes1 => "1m", + KlineInterval::Minutes3 => "3m", + KlineInterval::Minutes5 => "5m", + KlineInterval::Minutes15 => "15m", + KlineInterval::Minutes30 => "30m", + KlineInterval::Hours1 => "1h", + KlineInterval::Hours2 => "2h", + KlineInterval::Hours4 => "4h", + KlineInterval::Hours6 => "6h", + KlineInterval::Hours8 => "8h", + KlineInterval::Hours12 => "12h", + KlineInterval::Days1 => "1d", + KlineInterval::Days3 => "3d", + KlineInterval::Weeks1 => "1w", + KlineInterval::Months1 => "1M", + } +} + /// Convert binance REST kline to core kline type pub fn convert_binance_rest_kline( kline: &binance_types::BinanceRestKline, @@ -213,3 +234,25 @@ pub fn parse_websocket_message(value: Value) -> Option { } None } + +#[cfg(test)] +mod tests { + use super::kline_interval_to_binance_string; + use crate::core::types::KlineInterval; + + #[test] + fn kline_interval_to_binance_string_uses_exchange_format() { + assert_eq!( + kline_interval_to_binance_string(KlineInterval::Minutes1), + "1m" + ); + assert_eq!( + kline_interval_to_binance_string(KlineInterval::Hours1), + "1h" + ); + assert_eq!( + kline_interval_to_binance_string(KlineInterval::Months1), + "1M" + ); + } +} diff --git a/src/exchanges/binance/rest.rs b/src/exchanges/binance/rest.rs index e41de6b..95b2a3d 100644 --- a/src/exchanges/binance/rest.rs +++ b/src/exchanges/binance/rest.rs @@ -1,6 +1,7 @@ use crate::core::errors::ExchangeError; use crate::core::kernel::RestClient; use crate::core::types::KlineInterval; +use crate::exchanges::binance::conversions::kline_interval_to_binance_string; use crate::exchanges::binance::types::{ BinanceAccountInfo, BinanceExchangeInfo, BinanceOrderResponse, BinanceRestKline, }; @@ -32,8 +33,8 @@ impl BinanceRestClient { start_time: Option, end_time: Option, ) -> Result, ExchangeError> { - let interval_str = interval.to_binance_format(); - let mut params = vec![("symbol", symbol), ("interval", interval_str.as_str())]; + let interval_str = kline_interval_to_binance_string(interval); + let mut params = vec![("symbol", symbol), ("interval", interval_str)]; let limit_str; let start_time_str; @@ -88,31 +89,3 @@ impl BinanceRestClient { .await } } - -/// Extension trait for `KlineInterval` to support Binance format -pub trait BinanceKlineInterval { - fn to_binance_format(&self) -> &str; -} - -impl BinanceKlineInterval for KlineInterval { - fn to_binance_format(&self) -> &str { - match self { - // Seconds1 removed - not commonly supported - Self::Minutes1 => "1m", - Self::Minutes3 => "3m", - Self::Minutes5 => "5m", - Self::Minutes15 => "15m", - Self::Minutes30 => "30m", - Self::Hours1 => "1h", - Self::Hours2 => "2h", - Self::Hours4 => "4h", - Self::Hours6 => "6h", - Self::Hours8 => "8h", - Self::Hours12 => "12h", - Self::Days1 => "1d", - Self::Days3 => "3d", - Self::Weeks1 => "1w", - Self::Months1 => "1M", - } - } -} diff --git a/src/exchanges/binance_perp/codec.rs b/src/exchanges/binance_perp/codec.rs index a532e08..34ba288 100644 --- a/src/exchanges/binance_perp/codec.rs +++ b/src/exchanges/binance_perp/codec.rs @@ -1,5 +1,6 @@ use crate::core::errors::ExchangeError; use crate::core::kernel::WsCodec; +use crate::exchanges::binance_perp::conversions::kline_interval_to_binance_perp_string; use serde_json::{json, Value}; use tokio_tungstenite::tungstenite::Message; @@ -213,7 +214,7 @@ pub fn create_binance_perp_stream_identifiers( streams.push(format!( "{}@kline_{}", lower_symbol, - interval.to_binance_format() + kline_interval_to_binance_perp_string(*interval) )); } } diff --git a/src/exchanges/binance_perp/conversions.rs b/src/exchanges/binance_perp/conversions.rs index d7dcdce..61bae95 100644 --- a/src/exchanges/binance_perp/conversions.rs +++ b/src/exchanges/binance_perp/conversions.rs @@ -2,8 +2,8 @@ use crate::core::types::{ conversion::{ string_to_decimal, string_to_price, string_to_quantity, string_to_symbol, string_to_volume, }, - Balance, Kline, Market, MarketDataType, OrderBook, OrderBookEntry, Position, PositionSide, - Ticker, Trade, + Balance, Kline, KlineInterval, Market, MarketDataType, OrderBook, OrderBookEntry, Position, + PositionSide, Ticker, Trade, }; use crate::exchanges::binance_perp::types::{ BinancePerpBalance, BinancePerpMarket, BinancePerpPosition, BinancePerpRestKline, @@ -13,6 +13,27 @@ use crate::exchanges::binance_perp::types::{ use rust_decimal::Decimal; use tracing::warn; +/// Convert kline interval to Binance perpetual format +pub fn kline_interval_to_binance_perp_string(interval: KlineInterval) -> &'static str { + match interval { + KlineInterval::Minutes1 => "1m", + KlineInterval::Minutes3 => "3m", + KlineInterval::Minutes5 => "5m", + KlineInterval::Minutes15 => "15m", + KlineInterval::Minutes30 => "30m", + KlineInterval::Hours1 => "1h", + KlineInterval::Hours2 => "2h", + KlineInterval::Hours4 => "4h", + KlineInterval::Hours6 => "6h", + KlineInterval::Hours8 => "8h", + KlineInterval::Hours12 => "12h", + KlineInterval::Days1 => "1d", + KlineInterval::Days3 => "3d", + KlineInterval::Weeks1 => "1w", + KlineInterval::Months1 => "1M", + } +} + /// Convert Binance Perpetual market to core Market type pub fn convert_binance_perp_market(binance_market: BinancePerpMarket) -> Market { Market { diff --git a/src/exchanges/binance_perp/rest.rs b/src/exchanges/binance_perp/rest.rs index 849d767..ad50a8d 100644 --- a/src/exchanges/binance_perp/rest.rs +++ b/src/exchanges/binance_perp/rest.rs @@ -1,6 +1,7 @@ use crate::core::errors::ExchangeError; use crate::core::kernel::RestClient; use crate::core::types::KlineInterval; +use crate::exchanges::binance_perp::conversions::kline_interval_to_binance_perp_string; use crate::exchanges::binance_perp::types::{ BinancePerpBalance, BinancePerpExchangeInfo, BinancePerpFundingRate, BinancePerpOrderResponse, BinancePerpPosition, BinancePerpPremiumIndex, BinancePerpRestKline, @@ -86,12 +87,12 @@ impl BinancePerpRestClient { start_time: Option, end_time: Option, ) -> Result, ExchangeError> { - let interval_str = interval.to_binance_format(); + let interval_str = kline_interval_to_binance_perp_string(interval); let limit_str = limit.map(|l| l.to_string()); let start_time_str = start_time.map(|t| t.to_string()); let end_time_str = end_time.map(|t| t.to_string()); - let mut params = vec![("symbol", symbol), ("interval", &interval_str)]; + let mut params = vec![("symbol", symbol), ("interval", interval_str)]; if let Some(ref limit) = limit_str { params.push(("limit", limit.as_str())); diff --git a/src/exchanges/bybit_perp/connector/market_data.rs b/src/exchanges/bybit_perp/connector/market_data.rs index 9f22741..e6d7eca 100644 --- a/src/exchanges/bybit_perp/connector/market_data.rs +++ b/src/exchanges/bybit_perp/connector/market_data.rs @@ -10,7 +10,9 @@ use crate::core::types::{ conversion, FundingRate, Kline, KlineInterval, Market, MarketDataType, SubscriptionType, WebSocketConfig, }; -use crate::exchanges::bybit_perp::conversions::convert_bybit_perp_market; +use crate::exchanges::bybit_perp::conversions::{ + convert_bybit_perp_market, kline_interval_to_bybit_perp_string, +}; use crate::exchanges::bybit_perp::rest::BybitPerpRestClient; use crate::exchanges::bybit_perp::types::{self as bybit_perp_types}; use async_trait::async_trait; @@ -102,7 +104,11 @@ impl MarketDataSource for MarketData { - streams.push(format!("kline.{}.{}", interval.to_bybit_format(), symbol)); + streams.push(format!( + "kline.{}.{}", + kline_interval_to_bybit_perp_string(*interval), + symbol + )); } } } @@ -181,10 +187,10 @@ impl MarketDataSource for MarketData, end_time: Option, ) -> Result, ExchangeError> { - let interval_str = interval.to_bybit_format(); + let interval_str = kline_interval_to_bybit_perp_string(interval); let klines_response = self .rest - .get_klines(&symbol, &interval_str, limit, start_time, end_time) + .get_klines(&symbol, interval_str, limit, start_time, end_time) .await?; if klines_response.ret_code != 0 { @@ -235,7 +241,7 @@ impl MarketDataSource for MarketData MarketData { } } -// Extension trait for KlineInterval to convert to Bybit format -#[allow(dead_code)] -trait BybitFormat { - fn to_bybit_format(&self) -> String; -} - -impl BybitFormat for KlineInterval { - fn to_bybit_format(&self) -> String { - match self { - // Seconds1 removed - not commonly supported - KlineInterval::Minutes1 => "1", - KlineInterval::Minutes3 => "3", - KlineInterval::Minutes5 => "5", - KlineInterval::Minutes15 => "15", - KlineInterval::Minutes30 => "30", - KlineInterval::Hours1 => "60", - KlineInterval::Hours2 => "120", - KlineInterval::Hours4 => "240", - KlineInterval::Hours6 => "360", - KlineInterval::Hours8 => "480", - KlineInterval::Hours12 => "720", - KlineInterval::Days1 => "D", - KlineInterval::Days3 => "3D", - KlineInterval::Weeks1 => "W", - KlineInterval::Months1 => "M", - } - .to_string() - } -} - /// Convert `BybitPerpWsEvent` to `MarketDataType` fn convert_bybit_event_to_market_data( event: crate::exchanges::bybit_perp::codec::BybitPerpWsEvent, diff --git a/src/exchanges/bybit_perp/conversions.rs b/src/exchanges/bybit_perp/conversions.rs index aca0c2b..cedaf0c 100644 --- a/src/exchanges/bybit_perp/conversions.rs +++ b/src/exchanges/bybit_perp/conversions.rs @@ -1,8 +1,8 @@ use super::types as bybit_perp_types; use super::types::{BybitPerpKlineData, BybitPerpMarket}; use crate::core::types::{ - Kline, Market, MarketDataType, OrderBook, OrderBookEntry, OrderSide, OrderType, Symbol, Ticker, - TimeInForce, Trade, + Kline, KlineInterval, Market, MarketDataType, OrderBook, OrderBookEntry, OrderSide, OrderType, + Symbol, Ticker, TimeInForce, Trade, }; use serde_json::Value; @@ -70,6 +70,27 @@ pub fn convert_time_in_force(tif: &TimeInForce) -> String { } } +/// Convert kline interval to Bybit perpetual format +pub fn kline_interval_to_bybit_perp_string(interval: KlineInterval) -> &'static str { + match interval { + KlineInterval::Minutes1 => "1", + KlineInterval::Minutes3 => "3", + KlineInterval::Minutes5 => "5", + KlineInterval::Minutes15 => "15", + KlineInterval::Minutes30 => "30", + KlineInterval::Hours1 => "60", + KlineInterval::Hours2 => "120", + KlineInterval::Hours4 => "240", + KlineInterval::Hours6 => "360", + KlineInterval::Hours8 => "480", + KlineInterval::Hours12 => "720", + KlineInterval::Days1 => "D", + KlineInterval::Days3 => "3D", + KlineInterval::Weeks1 => "W", + KlineInterval::Months1 => "M", + } +} + /// Convert bybit perp kline to core kline type pub fn convert_bybit_perp_kline( symbol: String, diff --git a/src/exchanges/hyperliquid/codec.rs b/src/exchanges/hyperliquid/codec.rs index a3ebc38..538dad4 100644 --- a/src/exchanges/hyperliquid/codec.rs +++ b/src/exchanges/hyperliquid/codec.rs @@ -3,6 +3,7 @@ use crate::core::kernel::codec::WsCodec; use crate::core::types::{ conversion, Kline, KlineInterval, MarketDataType, OrderBook, OrderBookEntry, Ticker, Trade, }; +use crate::exchanges::hyperliquid::conversions::convert_kline_interval_to_hyperliquid; use serde_json::{json, Value}; use tokio_tungstenite::tungstenite::Message; use tracing::warn; @@ -447,7 +448,7 @@ impl HyperliquidCodec { symbol: conversion::string_to_symbol(symbol), open_time: timestamp, close_time: timestamp, - interval: KlineInterval::Minutes1.to_binance_format(), + interval: convert_kline_interval_to_hyperliquid(KlineInterval::Minutes1), open_price: conversion::string_to_price(&open.to_string()), high_price: conversion::string_to_price(&high.to_string()), low_price: conversion::string_to_price(&low.to_string()), diff --git a/src/exchanges/paradex/codec.rs b/src/exchanges/paradex/codec.rs index ff74f19..21a21db 100644 --- a/src/exchanges/paradex/codec.rs +++ b/src/exchanges/paradex/codec.rs @@ -4,6 +4,7 @@ use crate::core::types::conversion; use crate::core::types::{ Kline, MarketDataType, OrderBook, OrderBookEntry, SubscriptionType, Ticker, Trade, }; +use crate::exchanges::paradex::conversions::kline_interval_to_paradex_string; use serde_json::{json, Value}; use tokio_tungstenite::tungstenite::Message; @@ -305,7 +306,11 @@ pub fn create_subscription_channel(symbol: &str, subscription_type: &Subscriptio ), SubscriptionType::Trades => format!("trade@{}", symbol), SubscriptionType::Klines { interval } => { - format!("kline_{}@{}", interval.to_binance_format(), symbol) + format!( + "kline_{}@{}", + kline_interval_to_paradex_string(*interval), + symbol + ) } } } diff --git a/src/exchanges/paradex/connector/market_data.rs b/src/exchanges/paradex/connector/market_data.rs index a1576c2..09edaff 100644 --- a/src/exchanges/paradex/connector/market_data.rs +++ b/src/exchanges/paradex/connector/market_data.rs @@ -7,6 +7,7 @@ use crate::core::types::{ use crate::exchanges::paradex::codec::ParadexWsEvent; use crate::exchanges::paradex::conversions::{ convert_paradex_funding_rate, convert_paradex_kline, convert_paradex_market, + kline_interval_to_paradex_string, }; use crate::exchanges::paradex::rest::ParadexRestClient; use async_trait::async_trait; @@ -196,7 +197,11 @@ fn create_subscription_channel(symbol: &str, subscription_type: &SubscriptionTyp ), SubscriptionType::Trades => format!("trade@{}", symbol), SubscriptionType::Klines { interval } => { - format!("kline_{}@{}", interval.to_binance_format(), symbol) + format!( + "kline_{}@{}", + kline_interval_to_paradex_string(*interval), + symbol + ) } } } diff --git a/src/exchanges/paradex/conversions.rs b/src/exchanges/paradex/conversions.rs index 9eb1a6b..a4b3ba8 100644 --- a/src/exchanges/paradex/conversions.rs +++ b/src/exchanges/paradex/conversions.rs @@ -1,12 +1,33 @@ use crate::core::types::{ - conversion, Balance, FundingRate, Kline, Market, OrderResponse, OrderSide, OrderType, Position, - PositionSide, Symbol, + conversion, Balance, FundingRate, Kline, KlineInterval, Market, OrderResponse, OrderSide, + OrderType, Position, PositionSide, Symbol, }; use crate::exchanges::paradex::types::{ ParadexBalance, ParadexFundingRate, ParadexMarket, ParadexOrder, ParadexPosition, }; use serde_json::Value; +/// Convert kline interval to Paradex format +pub fn kline_interval_to_paradex_string(interval: KlineInterval) -> &'static str { + match interval { + KlineInterval::Minutes1 => "1m", + KlineInterval::Minutes3 => "3m", + KlineInterval::Minutes5 => "5m", + KlineInterval::Minutes15 => "15m", + KlineInterval::Minutes30 => "30m", + KlineInterval::Hours1 => "1h", + KlineInterval::Hours2 => "2h", + KlineInterval::Hours4 => "4h", + KlineInterval::Hours6 => "6h", + KlineInterval::Hours8 => "8h", + KlineInterval::Hours12 => "12h", + KlineInterval::Days1 => "1d", + KlineInterval::Days3 => "3d", + KlineInterval::Weeks1 => "1w", + KlineInterval::Months1 => "1M", + } +} + /// Convert `ParadexMarket` to Market pub fn convert_paradex_market(market: ParadexMarket) -> Market { Market { diff --git a/src/exchanges/paradex/rest.rs b/src/exchanges/paradex/rest.rs index 4d9dc9f..55012ed 100644 --- a/src/exchanges/paradex/rest.rs +++ b/src/exchanges/paradex/rest.rs @@ -1,6 +1,7 @@ use crate::core::errors::ExchangeError; use crate::core::kernel::RestClient; use crate::core::types::KlineInterval; +use crate::exchanges::paradex::conversions::kline_interval_to_paradex_string; use crate::exchanges::paradex::types::{ ParadexBalance, ParadexFundingRate, ParadexFundingRateHistory, ParadexMarket, ParadexOrder, ParadexPosition, @@ -44,8 +45,8 @@ impl ParadexRestClient { start_time: Option, end_time: Option, ) -> Result { - let interval_str = interval.to_paradex_format(); - let mut params = vec![("symbol", symbol), ("interval", interval_str.as_str())]; + let interval_str = kline_interval_to_paradex_string(interval); + let mut params = vec![("symbol", symbol), ("interval", interval_str)]; let limit_str; let start_time_str; @@ -198,31 +199,3 @@ impl ParadexRestClient { } } } - -/// Extension trait for `KlineInterval` to support Paradex format -pub trait ParadexKlineInterval { - fn to_paradex_format(&self) -> String; -} - -impl ParadexKlineInterval for KlineInterval { - fn to_paradex_format(&self) -> String { - match self { - // Seconds1 removed - not commonly supported - Self::Minutes1 => "1m".to_string(), - Self::Minutes3 => "3m".to_string(), - Self::Minutes5 => "5m".to_string(), - Self::Minutes15 => "15m".to_string(), - Self::Minutes30 => "30m".to_string(), - Self::Hours1 => "1h".to_string(), - Self::Hours2 => "2h".to_string(), - Self::Hours4 => "4h".to_string(), - Self::Hours6 => "6h".to_string(), - Self::Hours8 => "8h".to_string(), - Self::Hours12 => "12h".to_string(), - Self::Days1 => "1d".to_string(), - Self::Days3 => "3d".to_string(), - Self::Weeks1 => "1w".to_string(), - Self::Months1 => "1M".to_string(), - } - } -}