diff options
| author | JSDurand <mmemmew@gmail.com> | 2026-10-12 18:41:33 +0800 |
|---|---|---|
| committer | JSDurand <mmemmew@gmail.com> | 2026-10-12 18:41:33 +0800 |
| commit | 5c00355c2ccd94195bdb90e1c1e6f51acbcdd7a5 (patch) | |
| tree | d9e10c99a2f20de29cfa281275160b4194d175f2 /src/main.rs | |
| parent | 8c93a80302cee7ac73b9d4f2e90e47278164663f (diff) | |
added monthly and weekly analyses
* src/main.rs:
* src/resample.rs:
* src/signals.rs:
* web/template.html (Stock RSI / KD):
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 61 |
1 files changed, 42 insertions, 19 deletions
diff --git a/src/main.rs b/src/main.rs index 8ca9df5..f48b35a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ mod data; mod indicators; +mod resample; mod signals; +use resample::Interval; 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 <input.csv> [-o out.html] [--rsi 14] [--kd 9] [--atr 2.5 | --swing PCT] [--smooth N]"; +const USAGE: &str = "usage: stock <input.csv> [-o out.html] [--rsi 14] [--kd 9] [--atr 2.5 | --swing PCT] [--smooth N] [--interval day|week|month]"; // Swing detection for divergences: ZigZag with a reversal threshold of k*ATR(14) (--atr, default 2.5) // or a fixed percentage (--swing). --smooth N locates swings on an N-bar EMA, then snaps them to real highs/lows. @@ -19,12 +21,13 @@ struct Args { atr_k: f64, swing: Option<f64>, smooth: usize, + interval: Interval, } fn parse_args() -> Result<Args, String> { let mut it = std::env::args().skip(1); let (mut input, mut output, mut rsi, mut kd) = (None, None, 14, 9); - let (mut atr_k, mut swing, mut smooth) = (2.5, None, 0); + let (mut atr_k, mut swing, mut smooth, mut interval) = (2.5, None, 0, Interval::Day); while let Some(a) = it.next() { let mut val = |name: &str| it.next().ok_or(format!("{name} needs a value")); match a.as_str() { @@ -34,6 +37,10 @@ fn parse_args() -> Result<Args, String> { "--swing" => swing = Some(val("--swing")?.parse().map_err(|e| format!("--swing: {e}"))?), "--atr" => atr_k = val("--atr")?.parse().map_err(|e| format!("--atr: {e}"))?, "--smooth" => smooth = val("--smooth")?.parse().map_err(|e| format!("--smooth: {e}"))?, + "--interval" => { + let v = val("--interval")?; + interval = Interval::parse(&v).ok_or(format!("--interval: expected day, week or month, got '{v}'"))?; + } "-h" | "--help" => return Err(String::new()), _ if a.starts_with('-') => return Err(format!("unknown option {a}")), _ => input = Some(PathBuf::from(a)), @@ -42,39 +49,55 @@ fn parse_args() -> Result<Args, String> { if rsi == 0 || kd == 0 || atr_k <= 0.0 || swing.is_some_and(|p| !(p > 0.0 && p < 100.0)) { return Err("periods must be positive, --atr > 0, --swing within (0, 100)".into()); } - Ok(Args { input: input.ok_or("missing input.csv")?, output, rsi, kd, atr_k, swing, smooth }) + Ok(Args { input: input.ok_or("missing input.csv")?, output, rsi, kd, atr_k, swing, smooth, interval }) } -fn run() -> Result<PathBuf, String> { - 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); - +/// Indicators, signals and divergences for one bar interval; also returns a stats text. +fn analyse(candles: &[data::Candle], interval: Interval, args: &Args) -> (serde_json::Value, String) { + let rsi = indicators::rsi(candles, args.rsi); + let (k, d) = indicators::kd(candles, args.kd); let thr = match args.swing { Some(p) => signals::Threshold::Percent(p), - None => signals::Threshold::Atr { k: args.atr_k, atr: indicators::atr(&candles, 14) }, + None => signals::Threshold::Atr { k: args.atr_k, atr: indicators::atr(candles, 14) }, }; let swings = if args.smooth > 0 { - let found = signals::zigzag(&indicators::ema_candles(&candles, args.smooth), &thr); - signals::snap_swings(&candles, &found, args.smooth) + let found = signals::zigzag(&indicators::ema_candles(candles, args.smooth), &thr); + signals::snap_swings(candles, &found, args.smooth) } else { - signals::zigzag(&candles, &thr) + signals::zigzag(candles, &thr) }; - let mut divs = signals::divergences(&candles, &swings, &thr, &rsi, "RSI", &signals::Filters::rsi()); - divs.extend(signals::divergences(&candles, &swings, &thr, &k, "K", &signals::Filters::k())); + let mut divs = signals::divergences(candles, &swings, &thr, &rsi, "RSI", &signals::Filters::rsi()); + divs.extend(signals::divergences(candles, &swings, &thr, &k, "K", &signals::Filters::k())); divs.sort_by_key(|d| d.confirmed); - print!("{}", signals::forward_stats(&candles, &divs, &[5, 10, 20])); + let stats = format!("[{}] {} bars\n{}", interval.name(), candles.len(), 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, + "partial": resample::last_bar_partial(candles, interval), + }); + (payload, stats) +} + +fn run() -> Result<PathBuf, String> { + let args = parse_args()?; + let daily = data::load_csv(&args.input)?; + + let mut intervals = serde_json::Map::new(); + for interval in Interval::ALL { + let (payload, stats) = analyse(&resample::resample(&daily, interval), interval, &args); + print!("{stats}"); + intervals.insert(interval.name().into(), payload); + } + 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, + "initial": args.interval.name(), + "intervals": intervals, }); // "</" would end the <script> element early. let json = payload.to_string().replace("</", "<\\/"); |
