summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 24f4ccc7be9f5e213b2dba34d238a72392523272 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 <input.csv> [-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<PathBuf>,
    rsi: usize,
    kd: usize,
    swing: f64,
}

fn parse_args() -> Result<Args, String> {
    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<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);

    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,
    });
    // "</" would end the <script> element early.
    let json = payload.to_string().replace("</", "<\\/");
    let html = TEMPLATE.replace("/*__DATA__*/null", &json);

    let out = args.output.unwrap_or_else(|| args.input.with_extension("html"));
    std::fs::write(&out, html).map_err(|e| format!("{}: {e}", out.display()))?;
    Ok(out)
}

fn main() -> ExitCode {
    match run() {
        Ok(out) => {
            println!("wrote {}", out.display());
            ExitCode::SUCCESS
        }
        Err(e) => {
            if !e.is_empty() {
                eprintln!("error: {e}");
            }
            eprintln!("{USAGE}");
            ExitCode::FAILURE
        }
    }
}