From f8efc672a0ec5b669b75dbb8f93f3723c4ffbfbe Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Mon, 16 Feb 2026 17:12:08 +0100 Subject: [PATCH] Refactor data display and add timestamp formatting Introduced a helper function to format Delphi TDateTime values into human-readable strings. Updated the data display to render the latest 200 data points as a chart instead of a list. This change improves the visualization of market data by providing a graphical representation of recent price movements. --- Cargo.lock | 2 +- src/main.rs | 169 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 948f790..2aeb970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,7 +1535,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.57.0", + "windows-core 0.58.0", ] [[package]] diff --git a/src/main.rs b/src/main.rs index 321278b..c83c13a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,21 @@ mod records; +use chrono::{DateTime, Utc}; use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord}; + +fn format_ts(ts: f64) -> String { + // Delphi TDateTime: Days since 1899-12-30. + // Unix Epoch (1970-01-01) is 25569.0 days after 1899-12-30. + let unix_secs = (ts - 25569.0) * 86400.0; + let secs = unix_secs.floor() as i64; + let nanos = ((unix_secs - unix_secs.floor()) * 1_000_000_000.0) as u32; + + if let Some(dt) = DateTime::from_timestamp(secs, nanos) { + dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string() + } else { + format!("{:.6}", ts) + } +} use bytemuck::cast_slice; use eframe::egui; use std::collections::HashMap; @@ -8,7 +23,6 @@ use std::io::Read; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::Instant; -use sysinfo::System; use tokio::runtime::Runtime; use zip::ZipArchive; @@ -31,18 +45,14 @@ pub struct DataServer { base_path: PathBuf, symbols: RwLock>>, cache: RwLock>>, - sys: RwLock, } impl DataServer { pub fn new(path: impl AsRef) -> Self { - let mut sys = System::new_all(); - sys.refresh_memory(); Self { base_path: path.as_ref().to_path_buf(), symbols: RwLock::new(HashMap::new()), cache: RwLock::new(HashMap::new()), - sys: RwLock::new(sys), } } @@ -363,58 +373,113 @@ impl eframe::App for DataViewerApp { egui::CentralPanel::default().show(ctx, |ui| { if let Some(symbol) = &self.selected_symbol { - ui.heading(format!("Data: {}", symbol)); - if self.is_loading { - ui.horizontal(|ui| { + ui.horizontal(|ui| { + ui.heading(format!("Data: {}", symbol)); + if self.is_loading { ui.add(egui::Spinner::new()); - ui.label("Streaming from network share..."); - }); - } - + } + }); + match &self.loaded_data { - LoadedData::M1 { chunks, total_count } => { - ui.label(format!("Successfully loaded {} OHLC records.", total_count)); - let row_height = ui.text_style_height(&egui::TextStyle::Body); - egui::ScrollArea::vertical().show_rows(ui, row_height, *total_count, |ui, row_range| { - for i in row_range { - // Find which chunk contains index i - let mut current_idx = i; - for chunk in chunks { - if current_idx < chunk.len() { - let d = &chunk[current_idx]; - let open = d.data.open; - let high = d.data.high; - let low = d.data.low; - let close = d.data.close; - ui.label(format!("{}: O:{:.5} H:{:.5} L:{:.5} C:{:.5}", i, open, high, low, close)); - break; - } - current_idx -= chunk.len(); - } + LoadedData::M1 { chunks, total_count } if *total_count > 0 => { + let last_ts = chunks.last().and_then(|c| c.last()).map(|d| d.time).unwrap_or(0.0); + ui.label(format!("Records: {} | Latest: {}", total_count, format_ts(last_ts))); + + // Get last 200 points + let mut last_points = Vec::with_capacity(200); + let mut count_needed = 200; + for chunk in chunks.iter().rev() { + let take = chunk.len().min(count_needed); + let start = chunk.len() - take; + for i in (start..chunk.len()).rev() { + last_points.push(&chunk[i]); } - }); - } - LoadedData::Ticks { chunks, total_count } => { - ui.label(format!("Successfully loaded {} Tick records.", total_count)); - let row_height = ui.text_style_height(&egui::TextStyle::Body); - egui::ScrollArea::vertical().show_rows(ui, row_height, *total_count, |ui, row_range| { - for i in row_range { - let mut current_idx = i; - for chunk in chunks { - if current_idx < chunk.len() { - let d = &chunk[current_idx]; - let ask = d.data.ask; - let bid = d.data.bid; - ui.label(format!("{}: A:{:.5} B:{:.5}", i, ask, bid)); - break; - } - current_idx -= chunk.len(); - } + count_needed -= take; + if count_needed == 0 { break; } + } + last_points.reverse(); + + // Render Chart + let (rect, _response) = ui.allocate_at_least(egui::vec2(ui.available_width(), ui.available_height()), egui::Sense::hover()); + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, egui::Color32::from_black_alpha(20)); + + if !last_points.is_empty() { + let min_y = last_points.iter().map(|d| d.data.low).fold(f64::INFINITY, f64::min); + let max_y = last_points.iter().map(|d| d.data.high).fold(f64::NEG_INFINITY, f64::max); + let range_y = (max_y - min_y).max(0.00001); + + let to_screen_y = |y: f64| { + rect.bottom() - ((y - min_y) / range_y) as f32 * rect.height() + }; + + let x_step = rect.width() / 200.0; + for (i, p) in last_points.iter().enumerate() { + let x = rect.left() + i as f32 * x_step + x_step * 0.5; + let color = if p.data.close >= p.data.open { egui::Color32::GREEN } else { egui::Color32::RED }; + + // Wick + painter.line_segment([egui::pos2(x, to_screen_y(p.data.high)), egui::pos2(x, to_screen_y(p.data.low))], (1.0, color)); + // Body + let y_open = to_screen_y(p.data.open); + let y_close = to_screen_y(p.data.close); + let body_rect = egui::Rect::from_x_y_ranges(x - x_step * 0.4..=x + x_step * 0.4, y_open.min(y_close)..=y_open.max(y_close)); + painter.rect_filled(body_rect, 0.0, color); } - }); + } } - LoadedData::None => { - ui.label("Warte auf Daten..."); + LoadedData::Ticks { chunks, total_count } if *total_count > 0 => { + let last_ts = chunks.last().and_then(|c| c.last()).map(|d| d.time).unwrap_or(0.0); + ui.label(format!("Records: {} | Latest: {}", total_count, format_ts(last_ts))); + + let mut last_points = Vec::with_capacity(200); + let mut count_needed = 200; + for chunk in chunks.iter().rev() { + let take = chunk.len().min(count_needed); + let start = chunk.len() - take; + for i in (start..chunk.len()).rev() { + last_points.push(&chunk[i]); + } + count_needed -= take; + if count_needed == 0 { break; } + } + last_points.reverse(); + + let (rect, _response) = ui.allocate_at_least(egui::vec2(ui.available_width(), ui.available_height()), egui::Sense::hover()); + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, egui::Color32::from_black_alpha(20)); + + if !last_points.is_empty() { + let min_y = last_points.iter().map(|d| d.data.bid.min(d.data.ask)).fold(f64::INFINITY, f64::min); + let max_y = last_points.iter().map(|d| d.data.bid.max(d.data.ask)).fold(f64::NEG_INFINITY, f64::max); + let range_y = (max_y - min_y).max(0.00001); + + let to_screen_y = |y: f64| { + rect.bottom() - ((y - min_y) / range_y) as f32 * rect.height() + }; + + let x_step = rect.width() / 200.0; + let mut last_ask = None; + let mut last_bid = None; + + for (i, p) in last_points.iter().enumerate() { + let x = rect.left() + i as f32 * x_step; + let y_ask = to_screen_y(p.data.ask); + let y_bid = to_screen_y(p.data.bid); + + if let Some(prev_x) = last_ask { + painter.line_segment([prev_x, egui::pos2(x, y_ask)], (1.0, egui::Color32::LIGHT_BLUE)); + } + if let Some(prev_x) = last_bid { + painter.line_segment([prev_x, egui::pos2(x, y_bid)], (1.0, egui::Color32::LIGHT_RED)); + } + last_ask = Some(egui::pos2(x, y_ask)); + last_bid = Some(egui::pos2(x, y_bid)); + } + } + } + _ => { + ui.label("Keine Daten geladen oder warte auf Stream..."); } } } else {