use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct SignalRecord {
pub date: String, pub signal_type: String, pub score: Option<u32>, }
#[derive(Debug, Clone, Deserialize)]
pub struct IntradayTrade {
pub date: String, pub last_vol: u64,
pub side: String, }
#[derive(Debug, Clone, Deserialize)]
pub struct HistoricalRow {
pub date: String, pub price_close: f64,
}
pub struct DayResult {
pub date: String,
pub price_close: f64,
pub signal: Option<String>, pub score: Option<u32>,
pub num_abnormal_trades: u32,
pub price_change: Option<f64>, pub conclusion: String,
}
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();
let num_abnormal = trades.iter()
.filter(|t| t.last_vol >= last_vol_abnormal_threshold)
.count() as u32;
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);
}
}