Skip to content
This repository was archived by the owner on Mar 5, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions DOCS/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 6 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,15 +178,8 @@ fn fetch_more_history(set_status: WriteSignal<String>) {
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());
Expand Down
134 changes: 134 additions & 0 deletions src/infrastructure/http/binance_rest_client.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<Candle>, 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<Vec<Candle>, 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<BinanceHistoricalKline> =
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::<f64>().map_err(|_| "Invalid open price")?;
let high = kline.2.parse::<f64>().map_err(|_| "Invalid high price")?;
let low = kline.3.parse::<f64>().map_err(|_| "Invalid low price")?;
let close = kline.4.parse::<f64>().map_err(|_| "Invalid close price")?;
let volume = kline.5.parse::<f64>().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"
);
}
}
2 changes: 2 additions & 0 deletions src/infrastructure/http/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//! REST clients for external APIs.
pub mod binance_rest_client;
2 changes: 2 additions & 0 deletions src/infrastructure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -116,6 +117,7 @@ pub mod services {
}
}

pub use http::*;
pub use rendering::*;
pub use services::*;
pub use websocket::*;
Loading