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.
This commit is contained in:
Generated
+1
-1
@@ -1535,7 +1535,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.57.0",
|
"windows-core 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+108
-43
@@ -1,6 +1,21 @@
|
|||||||
mod records;
|
mod records;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord};
|
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 bytemuck::cast_slice;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -8,7 +23,6 @@ use std::io::Read;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use sysinfo::System;
|
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
@@ -31,18 +45,14 @@ pub struct DataServer<T: bytemuck::Pod + Send + Sync + 'static> {
|
|||||||
base_path: PathBuf,
|
base_path: PathBuf,
|
||||||
symbols: RwLock<HashMap<String, Vec<DataFile>>>,
|
symbols: RwLock<HashMap<String, Vec<DataFile>>>,
|
||||||
cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
|
cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
|
||||||
sys: RwLock<System>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
|
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
|
||||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||||
let mut sys = System::new_all();
|
|
||||||
sys.refresh_memory();
|
|
||||||
Self {
|
Self {
|
||||||
base_path: path.as_ref().to_path_buf(),
|
base_path: path.as_ref().to_path_buf(),
|
||||||
symbols: RwLock::new(HashMap::new()),
|
symbols: RwLock::new(HashMap::new()),
|
||||||
cache: 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| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
if let Some(symbol) = &self.selected_symbol {
|
if let Some(symbol) = &self.selected_symbol {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
ui.heading(format!("Data: {}", symbol));
|
ui.heading(format!("Data: {}", symbol));
|
||||||
if self.is_loading {
|
if self.is_loading {
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add(egui::Spinner::new());
|
ui.add(egui::Spinner::new());
|
||||||
ui.label("Streaming from network share...");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
match &self.loaded_data {
|
match &self.loaded_data {
|
||||||
LoadedData::M1 { chunks, total_count } => {
|
LoadedData::M1 { chunks, total_count } if *total_count > 0 => {
|
||||||
ui.label(format!("Successfully loaded {} OHLC records.", total_count));
|
let last_ts = chunks.last().and_then(|c| c.last()).map(|d| d.time).unwrap_or(0.0);
|
||||||
let row_height = ui.text_style_height(&egui::TextStyle::Body);
|
ui.label(format!("Records: {} | Latest: {}", total_count, format_ts(last_ts)));
|
||||||
egui::ScrollArea::vertical().show_rows(ui, row_height, *total_count, |ui, row_range| {
|
|
||||||
for i in row_range {
|
// Get last 200 points
|
||||||
// Find which chunk contains index i
|
let mut last_points = Vec::with_capacity(200);
|
||||||
let mut current_idx = i;
|
let mut count_needed = 200;
|
||||||
for chunk in chunks {
|
for chunk in chunks.iter().rev() {
|
||||||
if current_idx < chunk.len() {
|
let take = chunk.len().min(count_needed);
|
||||||
let d = &chunk[current_idx];
|
let start = chunk.len() - take;
|
||||||
let open = d.data.open;
|
for i in (start..chunk.len()).rev() {
|
||||||
let high = d.data.high;
|
last_points.push(&chunk[i]);
|
||||||
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();
|
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::Ticks { chunks, total_count } => {
|
LoadedData::Ticks { chunks, total_count } if *total_count > 0 => {
|
||||||
ui.label(format!("Successfully loaded {} Tick records.", total_count));
|
let last_ts = chunks.last().and_then(|c| c.last()).map(|d| d.time).unwrap_or(0.0);
|
||||||
let row_height = ui.text_style_height(&egui::TextStyle::Body);
|
ui.label(format!("Records: {} | Latest: {}", total_count, format_ts(last_ts)));
|
||||||
egui::ScrollArea::vertical().show_rows(ui, row_height, *total_count, |ui, row_range| {
|
|
||||||
for i in row_range {
|
let mut last_points = Vec::with_capacity(200);
|
||||||
let mut current_idx = i;
|
let mut count_needed = 200;
|
||||||
for chunk in chunks {
|
for chunk in chunks.iter().rev() {
|
||||||
if current_idx < chunk.len() {
|
let take = chunk.len().min(count_needed);
|
||||||
let d = &chunk[current_idx];
|
let start = chunk.len() - take;
|
||||||
let ask = d.data.ask;
|
for i in (start..chunk.len()).rev() {
|
||||||
let bid = d.data.bid;
|
last_points.push(&chunk[i]);
|
||||||
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();
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
LoadedData::None => {
|
_ => {
|
||||||
ui.label("Warte auf Daten...");
|
ui.label("Keine Daten geladen oder warte auf Stream...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user