Files
paracas/crates/paracas-estimate/src/data.rs
T

187 lines
5.4 KiB
Rust

//! Estimate database with historical averages.
use std::collections::HashMap;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
/// Embedded JSON data with historical size estimates.
const SIZE_ESTIMATES_JSON: &str = include_str!("../data/size_estimates.json");
/// Static estimate database instance.
static ESTIMATES: OnceLock<EstimateDatabase> = OnceLock::new();
/// Size estimate for a single instrument category.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CategoryEstimate {
/// Category name (e.g., "forex", "crypto").
pub category: String,
/// Average compressed bytes per hour of data.
pub avg_compressed_bytes_per_hour: u64,
/// Average number of ticks per hour.
pub avg_ticks_per_hour: u64,
/// Multiplier for peak trading hours.
pub peak_multiplier: f64,
}
impl CategoryEstimate {
/// Creates a new category estimate.
#[must_use]
pub fn new(
category: impl Into<String>,
avg_compressed_bytes_per_hour: u64,
avg_ticks_per_hour: u64,
peak_multiplier: f64,
) -> Self {
Self {
category: category.into(),
avg_compressed_bytes_per_hour,
avg_ticks_per_hour,
peak_multiplier,
}
}
/// Returns the maximum compressed bytes per hour (at peak).
#[must_use]
pub fn max_compressed_bytes_per_hour(&self) -> u64 {
(self.avg_compressed_bytes_per_hour as f64 * self.peak_multiplier) as u64
}
/// Returns the maximum ticks per hour (at peak).
#[must_use]
pub fn max_ticks_per_hour(&self) -> u64 {
(self.avg_ticks_per_hour as f64 * self.peak_multiplier) as u64
}
}
/// Raw JSON structure for deserialization.
#[derive(Debug, Deserialize)]
struct RawEstimateData {
categories: HashMap<String, RawCategoryEstimate>,
}
/// Raw category estimate from JSON.
#[derive(Debug, Deserialize)]
struct RawCategoryEstimate {
avg_compressed_bytes_per_hour: u64,
avg_ticks_per_hour: u64,
peak_multiplier: f64,
}
/// Database of historical size estimates per instrument category.
#[derive(Debug, Clone)]
pub struct EstimateDatabase {
categories: HashMap<String, CategoryEstimate>,
}
impl EstimateDatabase {
/// Returns the global estimate database instance.
///
/// This lazily initializes the database from embedded JSON on first access.
#[must_use]
pub fn global() -> &'static Self {
ESTIMATES.get_or_init(|| {
Self::from_json(SIZE_ESTIMATES_JSON)
.expect("embedded size_estimates.json should be valid")
})
}
/// Creates an estimate database from JSON string.
///
/// # Errors
///
/// Returns an error if the JSON is invalid.
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let raw: RawEstimateData = serde_json::from_str(json)?;
let categories = raw
.categories
.into_iter()
.map(|(name, raw_est)| {
let estimate = CategoryEstimate::new(
name.clone(),
raw_est.avg_compressed_bytes_per_hour,
raw_est.avg_ticks_per_hour,
raw_est.peak_multiplier,
);
(name, estimate)
})
.collect();
Ok(Self { categories })
}
/// Returns the estimate for a category by name.
#[must_use]
pub fn get(&self, category: &str) -> Option<&CategoryEstimate> {
self.categories.get(category)
}
/// Returns all available categories.
pub fn categories(&self) -> impl Iterator<Item = &str> {
self.categories.keys().map(String::as_str)
}
/// Returns the number of categories in the database.
#[must_use]
pub fn len(&self) -> usize {
self.categories.len()
}
/// Returns true if the database is empty.
#[must_use]
pub fn is_empty(&self) -> bool {
self.categories.is_empty()
}
/// Returns a default estimate for unknown categories.
#[must_use]
pub fn default_estimate() -> CategoryEstimate {
CategoryEstimate::new("unknown", 50000, 3000, 2.0)
}
}
impl Default for EstimateDatabase {
fn default() -> Self {
Self::global().clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_database_loads() {
let db = EstimateDatabase::global();
assert!(!db.is_empty());
assert_eq!(db.len(), 7);
}
#[test]
fn test_category_estimates_present() {
let db = EstimateDatabase::global();
let forex = db.get("forex").expect("forex should exist");
assert_eq!(forex.avg_compressed_bytes_per_hour, 75000);
assert_eq!(forex.avg_ticks_per_hour, 5000);
let crypto = db.get("crypto").expect("crypto should exist");
assert_eq!(crypto.avg_compressed_bytes_per_hour, 150000);
assert_eq!(crypto.avg_ticks_per_hour, 10000);
}
#[test]
fn test_peak_calculations() {
let estimate = CategoryEstimate::new("test", 100000, 5000, 2.0);
assert_eq!(estimate.max_compressed_bytes_per_hour(), 200000);
assert_eq!(estimate.max_ticks_per_hour(), 10000);
}
#[test]
fn test_default_estimate() {
let default = EstimateDatabase::default_estimate();
assert_eq!(default.category, "unknown");
assert_eq!(default.avg_compressed_bytes_per_hour, 50000);
}
}