summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs87
1 files changed, 87 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..24f4ccc
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,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
+ }
+ }
+}