From 0c57ff4392ea4fffb9ba9f150fd7865dffae1ff0 Mon Sep 17 00:00:00 2001 From: Alexey <4681325+qqrm@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:36:26 +0300 Subject: [PATCH] test: cover backfill merging --- src/app.rs | 63 ++++++++++++------- .../websocket/binance_client.rs | 60 ++++++++++++++++++ tests/history_backfill.rs | 44 +++++++++++++ tests/history_fetch.rs | 6 +- tests/pan_offset.rs | 21 ++----- 5 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 tests/history_backfill.rs diff --git a/src/app.rs b/src/app.rs index a11fe98..bdcc77e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; +use std::time::Duration; use wasm_bindgen::JsCast; use crate::event_utils::{EventOptions, wheel_event_options, window_event_listener_with_options}; @@ -33,32 +34,27 @@ use crate::{ infrastructure::{rendering::WebGpuRenderer, websocket::BinanceWebSocketClient}, time_utils::format_time_label, }; +use gloo_timers::future::sleep; /// Maximum number of candles visible at 1x zoom const MAX_VISIBLE_CANDLES: f64 = 32.0; /// Minimum number of candles that must remain visible const MIN_VISIBLE_CANDLES: f64 = 1.0; -/// Default canvas width -const CHART_WIDTH: f64 = 800.0; - -/// Base factor for converting mouse movement to candle offset -pub const PAN_SENSITIVITY_BASE: f64 = MAX_VISIBLE_CANDLES / CHART_WIDTH; - /// Minimum allowed zoom level const MIN_ZOOM_LEVEL: f64 = MAX_VISIBLE_CANDLES / 300.0; /// Maximum allowed zoom level const MAX_ZOOM_LEVEL: f64 = 32.0; -/// Pan offset required to trigger history loading -pub const HISTORY_FETCH_THRESHOLD: f64 = -50.0; +/// Index threshold to trigger history backfill +pub const HISTORY_PRELOAD_THRESHOLD: usize = 200; -/// Number of candles kept in memory beyond the visible range -const HISTORY_BUFFER_SIZE: usize = 150; +/// Maximum candles per backfill request +const HISTORY_FETCH_LIMIT: u32 = 1000; /// Check if more historical data should be fetched -pub fn should_fetch_history(pan: f64) -> bool { - pan <= HISTORY_FETCH_THRESHOLD +pub fn should_fetch_history(left_index: usize) -> bool { + left_index < HISTORY_PRELOAD_THRESHOLD } /// Calculate visible range based on zoom level and pan offset @@ -179,21 +175,17 @@ fn fetch_more_history(set_status: WriteSignal) { let interval = current_interval().get_untracked(); let client_arc = Arc::new(Mutex::new(BinanceWebSocketClient::new(symbol.clone(), interval))); - let visible = chart.with(|c| { - let interval = current_interval().get_untracked(); - let series = c.get_series(interval).unwrap(); - let (zoom, pan) = viewport_zoom_pan(series.get_candles(), &c.viewport); - let len = c.get_candle_count(); - visible_range(len, zoom, pan).1 - }); - let limit = (visible + HISTORY_BUFFER_SIZE) as u32; let result = { let client = client_arc.lock().await; - client.fetch_historical_data_before(end_time, limit).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, + } }; match result { Ok(mut new_candles) => { - new_candles.sort_by(|a, b| a.timestamp.value().cmp(&b.timestamp.value())); + new_candles.sort_by_key(|c| c.timestamp.value()); + new_candles.dedup_by_key(|c| c.timestamp.value()); chart.update(|ch| { for candle in new_candles.iter() { ch.add_candle(candle.clone()); @@ -232,6 +224,7 @@ fn fetch_more_history(set_status: WriteSignal) { Err(e) => set_status.set(format!("❌ Failed to load more data: {e}")), } + sleep(Duration::from_millis(500)).await; loading_more().set(false); }); } @@ -670,6 +663,7 @@ fn ChartContainer() -> impl IntoView { // 🎯 Mouse events for the tooltip let handle_mouse_move = { let chart_signal = chart; + let status_clone = set_status; move |event: web_sys::MouseEvent| { let mouse_x = event.offset_x() as f64; let mouse_y = event.offset_y() as f64; @@ -699,6 +693,13 @@ fn ChartContainer() -> impl IntoView { }); } })); + let need_history = chart_signal().with_untracked(|ch| { + let len = ch.get_candle_count(); + view_state().with(|v| v.visible_range(len, 800.0).0) + }); + if should_fetch_history(need_history) { + fetch_more_history(status_clone); + } } else { // Convert to NDC coordinates (assuming an 800x500 canvas) let canvas_width = 800.0; @@ -753,6 +754,7 @@ fn ChartContainer() -> impl IntoView { // 🔍 Mouse wheel zoom - simplified without effects let handle_wheel = { let chart_signal = chart; + let status_clone = set_status; move |event: web_sys::WheelEvent| { if chart_signal().try_get_untracked().is_none() { return; @@ -779,6 +781,13 @@ fn ChartContainer() -> impl IntoView { }); } }); + let need_history = chart_signal().with_untracked(|ch| { + let len = ch.get_candle_count(); + view_state().with(|v| v.visible_range(len, 800.0).0) + }); + if should_fetch_history(need_history) { + fetch_more_history(status_clone); + } get_logger().info(LogComponent::Presentation("ChartZoom"), "🔍 Zoom applied"); } }; @@ -872,8 +881,11 @@ fn ChartContainer() -> impl IntoView { let need_history = chart_signal().with_untracked(|c| { let interval = current_interval().get_untracked(); let series = c.get_series(interval).unwrap(); - let (_, pan) = viewport_zoom_pan(series.get_candles(), &c.viewport); - should_fetch_history(pan) + view_state().with(|v| { + let len = series.get_candles().len(); + let (start, _) = v.visible_range(len, 800.0); + should_fetch_history(start) + }) }); if need_history { fetch_more_history(status_clone); @@ -911,6 +923,9 @@ fn ChartContainer() -> impl IntoView {
+ +
Loading...
+
Result, String> { + let symbol_upper = self.symbol.value().to_uppercase(); + let interval_str = self.interval.to_binance_str(); + + let url = format!( + "https://api.binance.com/api/v3/uiKlines?symbol={symbol_upper}&interval={interval_str}&endTime={end_time}&limit={limit}" + ); + + get_logger().info( + LogComponent::Infrastructure("BinanceAPI"), + &format!("📈 Fetching {limit} uiKlines before {end_time} 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 {} uiKlines", candles.len()), + ); + + Ok(candles) + } } /// Simple helper to create a WebSocket connection diff --git a/tests/history_backfill.rs b/tests/history_backfill.rs new file mode 100644 index 0000000..e63f1a0 --- /dev/null +++ b/tests/history_backfill.rs @@ -0,0 +1,44 @@ +use price_chart_wasm::domain::{ + chart::{Chart, value_objects::ChartType}, + market_data::{Candle, OHLCV, Price, TimeInterval, Timestamp, Volume}, +}; + +fn make_candle(ts: u64) -> Candle { + Candle::new( + Timestamp::new(ts), + OHLCV::new( + Price::new(1.0), + Price::new(1.0), + Price::new(1.0), + Price::new(1.0), + Volume::new(1.0), + ), + ) +} + +fn merge_batch(chart: &mut Chart, mut batch: Vec) { + batch.sort_by_key(|c| c.timestamp.value()); + batch.dedup_by_key(|c| c.timestamp.value()); + for candle in batch { + chart.add_candle(candle); + } +} + +#[test] +fn backfill_three_batches() { + let mut chart = Chart::new("TST".to_string(), ChartType::Candlestick, 100); + for ts in 1000..=1002 { + chart.add_candle(make_candle(ts)); + } + + merge_batch(&mut chart, (997..=999).map(make_candle).collect()); + merge_batch(&mut chart, (994..=996).map(make_candle).collect()); + merge_batch(&mut chart, (991..=993).map(make_candle).collect()); + + let series = chart.get_series(TimeInterval::TwoSeconds).unwrap(); + let candles = series.get_candles(); + assert_eq!(candles.len(), 12); + for (i, c) in candles.iter().enumerate() { + assert_eq!(c.timestamp.value(), 991 + i as u64); + } +} diff --git a/tests/history_fetch.rs b/tests/history_fetch.rs index 50106df..c53878b 100644 --- a/tests/history_fetch.rs +++ b/tests/history_fetch.rs @@ -1,7 +1,7 @@ -use price_chart_wasm::app::should_fetch_history; +use price_chart_wasm::app::{HISTORY_PRELOAD_THRESHOLD, should_fetch_history}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[test] fn history_threshold_check() { - assert!(should_fetch_history(-60.0)); - assert!(!should_fetch_history(-10.0)); + assert!(should_fetch_history(HISTORY_PRELOAD_THRESHOLD - 1)); + assert!(!should_fetch_history(HISTORY_PRELOAD_THRESHOLD + 1)); } diff --git a/tests/pan_offset.rs b/tests/pan_offset.rs index 62b9c52..e51129d 100644 --- a/tests/pan_offset.rs +++ b/tests/pan_offset.rs @@ -1,20 +1,7 @@ -use price_chart_wasm::app::{HISTORY_FETCH_THRESHOLD, PAN_SENSITIVITY_BASE, should_fetch_history}; +use price_chart_wasm::app::{HISTORY_PRELOAD_THRESHOLD, should_fetch_history}; #[test] -fn pan_direction_and_history_activation() { - let mut offset = 0.0; - let delta_x = 10.0; - let zoom_level = 1.0; - let pan_sensitivity = PAN_SENSITIVITY_BASE / zoom_level; - - // Simulate dragging to the right - offset -= delta_x * pan_sensitivity; - assert!(offset < 0.0); - assert!(!should_fetch_history(offset)); - - // Drag far enough to cross the history threshold - let mut offset_big = 0.0; - let delta_x_big = (HISTORY_FETCH_THRESHOLD.abs() + 1.0) / pan_sensitivity; - offset_big -= delta_x_big * pan_sensitivity; - assert!(should_fetch_history(offset_big)); +fn start_index_triggers_history() { + assert!(should_fetch_history(HISTORY_PRELOAD_THRESHOLD - 5)); + assert!(!should_fetch_history(HISTORY_PRELOAD_THRESHOLD + 5)); }