mod data; mod indicators; mod signals; use serde_json::json; use std::path::PathBuf; use std::process::ExitCode; const TEMPLATE: &str = include_str!("../web/template.html"); const USAGE: &str = "usage: stock [-o out.html] [--rsi 14] [--kd 9] [--swing 5]"; // --swing: minimum reversal in percent for a price swing (ZigZag) used by divergence detection struct Args { input: PathBuf, output: Option, rsi: usize, kd: usize, swing: f64, } fn parse_args() -> Result { let mut it = std::env::args().skip(1); let (mut input, mut output, mut rsi, mut kd, mut swing) = (None, None, 14, 9, 5.0); while let Some(a) = it.next() { let mut val = |name: &str| it.next().ok_or(format!("{name} needs a value")); match a.as_str() { "-o" => output = Some(PathBuf::from(val("-o")?)), "--rsi" => rsi = val("--rsi")?.parse().map_err(|e| format!("--rsi: {e}"))?, "--kd" => kd = val("--kd")?.parse().map_err(|e| format!("--kd: {e}"))?, "--swing" => swing = val("--swing")?.parse().map_err(|e| format!("--swing: {e}"))?, "-h" | "--help" => return Err(String::new()), _ if a.starts_with('-') => return Err(format!("unknown option {a}")), _ => input = Some(PathBuf::from(a)), } } if rsi == 0 || kd == 0 || !(swing > 0.0 && swing < 100.0) { return Err("periods must be positive and --swing within (0, 100)".into()); } Ok(Args { input: input.ok_or("missing input.csv")?, output, rsi, kd, swing }) } fn run() -> Result { let args = parse_args()?; let candles = data::load_csv(&args.input)?; let rsi = indicators::rsi(&candles, args.rsi); let (k, d) = indicators::kd(&candles, args.kd); let swings = signals::zigzag(&candles, args.swing); let mut divs = signals::divergences(&swings, &rsi, "RSI"); divs.extend(signals::divergences(&swings, &k, "K")); divs.sort_by_key(|d| d.confirmed); print!("{}", signals::forward_stats(&candles, &divs, &[5, 10, 20])); let payload = json!({ "name": args.input.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(), "rsiPeriod": args.rsi, "kdPeriod": args.kd, "candles": candles, "rsi": rsi, "k": k, "d": d, "crosses": signals::crosses(&k, &d), "divergences": divs, }); // " element early. let json = payload.to_string().replace(" ExitCode { match run() { Ok(out) => { println!("wrote {}", out.display()); ExitCode::SUCCESS } Err(e) => { if !e.is_empty() { eprintln!("error: {e}"); } eprintln!("{USAGE}"); ExitCode::FAILURE } } }