shindo_coding_utils 0.2.9

A utils crates which will be used in various micro-services
Documentation
use std::collections::HashMap;
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct SignalRecord {
    pub date: String,               // YYYY-MM-DD
    pub signal_type: String,        // "foreign_volume_spike_sell" or "foreign_volume_spike_buy"
    pub score: Option<u32>,         // how many times (ex: 6)
}

#[derive(Debug, Clone, Deserialize)]
pub struct IntradayTrade {
    pub date: String,           // YYYY-MM-DD
    pub last_vol: u64,
    pub side: String,           // "B" or "S" or "BS" (if available)
}

#[derive(Debug, Clone, Deserialize)]
pub struct HistoricalRow {
    pub date: String,           // YYYY-MM-DD
    pub price_close: f64,
}

/// Result per day for MWG abnormal/foreign/block trade analysis
pub struct DayResult {
    pub date: String,
    pub price_close: f64,
    pub signal: Option<String>,   // Sell spike, Buy spike, ...
    pub score: Option<u32>,
    pub num_abnormal_trades: u32,
    pub price_change: Option<f64>, // relative, vs previous day (% change)
    pub conclusion: String,
}

/// Perform multi-day analysis with provided threshold for abnormal last_vol
pub fn analyze_pattern(
    signals: &[SignalRecord],
    intradays: &[IntradayTrade],
    historicals: &[HistoricalRow],
    last_vol_abnormal_threshold: u64,
) -> Vec<DayResult> {
    let mut signal_map: HashMap<String, (&str, Option<u32>)> = HashMap::new();
    for sig in signals {
        signal_map.insert(sig.date.clone(), (sig.signal_type.as_str(), sig.score));
    }
    let mut historical_map: HashMap<String, f64> = HashMap::new();
    for h in historicals {
        historical_map.insert(h.date.clone(), h.price_close);
    }
    let mut trades_per_day: HashMap<String, Vec<&IntradayTrade>> = HashMap::new();
    for t in intradays {
        trades_per_day.entry(t.date.clone()).or_default().push(t);
    }
    let mut sorted_dates: Vec<String> =
        historical_map.keys().cloned().collect();
    sorted_dates.sort();
    let mut last_price: Option<f64> = None;
    let mut res = Vec::new();
    for date in sorted_dates.iter() {
        let price_close = *historical_map.get(date).unwrap();
        let signal = signal_map.get(date).cloned();
        let trades = trades_per_day.get(date).cloned().unwrap_or_default();
        // Count abnormal trades
        let num_abnormal = trades.iter()
            .filter(|t| t.last_vol >= last_vol_abnormal_threshold)
            .count() as u32;
        // Detect signal type for reporting
        let (signal_type, score) = match &signal {
            Some((sig, score)) => (Some((*sig).to_string()), *score),
            None => (None, None),
        };
        let price_change = if let Some(prev) = last_price {
            Some(((price_close - prev) / prev) * 100.0)
        } else { None };
        let conclusion = match (signal_type.as_deref(), num_abnormal, price_change) {
            (Some("foreign_volume_spike_sell"), n, Some(dp)) if n > 0 && dp < 0.0 =>
                format!("Giá giảm, nhiều lệnh lớn ({} lệnh)", n),
            (Some("foreign_volume_spike_buy"), n, Some(dp)) if n > 0 && dp > 0.0 =>
                format!("Giá bật tăng, có lệnh lớn mua ({} lệnh)", n),
            (Some(sig), n, _) if n > 0 =>
                format!("{} với nhiều lệnh lớn bất thường", sig),
            _ => "Bình thường".to_string(),
        };
        res.push(DayResult {
            date: date.clone(),
            price_close,
            signal: signal_type,
            score,
            num_abnormal_trades: num_abnormal,
            price_change,
            conclusion,
        });
        last_price = Some(price_close);
    }
    res
}

pub fn print_report(results: &[DayResult]) {
    println!("{:<12} | {:>8} | {:<20} | {:>4} | {:>7} | {}",
        "Ngày", "Đóng cửa", "Trạng thái", "L.khủng", "%Δ giá", "Nhận xét");
    println!("{}", "".repeat(80));
    for r in results {
        println!("{:<12} | {:>8.0} | {:<20} | {:>4} | {:>6.2?} | {}",
            r.date,
            r.price_close,
            r.signal.clone().unwrap_or("").replace("foreign_volume_spike_sell", "Sell spike").replace("foreign_volume_spike_buy", "Buy spike"),
            r.num_abnormal_trades,
            r.price_change.unwrap_or_default(),
            r.conclusion);
    }
}