From 57b0103899d6b1820428189e139fb33b3b62ed9d Mon Sep 17 00:00:00 2001 From: Alexey <4681325+qqrm@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:14:59 +0300 Subject: [PATCH] feat: add binance rest client --- DOCS/ARCHITECTURE.md | 1 + src/app.rs | 16 +-- .../http/binance_rest_client.rs | 134 ++++++++++++++++++ src/infrastructure/http/mod.rs | 2 + src/infrastructure/mod.rs | 2 + 5 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 src/infrastructure/http/binance_rest_client.rs create mode 100644 src/infrastructure/http/mod.rs diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 6e3ceb5..e2cafc9 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -10,6 +10,7 @@ - ✅ Solid line for the current price - ✅ Interactive tooltip - ✅ Professional look similar to TradingView +- ✅ Historical data backfill via REST when panning left See [PIPELINES.md](../.github/workflows/PIPELINES.md) for optimization details. ## 🗂️ File Structure diff --git a/src/app.rs b/src/app.rs index 43f4e3e..94473eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,10 @@ use crate::{ EDGE_GAP, LineVisibility, MAX_ELEMENT_WIDTH, MIN_ELEMENT_WIDTH, enqueue_render_task, init_render_queue, set_global_renderer, spacing_ratio_for, with_global_renderer, }, - infrastructure::{rendering::WebGpuRenderer, websocket::BinanceWebSocketClient}, + infrastructure::{ + http::binance_rest_client::BinanceRestClient, rendering::WebGpuRenderer, + websocket::BinanceWebSocketClient, + }, time_utils::format_time_label, }; use gloo_timers::future::sleep; @@ -175,15 +178,8 @@ fn fetch_more_history(set_status: WriteSignal) { let symbol = current_symbol().get_untracked(); let _ = spawn_local_with_current_owner(async move { let interval = current_interval().get_untracked(); - let client_arc = - Arc::new(Mutex::new(BinanceWebSocketClient::new(symbol.clone(), interval))); - let result = { - let client = client_arc.lock().await; - match client.fetch_historical_ui_klines_before(end_time, HISTORY_FETCH_LIMIT).await { - Ok(c) => Ok(c), - Err(_) => client.fetch_historical_data_before(end_time, HISTORY_FETCH_LIMIT).await, - } - }; + let client = BinanceRestClient::new(symbol.clone(), interval); + let result = client.fetch_historical_before(end_time, HISTORY_FETCH_LIMIT).await; match result { Ok(mut new_candles) => { new_candles.sort_by_key(|c| c.timestamp.value()); diff --git a/src/infrastructure/http/binance_rest_client.rs b/src/infrastructure/http/binance_rest_client.rs new file mode 100644 index 0000000..34315a8 --- /dev/null +++ b/src/infrastructure/http/binance_rest_client.rs @@ -0,0 +1,134 @@ +use crate::domain::logging::{LogComponent, get_logger}; +use crate::domain::market_data::{ + Candle, TimeInterval, + value_objects::{OHLCV, Price, Symbol, Timestamp, Volume}, +}; +use gloo_net::http::Request; + +#[derive(Debug, serde::Deserialize)] +struct BinanceHistoricalKline( + u64, + String, + String, + String, + String, + String, + serde::de::IgnoredAny, + serde::de::IgnoredAny, + serde::de::IgnoredAny, + serde::de::IgnoredAny, + serde::de::IgnoredAny, + serde::de::IgnoredAny, +); + +/// Simple REST client for Binance API +pub struct BinanceRestClient { + symbol: Symbol, + interval: TimeInterval, +} + +impl BinanceRestClient { + pub fn new(symbol: Symbol, interval: TimeInterval) -> Self { + Self { symbol, interval } + } + + fn base_url(&self) -> String { + "https://api.binance.com/api/v3".to_string() + } + + pub fn ui_klines_url_before(&self, end_time: u64, limit: u32) -> String { + format!( + "{}/uiKlines?symbol={}&interval={}&endTime={}&limit={}", + self.base_url(), + self.symbol.value().to_uppercase(), + self.interval.to_binance_str(), + end_time, + limit + ) + } + + pub fn klines_url_before(&self, end_time: u64, limit: u32) -> String { + format!( + "{}/klines?symbol={}&interval={}&endTime={}&limit={}", + self.base_url(), + self.symbol.value().to_uppercase(), + self.interval.to_binance_str(), + end_time, + limit + ) + } + + /// Fetch candles before the specified time, using uiKlines then falling back to klines + pub async fn fetch_historical_before( + &self, + end_time: u64, + limit: u32, + ) -> Result, String> { + match self.fetch_from_url(self.ui_klines_url_before(end_time, limit)).await { + Ok(c) if !c.is_empty() => Ok(c), + _ => self.fetch_from_url(self.klines_url_before(end_time, limit)).await, + } + } + + async fn fetch_from_url(&self, url: String) -> Result, String> { + get_logger().info( + LogComponent::Infrastructure("BinanceAPI"), + &format!("📈 Fetching candles from: {url}"), + ); + + let response = Request::get(&url) + .send() + .await + .map_err(|e| format!("Failed to fetch historical data: {e:?}"))?; + + if !response.ok() { + return Err(format!("HTTP error: {}", response.status())); + } + + let klines: Vec = + response.json().await.map_err(|e| format!("Failed to parse JSON: {e:?}"))?; + + let mut candles = Vec::new(); + for kline in klines { + let open = kline.1.parse::().map_err(|_| "Invalid open price")?; + let high = kline.2.parse::().map_err(|_| "Invalid high price")?; + let low = kline.3.parse::().map_err(|_| "Invalid low price")?; + let close = kline.4.parse::().map_err(|_| "Invalid close price")?; + let volume = kline.5.parse::().map_err(|_| "Invalid volume")?; + + let ohlcv = OHLCV::new( + Price::new(open), + Price::new(high), + Price::new(low), + Price::new(close), + Volume::new(volume), + ); + + let candle = Candle::new(Timestamp::new(kline.0), ohlcv); + candles.push(candle); + } + + get_logger().info( + LogComponent::Infrastructure("BinanceAPI"), + &format!("✅ Loaded {} historical candles", candles.len()), + ); + + Ok(candles) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::market_data::TimeInterval; + + #[test] + fn test_ui_klines_url_before() { + let client = BinanceRestClient::new(Symbol::from("BTCUSDT"), TimeInterval::OneMinute); + let url = client.ui_klines_url_before(12345, 1000); + assert_eq!( + url, + "https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1m&endTime=12345&limit=1000" + ); + } +} diff --git a/src/infrastructure/http/mod.rs b/src/infrastructure/http/mod.rs new file mode 100644 index 0000000..6103d75 --- /dev/null +++ b/src/infrastructure/http/mod.rs @@ -0,0 +1,2 @@ +//! REST clients for external APIs. +pub mod binance_rest_client; diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index 40a2868..479eb91 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -3,6 +3,7 @@ //! This module houses rendering and WebSocket communication layers along with //! helper utilities such as logging and time providers. +pub mod http; pub mod rendering; pub mod websocket; @@ -116,6 +117,7 @@ pub mod services { } } +pub use http::*; pub use rendering::*; pub use services::*; pub use websocket::*;