mod error;
mod models;
mod config;
mod api;
mod calculator;
mod storage;
mod notification;
mod cli;
use clap::Parser;
use crate::error::Result;
use crate::cli::{Cli, Commands, InteractiveMode};
use crate::config::AppConfig;
use crate::models::StockData;
use chrono::Utc;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
let config = AppConfig::load().unwrap_or_else(|_| {
log::warn!("无法加载配置文件,使用默认配置");
AppConfig::default()
});
match cli.command {
Commands::Calculate { code, quantity, avg_price, target_profit, max_loss, save } => {
handle_calculate(&config, &code, quantity, avg_price, target_profit, max_loss, save).await?;
}
Commands::Monitor { code, interval, retry } => {
handle_monitor(&config, &code, interval, retry).await?;
}
Commands::List { detailed } => {
handle_list(&config, detailed).await?;
}
Commands::Remove { code } => {
handle_remove(&config, &code).await?;
}
Commands::Interactive => {
InteractiveMode::run().await?;
}
Commands::Config { subcommand } => {
handle_config(&config, subcommand).await?;
}
Commands::Test { code } => {
handle_test(&config, &code).await?;
}
}
Ok(())
}
async fn handle_calculate(
config: &AppConfig,
code: &str,
quantity: f64,
avg_price: f64,
target_profit: f64,
max_loss: f64,
save: bool,
) -> Result<()> {
let stock_codes = crate::cli::parse_stock_codes(code);
if stock_codes.is_empty() {
return Err(crate::error::StockCalcError::ParseError("未提供有效的股票代码".to_string()));
}
let api = crate::api::StockApi::new(config.clone())?;
let storage = crate::storage::Storage::new(config.clone());
let notifier = crate::notification::Notifier::new(config.clone());
for (i, stock_code) in stock_codes.iter().enumerate() {
if i > 0 {
println!("{}", "━".repeat(50));
}
crate::calculator::StockCalculator::validate_input(
stock_code, quantity, avg_price, target_profit, max_loss,
)?;
let stock_data = StockData {
code: stock_code.clone(),
quantity,
avg_price,
target_profit,
max_loss,
last_updated: Utc::now(),
};
let current_price = api.fetch_stock_price(stock_code).await?;
let analysis = crate::calculator::StockCalculator::calculate_analysis(&stock_data, current_price);
notifier.print_analysis(&analysis);
notifier.check_alerts(&analysis).await?;
if save {
storage.add_stock(stock_data)?;
println!("✅ 数据已保存到数据库");
}
}
Ok(())
}
async fn handle_monitor(
config: &AppConfig,
code: &str,
interval: u64,
retry: u32,
) -> Result<()> {
let stock_codes = crate::cli::parse_stock_codes(code);
if stock_codes.is_empty() {
return Err(crate::error::StockCalcError::ParseError("未提供有效的股票代码".to_string()));
}
let api = crate::api::StockApi::new(config.clone())?;
let storage = crate::storage::Storage::new(config.clone());
let notifier = crate::notification::Notifier::new(config.clone());
let mut stock_data_map = std::collections::HashMap::new();
for stock_code in &stock_codes {
match storage.get_stock(stock_code)? {
Some(data) => {
stock_data_map.insert(stock_code.clone(), data);
}
None => {
println!("❌ 未找到股票 {} 的数据,请先使用 calculate 命令添加", stock_code);
return Ok(());
}
}
}
println!("🔄 实时监控: {} (每{}秒更新)", stock_codes.join(", "), interval);
println!("{}", "━".repeat(50));
let mut interval_timer = tokio::time::interval(Duration::from_secs(interval));
loop {
interval_timer.tick().await;
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
println!("⏰ {}", timestamp);
for (i, stock_code) in stock_codes.iter().enumerate() {
if i > 0 {
println!("{}", "─".repeat(30));
}
match api.fetch_stock_price(stock_code).await {
Ok(current_price) => {
let stock_data = &stock_data_map[stock_code];
let analysis = crate::calculator::StockCalculator::calculate_analysis(stock_data, current_price);
println!("📈 {}: {} ({})",
stock_code,
crate::calculator::StockCalculator::format_currency(current_price),
if analysis.profit_ratio > 0.0 {
format!("+{:.2}%", analysis.profit_ratio)
} else {
format!("{:.2}%", analysis.profit_ratio)
}
);
println!("📊 距离目标: {:.2}% | 距离止损: {:.2}%",
analysis.distance_to_target, analysis.distance_to_stop_loss);
notifier.check_alerts(&analysis).await?;
}
Err(e) => {
println!("❌ {} 获取价格失败: {}", stock_code, e);
}
}
}
println!();
}
}
async fn handle_list(config: &AppConfig, detailed: bool) -> Result<()> {
let storage = crate::storage::Storage::new(config.clone());
let stocks = storage.list_stocks()?;
if stocks.is_empty() {
println!("📭 数据库中没有股票数据");
println!("💡 使用 'stock-calc calculate' 或 'stock-calc interactive' 添加数据");
return Ok(());
}
println!("📋 股票列表 (共 {} 只)", stocks.len());
println!("{}", "━".repeat(50));
for stock in stocks {
println!("📈 {} - {} 股 @ ¥{:.3}",
stock.code,
stock.quantity,
stock.avg_price
);
if detailed {
println!(" 预期收益: ¥{:.3}", stock.target_profit);
println!(" 最大亏损: ¥{:.3}", stock.max_loss);
println!(" 最后更新: {}", stock.last_updated.format("%Y-%m-%d %H:%M:%S"));
println!();
}
}
if !detailed {
println!("\n💡 使用 --detailed 查看详细信息");
}
Ok(())
}
async fn handle_remove(config: &AppConfig, code: &str) -> Result<()> {
let stock_codes = crate::cli::parse_stock_codes(code);
if stock_codes.is_empty() {
return Err(crate::error::StockCalcError::ParseError("未提供有效的股票代码".to_string()));
}
let storage = crate::storage::Storage::new(config.clone());
for stock_code in stock_codes {
match storage.remove_stock(&stock_code)? {
Some(_) => println!("✅ 已删除股票 {} 的数据", stock_code),
None => println!("❌ 未找到股票 {} 的数据", stock_code),
}
}
Ok(())
}
async fn handle_config(config: &AppConfig, subcommand: crate::cli::ConfigSubcommand) -> Result<()> {
match subcommand {
crate::cli::ConfigSubcommand::Show => {
println!("📋 当前配置:");
println!("{}", "━".repeat(30));
println!("默认间隔: {} 秒", config.general.default_interval);
println!("启用通知: {}", config.general.enable_notifications);
println!("启用声音: {}", config.general.enable_sound);
println!("日志级别: {}", config.general.log_level);
println!("API超时: {} 秒", config.api.timeout);
println!("重试次数: {}", config.api.retry_count);
println!("彩色输出: {}", config.display.color_output);
println!("显示进度: {}", config.display.show_progress);
}
crate::cli::ConfigSubcommand::Reset => {
let default_config = AppConfig::default();
default_config.save()?;
println!("✅ 配置已重置为默认值");
}
crate::cli::ConfigSubcommand::Edit => {
println!("💡 请手动编辑配置文件:");
match AppConfig::get_config_path() {
Ok(path) => println!(" {}", path.display()),
Err(_) => println!(" 无法获取配置文件路径"),
}
}
}
Ok(())
}
async fn handle_test(config: &AppConfig, code: &str) -> Result<()> {
let stock_codes = crate::cli::parse_stock_codes(code);
if stock_codes.is_empty() {
return Err(crate::error::StockCalcError::ParseError("未提供有效的股票代码".to_string()));
}
let api = crate::api::StockApi::new(config.clone())?;
for (i, stock_code) in stock_codes.iter().enumerate() {
if i > 0 {
println!("{}", "━".repeat(50));
}
match api.fetch_stock_info(stock_code).await {
Ok(stock_info) => {
println!("✅ 股票代码 {} 可查", stock_code);
println!("{}", "━".repeat(50));
println!("📈 股票名称: {}", stock_info.name);
println!("🔢 股票代码: {}", stock_info.code);
println!("💰 当前价格: ¥{:.3}", stock_info.current_price);
println!("📊 涨跌金额: ¥{:.3}", stock_info.change_amount);
println!("📈 涨跌幅度: {:.2}%", stock_info.change_percent);
println!("📅 昨收价格: ¥{:.3}", stock_info.yesterday_close);
println!("🌅 开盘价格: ¥{:.3}", stock_info.open_price);
println!("📈 最高价格: ¥{:.3}", stock_info.high_price);
println!("📉 最低价格: ¥{:.3}", stock_info.low_price);
println!("📊 成交量: {} 手", stock_info.volume / 100);
println!("💵 成交额: ¥{:.2} 万", stock_info.turnover / 10000.0);
}
Err(_) => {
println!("❌ 请重新填写正确的股票代码: {}", stock_code);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_calculate_command() {
let config = AppConfig::default();
let result = handle_calculate(
&config,
"000001",
1000.0,
15.5,
5000.0,
2000.0,
false,
).await;
assert!(result.is_ok() || result.is_err());
}
}