NAC_Blockchain/nac-ai-valuation/src/history.rs

590 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 历史跟踪系统
//!
//! 提供估值历史记录、趋势分析、数据可视化和导出功能
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use anyhow::{Result, Context};
use std::collections::VecDeque;
use chrono::{DateTime, Utc};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::{FinalValuationResult, Jurisdiction, InternationalAgreement};
/// 历史记录条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValuationHistoryEntry {
/// 资产ID
pub asset_id: String,
/// 辖区
pub jurisdiction: Jurisdiction,
/// 国际协定
pub agreement: InternationalAgreement,
/// 估值结果
pub result: FinalValuationResult,
/// 记录时间
pub timestamp: DateTime<Utc>,
/// 用户ID可选
pub user_id: Option<String>,
/// 备注(可选)
pub notes: Option<String>,
}
impl ValuationHistoryEntry {
/// 创建新的历史记录
pub fn new(
asset_id: String,
jurisdiction: Jurisdiction,
agreement: InternationalAgreement,
result: FinalValuationResult,
) -> Self {
Self {
asset_id,
jurisdiction,
agreement,
result,
timestamp: Utc::now(),
user_id: None,
notes: None,
}
}
/// 设置用户ID
pub fn with_user_id(mut self, user_id: String) -> Self {
self.user_id = Some(user_id);
self
}
/// 设置备注
pub fn with_notes(mut self, notes: String) -> Self {
self.notes = Some(notes);
self
}
}
/// 历史记录存储
pub struct ValuationHistory {
/// 内存存储(最近的记录)
memory_storage: VecDeque<ValuationHistoryEntry>,
/// 最大内存记录数
max_memory_entries: usize,
/// 持久化文件路径
persistence_path: Option<String>,
}
impl ValuationHistory {
/// 创建新的历史记录存储
pub fn new(max_memory_entries: usize) -> Self {
Self {
memory_storage: VecDeque::new(),
max_memory_entries,
persistence_path: None,
}
}
/// 设置持久化路径
pub fn with_persistence(mut self, path: String) -> Self {
self.persistence_path = Some(path);
self
}
/// 添加历史记录
pub fn add(&mut self, entry: ValuationHistoryEntry) -> Result<()> {
// 添加到内存存储
self.memory_storage.push_back(entry.clone());
// 如果超过最大数量,移除最旧的记录
if self.memory_storage.len() > self.max_memory_entries {
self.memory_storage.pop_front();
}
// 持久化到文件
if let Some(ref path) = self.persistence_path {
self.persist_entry(&entry, path)?;
}
Ok(())
}
/// 持久化单条记录到文件
fn persist_entry(&self, entry: &ValuationHistoryEntry, path: &str) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.context("打开持久化文件失败")?;
let json = serde_json::to_string(entry)?;
writeln!(file, "{}", json)?;
Ok(())
}
/// 获取指定资产的所有历史记录
pub fn get_by_asset(&self, asset_id: &str) -> Vec<ValuationHistoryEntry> {
self.memory_storage
.iter()
.filter(|entry| entry.asset_id == asset_id)
.cloned()
.collect()
}
/// 获取指定时间范围内的历史记录
pub fn get_by_time_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Vec<ValuationHistoryEntry> {
self.memory_storage
.iter()
.filter(|entry| entry.timestamp >= start && entry.timestamp <= end)
.cloned()
.collect()
}
/// 获取最近N条记录
pub fn get_recent(&self, count: usize) -> Vec<ValuationHistoryEntry> {
self.memory_storage
.iter()
.rev()
.take(count)
.cloned()
.collect()
}
/// 获取所有记录
pub fn get_all(&self) -> Vec<ValuationHistoryEntry> {
self.memory_storage.iter().cloned().collect()
}
/// 清空内存存储
pub fn clear(&mut self) {
self.memory_storage.clear();
}
/// 从持久化文件加载历史记录
pub fn load_from_file(&mut self, path: &str) -> Result<usize> {
let content = std::fs::read_to_string(path)
.context("读取持久化文件失败")?;
let mut count = 0;
for line in content.lines() {
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<ValuationHistoryEntry>(line) {
Ok(entry) => {
self.memory_storage.push_back(entry);
count += 1;
// 保持最大数量限制
if self.memory_storage.len() > self.max_memory_entries {
self.memory_storage.pop_front();
}
}
Err(e) => {
log::warn!("解析历史记录失败: {}", e);
}
}
}
Ok(count)
}
}
/// 趋势分析结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendAnalysis {
/// 资产ID
pub asset_id: String,
/// 分析时间范围
pub time_range: (DateTime<Utc>, DateTime<Utc>),
/// 数据点数量
pub data_points: usize,
/// 平均估值
pub average_valuation: Decimal,
/// 最小估值
pub min_valuation: Decimal,
/// 最大估值
pub max_valuation: Decimal,
/// 标准差
pub std_deviation: f64,
/// 变化率(%
pub change_rate: f64,
/// 趋势方向
pub trend_direction: TrendDirection,
/// 波动性
pub volatility: f64,
/// 置信度平均值
pub avg_confidence: f64,
}
/// 趋势方向
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TrendDirection {
/// 上升
Upward,
/// 下降
Downward,
/// 稳定
Stable,
/// 波动
Volatile,
}
/// 趋势分析器
pub struct TrendAnalyzer;
impl TrendAnalyzer {
/// 分析资产估值趋势
pub fn analyze(entries: &[ValuationHistoryEntry]) -> Result<TrendAnalysis> {
if entries.is_empty() {
anyhow::bail!("没有历史数据可供分析");
}
let asset_id = entries[0].asset_id.clone();
// 按时间排序
let mut sorted_entries = entries.to_vec();
sorted_entries.sort_by_key(|e| e.timestamp);
let valuations: Vec<Decimal> = sorted_entries
.iter()
.map(|e| e.result.valuation_xtzh)
.collect();
let confidences: Vec<f64> = sorted_entries
.iter()
.map(|e| e.result.confidence)
.collect();
// 计算统计指标
let average_valuation = Self::calculate_average(&valuations);
let min_valuation = valuations.iter().min().expect("mainnet: handle error").clone();
let max_valuation = valuations.iter().max().expect("mainnet: handle error").clone();
let std_deviation = Self::calculate_std_deviation(&valuations, average_valuation);
// 计算变化率
let first_val = valuations.first().expect("mainnet: handle error");
let last_val = valuations.last().expect("mainnet: handle error");
let change_rate = if *first_val > Decimal::ZERO {
((*last_val - *first_val) / *first_val * Decimal::new(100, 0))
.to_string()
.parse::<f64>()
.unwrap_or(0.0)
} else {
0.0
};
// 判断趋势方向
let trend_direction = Self::determine_trend_direction(change_rate, std_deviation);
// 计算波动性
let volatility = Self::calculate_volatility(&valuations);
// 计算平均置信度
let avg_confidence = confidences.iter().sum::<f64>() / confidences.len() as f64;
let time_range = (
sorted_entries.first().expect("mainnet: handle error").timestamp,
sorted_entries.last().expect("mainnet: handle error").timestamp,
);
Ok(TrendAnalysis {
asset_id,
time_range,
data_points: entries.len(),
average_valuation,
min_valuation,
max_valuation,
std_deviation,
change_rate,
trend_direction,
volatility,
avg_confidence,
})
}
/// 计算平均值
fn calculate_average(values: &[Decimal]) -> Decimal {
let sum: Decimal = values.iter().sum();
sum / Decimal::new(values.len() as i64, 0)
}
/// 计算标准差
fn calculate_std_deviation(values: &[Decimal], mean: Decimal) -> f64 {
let variance: f64 = values
.iter()
.map(|v| {
let diff = (*v - mean).to_string().parse::<f64>().unwrap_or(0.0);
diff * diff
})
.sum::<f64>() / values.len() as f64;
variance.sqrt()
}
/// 判断趋势方向
fn determine_trend_direction(change_rate: f64, std_deviation: f64) -> TrendDirection {
if std_deviation > 1000.0 {
TrendDirection::Volatile
} else if change_rate > 5.0 {
TrendDirection::Upward
} else if change_rate < -5.0 {
TrendDirection::Downward
} else {
TrendDirection::Stable
}
}
/// 计算波动性
fn calculate_volatility(values: &[Decimal]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let returns: Vec<f64> = values
.windows(2)
.map(|w| {
let prev = w[0].to_string().parse::<f64>().unwrap_or(1.0);
let curr = w[1].to_string().parse::<f64>().unwrap_or(1.0);
(curr - prev) / prev
})
.collect();
let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
let variance = returns
.iter()
.map(|r| (r - mean_return).powi(2))
.sum::<f64>() / returns.len() as f64;
variance.sqrt()
}
}
/// 数据导出器
pub struct DataExporter;
impl DataExporter {
/// 导出为JSON格式
pub fn export_json(entries: &[ValuationHistoryEntry], path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(entries)?;
let mut file = File::create(path)?;
file.write_all(json.as_bytes())?;
Ok(())
}
/// 导出为CSV格式
pub fn export_csv(entries: &[ValuationHistoryEntry], path: &Path) -> Result<()> {
let mut file = File::create(path)?;
// 写入CSV头
writeln!(
file,
"Timestamp,Asset ID,Jurisdiction,Agreement,Valuation (XTZH),Confidence,Requires Review,Notes"
)?;
// 写入数据行
for entry in entries {
writeln!(
file,
"{},{},{:?},{:?},{},{},{},{}",
entry.timestamp.to_rfc3339(),
entry.asset_id,
entry.jurisdiction,
entry.agreement,
entry.result.valuation_xtzh,
entry.result.confidence,
entry.result.requires_human_review,
entry.notes.as_deref().unwrap_or(""),
)?;
}
Ok(())
}
/// 导出为Markdown报告
pub fn export_markdown(
entries: &[ValuationHistoryEntry],
trend: &TrendAnalysis,
path: &Path,
) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# NAC资产估值历史报告\n")?;
writeln!(file, "## 趋势分析\n")?;
writeln!(file, "- **资产ID**: {}", trend.asset_id)?;
writeln!(file, "- **数据点数量**: {}", trend.data_points)?;
writeln!(file, "- **时间范围**: {} 至 {}",
trend.time_range.0.format("%Y-%m-%d %H:%M:%S"),
trend.time_range.1.format("%Y-%m-%d %H:%M:%S")
)?;
writeln!(file, "- **平均估值**: {} XTZH", trend.average_valuation)?;
writeln!(file, "- **估值范围**: {} - {} XTZH", trend.min_valuation, trend.max_valuation)?;
writeln!(file, "- **标准差**: {:.2}", trend.std_deviation)?;
writeln!(file, "- **变化率**: {:.2}%", trend.change_rate)?;
writeln!(file, "- **趋势方向**: {:?}", trend.trend_direction)?;
writeln!(file, "- **波动性**: {:.4}", trend.volatility)?;
writeln!(file, "- **平均置信度**: {:.1}%\n", trend.avg_confidence * 100.0)?;
writeln!(file, "## 历史记录\n")?;
writeln!(file, "| 时间 | 估值 (XTZH) | 置信度 | 需要审核 |")?;
writeln!(file, "|------|-------------|--------|----------|")?;
for entry in entries {
writeln!(
file,
"| {} | {} | {:.1}% | {} |",
entry.timestamp.format("%Y-%m-%d %H:%M"),
entry.result.valuation_xtzh,
entry.result.confidence * 100.0,
if entry.result.requires_human_review { "" } else { "" }
)?;
}
Ok(())
}
/// 导出为HTML可视化报告
pub fn export_html(
entries: &[ValuationHistoryEntry],
trend: &TrendAnalysis,
path: &Path,
) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "<!DOCTYPE html>")?;
writeln!(file, "<html lang='zh-CN'>")?;
writeln!(file, "<head>")?;
writeln!(file, " <meta charset='UTF-8'>")?;
writeln!(file, " <meta name='viewport' content='width=device-width, initial-scale=1.0'>")?;
writeln!(file, " <title>NAC资产估值历史报告</title>")?;
writeln!(file, " <script src='https://cdn.jsdelivr.net/npm/chart.js'></script>")?;
writeln!(file, " <style>")?;
writeln!(file, " body {{ font-family: Arial, sans-serif; margin: 20px; }}")?;
writeln!(file, " h1 {{ color: #333; }}")?;
writeln!(file, " .stats {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }}")?;
writeln!(file, " .stat-card {{ background: #f5f5f5; padding: 15px; border-radius: 5px; }}")?;
writeln!(file, " .stat-label {{ font-size: 14px; color: #666; }}")?;
writeln!(file, " .stat-value {{ font-size: 24px; font-weight: bold; color: #333; }}")?;
writeln!(file, " canvas {{ max-width: 100%; height: 400px; }}")?;
writeln!(file, " </style>")?;
writeln!(file, "</head>")?;
writeln!(file, "<body>")?;
writeln!(file, " <h1>NAC资产估值历史报告</h1>")?;
writeln!(file, " <div class='stats'>")?;
writeln!(file, " <div class='stat-card'>")?;
writeln!(file, " <div class='stat-label'>平均估值</div>")?;
writeln!(file, " <div class='stat-value'>{} XTZH</div>", trend.average_valuation)?;
writeln!(file, " </div>")?;
writeln!(file, " <div class='stat-card'>")?;
writeln!(file, " <div class='stat-label'>变化率</div>")?;
writeln!(file, " <div class='stat-value'>{:.2}%</div>", trend.change_rate)?;
writeln!(file, " </div>")?;
writeln!(file, " <div class='stat-card'>")?;
writeln!(file, " <div class='stat-label'>平均置信度</div>")?;
writeln!(file, " <div class='stat-value'>{:.1}%</div>", trend.avg_confidence * 100.0)?;
writeln!(file, " </div>")?;
writeln!(file, " </div>")?;
writeln!(file, " <canvas id='valuationChart'></canvas>")?;
writeln!(file, " <script>")?;
writeln!(file, " const ctx = document.getElementById('valuationChart').getContext('2d');")?;
writeln!(file, " const chart = new Chart(ctx, {{")?;
writeln!(file, " type: 'line',")?;
writeln!(file, " data: {{")?;
writeln!(file, " labels: [")?;
for entry in entries {
writeln!(file, " '{}',", entry.timestamp.format("%m-%d %H:%M"))?;
}
writeln!(file, " ],")?;
writeln!(file, " datasets: [{{")?;
writeln!(file, " label: '估值 (XTZH)',")?;
writeln!(file, " data: [")?;
for entry in entries {
writeln!(file, " {},", entry.result.valuation_xtzh)?;
}
writeln!(file, " ],")?;
writeln!(file, " borderColor: 'rgb(75, 192, 192)',")?;
writeln!(file, " tension: 0.1")?;
writeln!(file, " }}]")?;
writeln!(file, " }},")?;
writeln!(file, " options: {{")?;
writeln!(file, " responsive: true,")?;
writeln!(file, " plugins: {{")?;
writeln!(file, " title: {{ display: true, text: '估值趋势图' }}")?;
writeln!(file, " }}")?;
writeln!(file, " }}")?;
writeln!(file, " }});")?;
writeln!(file, " </script>")?;
writeln!(file, "</body>")?;
writeln!(file, "</html>")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn create_test_entry(valuation: i64) -> ValuationHistoryEntry {
ValuationHistoryEntry::new(
"test_asset".to_string(),
Jurisdiction::US,
InternationalAgreement::WTO,
FinalValuationResult {
valuation_xtzh: Decimal::new(valuation, 0),
confidence: 0.85,
model_results: vec![],
weights: HashMap::new(),
is_anomaly: false,
anomaly_report: None,
divergence_report: "Test".to_string(),
requires_human_review: false,
},
)
}
#[test]
fn test_history_storage() {
let mut history = ValuationHistory::new(10);
let entry = create_test_entry(1000000);
history.add(entry.clone()).expect("mainnet: handle error");
let entries = history.get_by_asset("test_asset");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].result.valuation_xtzh, Decimal::new(1000000, 0));
}
#[test]
fn test_trend_analysis() {
let entries = vec![
create_test_entry(1000000),
create_test_entry(1050000),
create_test_entry(1100000),
create_test_entry(1150000),
];
let trend = TrendAnalyzer::analyze(&entries).expect("mainnet: handle error");
assert_eq!(trend.data_points, 4);
assert!(trend.change_rate > 0.0); // 上升趋势
}
#[test]
fn test_data_export_json() {
let entries = vec![create_test_entry(1000000)];
let path = Path::new("/tmp/test_export.json");
let result = DataExporter::export_json(&entries, path);
assert!(result.is_ok());
}
}