Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`.
Expand Down
42 changes: 0 additions & 42 deletions src/core/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 9 additions & 35 deletions src/exchanges/backpack/connector/market_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -173,10 +175,10 @@ impl<R: RestClient + Clone, W: WsSession<BackpackCodec>> MarketDataSource for Ma
start_time: Option<i64>,
end_time: Option<i64>,
) -> Result<Vec<Kline>, 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
Expand All @@ -185,7 +187,7 @@ impl<R: RestClient + Clone, W: WsSession<BackpackCodec>> MarketDataSource for Ma
symbol: conversion::string_to_symbol(&symbol),
open_time: k.start.parse::<i64>().unwrap_or(0),
close_time: k.end.parse::<i64>().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),
Expand Down Expand Up @@ -266,10 +268,10 @@ impl<R: RestClient + Clone> MarketDataSource for MarketData<R, ()> {
start_time: Option<i64>,
end_time: Option<i64>,
) -> Result<Vec<Kline>, 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
Expand All @@ -278,7 +280,7 @@ impl<R: RestClient + Clone> MarketDataSource for MarketData<R, ()> {
symbol: conversion::string_to_symbol(&symbol),
open_time: k.start.parse::<i64>().unwrap_or(0),
close_time: k.end.parse::<i64>().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),
Expand All @@ -291,34 +293,6 @@ impl<R: RestClient + Clone> MarketDataSource for MarketData<R, ()> {
}
}

/// 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,
Expand Down
25 changes: 23 additions & 2 deletions src/exchanges/backpack/conversions.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
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,
BackpackTicker, BackpackTrade, BackpackWebSocketKline, BackpackWebSocketOrderBook,
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 {
Expand Down
32 changes: 3 additions & 29 deletions src/exchanges/backpack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
));
}
Expand All @@ -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
}
}
}
3 changes: 2 additions & 1 deletion src/exchanges/binance/codec.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)
));
}
}
Expand Down
47 changes: 45 additions & 2 deletions src/exchanges/binance/conversions.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -213,3 +234,25 @@ pub fn parse_websocket_message(value: Value) -> Option<MarketDataType> {
}
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"
);
}
}
33 changes: 3 additions & 30 deletions src/exchanges/binance/rest.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -32,8 +33,8 @@ impl<R: RestClient> BinanceRestClient<R> {
start_time: Option<i64>,
end_time: Option<i64>,
) -> Result<Vec<BinanceRestKline>, 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;
Expand Down Expand Up @@ -88,31 +89,3 @@ impl<R: RestClient> BinanceRestClient<R> {
.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",
}
}
}
3 changes: 2 additions & 1 deletion src/exchanges/binance_perp/codec.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)
));
}
}
Expand Down
Loading
Loading