Refactor: Move DataServer to its own module
Moves the `DataServer` struct and its implementation into a new `dataserver` module. This improves code organization and separates data loading logic from the main application logic.
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
use bytemuck::cast_slice;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Instant;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataFile {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub symbol: String,
|
||||||
|
pub year: i32,
|
||||||
|
pub month: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CacheEntry<T> {
|
||||||
|
pub data: Arc<Vec<T>>,
|
||||||
|
pub last_accessed: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DataServer<T: bytemuck::Pod + Send + Sync + 'static> {
|
||||||
|
base_path: PathBuf,
|
||||||
|
pub symbols: RwLock<HashMap<String, Vec<DataFile>>>,
|
||||||
|
pub cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
|
||||||
|
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||||
|
Self {
|
||||||
|
base_path: path.as_ref().to_path_buf(),
|
||||||
|
symbols: RwLock::new(HashMap::new()),
|
||||||
|
cache: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_symbols(&self) -> tokio::io::Result<()> {
|
||||||
|
let mut entries = tokio::fs::read_dir(&self.base_path).await?;
|
||||||
|
let mut temp_map: HashMap<String, Vec<DataFile>> = HashMap::new();
|
||||||
|
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if file_name.ends_with(".m1") || file_name.ends_with(".tick") {
|
||||||
|
if let Some(data_file) = self.parse_file_name(&path, file_name) {
|
||||||
|
temp_map
|
||||||
|
.entry(data_file.symbol.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(data_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for files in temp_map.values_mut() {
|
||||||
|
files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut symbols = self.symbols.write().unwrap();
|
||||||
|
*symbols = temp_map;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumerate_symbols(&self) -> Vec<String> {
|
||||||
|
let symbols = self.symbols.read().unwrap();
|
||||||
|
let mut keys: Vec<String> = symbols.keys().cloned().collect();
|
||||||
|
keys.sort();
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_file_name(&self, path: &Path, file_name: &str) -> Option<DataFile> {
|
||||||
|
let stem = Path::new(file_name).file_stem()?.to_str()?;
|
||||||
|
let ext = Path::new(file_name).extension()?.to_str()?;
|
||||||
|
let parts: Vec<&str> = stem.split('_').collect();
|
||||||
|
if parts.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let month = parts.last()?.parse::<i32>().ok()?;
|
||||||
|
let year = parts[parts.len() - 2].parse::<i32>().ok()?;
|
||||||
|
let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext);
|
||||||
|
Some(DataFile {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
symbol,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_data<R>(&self, data_file: &DataFile) -> anyhow::Result<Arc<Vec<T>>>
|
||||||
|
where
|
||||||
|
R: crate::records::SourceRecord,
|
||||||
|
crate::records::DataPoint<R::Mapped>: Into<T>,
|
||||||
|
{
|
||||||
|
{
|
||||||
|
let mut cache = self.cache.write().unwrap();
|
||||||
|
if let Some(entry) = cache.get_mut(&data_file.path) {
|
||||||
|
entry.last_accessed = Instant::now();
|
||||||
|
return Ok(Arc::clone(&entry.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = data_file.path.clone();
|
||||||
|
let shared_data = tokio::task::spawn_blocking(move || {
|
||||||
|
let file = std::fs::File::open(&path)?;
|
||||||
|
let mut archive = ZipArchive::new(file)?;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
if file.name().ends_with(".bin") {
|
||||||
|
let mut bin_content = vec![0u8; file.size() as usize];
|
||||||
|
file.read_exact(&mut bin_content)?;
|
||||||
|
let raw_data: &[R] = cast_slice(&bin_content);
|
||||||
|
|
||||||
|
// Perform mapping inside blocking task
|
||||||
|
let mapped_data: Vec<T> =
|
||||||
|
raw_data.iter().map(|&r| r.to_data_point().into()).collect();
|
||||||
|
|
||||||
|
return Ok(Arc::new(mapped_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("No .bin file found"))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let mut cache = self.cache.write().unwrap();
|
||||||
|
cache.insert(
|
||||||
|
data_file.path.clone(),
|
||||||
|
CacheEntry {
|
||||||
|
data: Arc::clone(&shared_data),
|
||||||
|
last_accessed: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(shared_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// async fn read_from_zip - removed as it was unused in main.rs context or seemingly redundant/internal helper not used in extracted public interface?
|
||||||
|
// Checking main.rs: It was NOT used in main.rs, it was a private helper method inside impl DataServer.
|
||||||
|
// However, it wasn't used by load_data either. It seems dead code or future code.
|
||||||
|
// I will include it to be safe, but make it private or allow dead code if it warns.
|
||||||
|
// Actually, looking at main.rs again, read_from_zip was defined but NOT used in the provided snippet of DataServer.
|
||||||
|
// Wait, load_data implements similar logic inline.
|
||||||
|
// I will keep it to strictly follow "never change code unless..." (preserving the code structure).
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn read_from_zip<R: bytemuck::Pod + Send + 'static>(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
) -> anyhow::Result<Vec<R>> {
|
||||||
|
let path = path.to_path_buf();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let file = std::fs::File::open(&path)?;
|
||||||
|
let mut archive = ZipArchive::new(file)?;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
if file.name().ends_with(".bin") {
|
||||||
|
let mut bin_content = vec![0u8; file.size() as usize];
|
||||||
|
file.read_exact(&mut bin_content)?;
|
||||||
|
let records: &[R] = cast_slice(&bin_content);
|
||||||
|
return Ok(records.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("No .bin file found"))
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-161
@@ -1,7 +1,12 @@
|
|||||||
|
mod dataserver;
|
||||||
mod records;
|
mod records;
|
||||||
|
|
||||||
|
use crate::dataserver::DataServer;
|
||||||
use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord};
|
use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::DateTime;
|
||||||
|
use eframe::egui;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
fn format_ts(ts: f64) -> String {
|
fn format_ts(ts: f64) -> String {
|
||||||
// Delphi TDateTime: Days since 1899-12-30.
|
// Delphi TDateTime: Days since 1899-12-30.
|
||||||
@@ -16,166 +21,6 @@ fn format_ts(ts: f64) -> String {
|
|||||||
format!("{:.6}", ts)
|
format!("{:.6}", ts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use bytemuck::cast_slice;
|
|
||||||
use eframe::egui;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::Instant;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
// --- Data Structures ---
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DataFile {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub symbol: String,
|
|
||||||
pub year: i32,
|
|
||||||
pub month: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CacheEntry<T> {
|
|
||||||
data: Arc<Vec<T>>,
|
|
||||||
last_accessed: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DataServer<T: bytemuck::Pod + Send + Sync + 'static> {
|
|
||||||
base_path: PathBuf,
|
|
||||||
symbols: RwLock<HashMap<String, Vec<DataFile>>>,
|
|
||||||
cache: RwLock<HashMap<PathBuf, CacheEntry<T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: bytemuck::Pod + Send + Sync + 'static> DataServer<T> {
|
|
||||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
|
||||||
Self {
|
|
||||||
base_path: path.as_ref().to_path_buf(),
|
|
||||||
symbols: RwLock::new(HashMap::new()),
|
|
||||||
cache: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_symbols(&self) -> tokio::io::Result<()> {
|
|
||||||
let mut entries = tokio::fs::read_dir(&self.base_path).await?;
|
|
||||||
let mut temp_map: HashMap<String, Vec<DataFile>> = HashMap::new();
|
|
||||||
|
|
||||||
while let Some(entry) = entries.next_entry().await? {
|
|
||||||
let path = entry.path();
|
|
||||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
if file_name.ends_with(".m1") || file_name.ends_with(".tick") {
|
|
||||||
if let Some(data_file) = self.parse_file_name(&path, file_name) {
|
|
||||||
temp_map
|
|
||||||
.entry(data_file.symbol.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(data_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for files in temp_map.values_mut() {
|
|
||||||
files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut symbols = self.symbols.write().unwrap();
|
|
||||||
*symbols = temp_map;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enumerate_symbols(&self) -> Vec<String> {
|
|
||||||
let symbols = self.symbols.read().unwrap();
|
|
||||||
let mut keys: Vec<String> = symbols.keys().cloned().collect();
|
|
||||||
keys.sort();
|
|
||||||
keys
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_file_name(&self, path: &Path, file_name: &str) -> Option<DataFile> {
|
|
||||||
let stem = Path::new(file_name).file_stem()?.to_str()?;
|
|
||||||
let ext = Path::new(file_name).extension()?.to_str()?;
|
|
||||||
let parts: Vec<&str> = stem.split('_').collect();
|
|
||||||
if parts.len() < 3 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let month = parts.last()?.parse::<i32>().ok()?;
|
|
||||||
let year = parts[parts.len() - 2].parse::<i32>().ok()?;
|
|
||||||
let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext);
|
|
||||||
Some(DataFile {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
symbol,
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_data<R>(&self, data_file: &DataFile) -> anyhow::Result<Arc<Vec<T>>>
|
|
||||||
where
|
|
||||||
R: crate::records::SourceRecord,
|
|
||||||
crate::records::DataPoint<R::Mapped>: Into<T>,
|
|
||||||
{
|
|
||||||
{
|
|
||||||
let mut cache = self.cache.write().unwrap();
|
|
||||||
if let Some(entry) = cache.get_mut(&data_file.path) {
|
|
||||||
entry.last_accessed = Instant::now();
|
|
||||||
return Ok(Arc::clone(&entry.data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = data_file.path.clone();
|
|
||||||
let shared_data = tokio::task::spawn_blocking(move || {
|
|
||||||
let file = std::fs::File::open(&path)?;
|
|
||||||
let mut archive = ZipArchive::new(file)?;
|
|
||||||
for i in 0..archive.len() {
|
|
||||||
let mut file = archive.by_index(i)?;
|
|
||||||
if file.name().ends_with(".bin") {
|
|
||||||
let mut bin_content = vec![0u8; file.size() as usize];
|
|
||||||
file.read_exact(&mut bin_content)?;
|
|
||||||
let raw_data: &[R] = cast_slice(&bin_content);
|
|
||||||
|
|
||||||
// Perform mapping inside blocking task
|
|
||||||
let mapped_data: Vec<T> =
|
|
||||||
raw_data.iter().map(|&r| r.to_data_point().into()).collect();
|
|
||||||
|
|
||||||
return Ok(Arc::new(mapped_data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(anyhow::anyhow!("No .bin file found"))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let mut cache = self.cache.write().unwrap();
|
|
||||||
cache.insert(
|
|
||||||
data_file.path.clone(),
|
|
||||||
CacheEntry {
|
|
||||||
data: Arc::clone(&shared_data),
|
|
||||||
last_accessed: Instant::now(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Ok(shared_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_from_zip<R: bytemuck::Pod + Send + 'static>(
|
|
||||||
&self,
|
|
||||||
path: &Path,
|
|
||||||
) -> anyhow::Result<Vec<R>> {
|
|
||||||
let path = path.to_path_buf();
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let file = std::fs::File::open(&path)?;
|
|
||||||
let mut archive = ZipArchive::new(file)?;
|
|
||||||
for i in 0..archive.len() {
|
|
||||||
let mut file = archive.by_index(i)?;
|
|
||||||
if file.name().ends_with(".bin") {
|
|
||||||
let mut bin_content = vec![0u8; file.size() as usize];
|
|
||||||
file.read_exact(&mut bin_content)?;
|
|
||||||
let records: &[R] = cast_slice(&bin_content);
|
|
||||||
return Ok(records.to_vec());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(anyhow::anyhow!("No .bin file found"))
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- EGui App ---
|
// --- EGui App ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user