diff --git a/examples/README.md b/examples/README.md index 8440141a..5477a933 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,6 +11,7 @@ The Rust examples live in the `wickra-examples` workspace member crate. | Example | What it does | Run | | --- | --- | --- | +| `streaming.rs` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `cargo run -p wickra-examples --bin streaming` | | `backtest.rs` | Compute a basket of indicators over an OHLCV CSV and print a summary. | `cargo run -p wickra-examples --bin backtest -- ` | | `fetch_btcusdt.rs` | Download real BTCUSDT klines from the Binance REST API into `examples/data/`. | `cargo run -p wickra-examples --bin fetch_btcusdt` | | `live_binance.rs` | Stream live Binance klines through an indicator over a resilient WebSocket. | `cargo run -p wickra-examples --bin live_binance` | @@ -19,6 +20,7 @@ The Rust examples live in the `wickra-examples` workspace member crate. | Example | What it does | Run | | --- | --- | --- | +| `streaming.py` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `python -m examples.python.streaming` | | `backtest.py` | Basket of indicators over an OHLCV CSV. | `python -m examples.python.backtest ` | | `live_trading.py` | Live Binance feed → RSI / MACD / Bollinger → signals. | `python -m examples.python.live_trading --symbol BTCUSDT --interval 1m` | | `multi_timeframe.py` | Resample a 1-minute CSV to coarser timeframes and compare. | `python -m examples.python.multi_timeframe <1m.csv>` | diff --git a/examples/python/streaming.py b/examples/python/streaming.py new file mode 100644 index 00000000..46988a7d --- /dev/null +++ b/examples/python/streaming.py @@ -0,0 +1,101 @@ +"""Streaming indicators with the Wickra Python binding. + +Feeds a synthetic price series through several indicators tick by tick — the +same O(1)-per-update model a live trading bot would use — and prints a status +line whenever every indicator has warmed up. The Python counterpart of +``examples/node/streaming.js`` and ``examples/rust/src/bin/streaming.rs``. + +Run with:: + + python -m examples.python.streaming +""" + +from __future__ import annotations + +import argparse +import math + +import wickra as ta + + +def make_series(n: int) -> list[float]: + """Deterministic synthetic series: slow trend + two oscillations + tiny noise. + + The seeded linear-congruential generator matches the Node sibling example + so a side-by-side run produces visibly comparable streams. + """ + seed = 1234567 + prices: list[float] = [] + for t in range(n): + seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF + rand = seed / 0x7FFFFFFF + price = ( + 100.0 + + t * 0.05 + + math.sin(t * 0.07) * 8.0 + + math.cos(t * 0.21) * 3.0 + + (rand - 0.5) + ) + prices.append(price) + return prices + + +def fmt(value: float | None) -> str: + if value is None: + return " -- " + if isinstance(value, float) and math.isnan(value): + return " -- " + return f"{value:7.2f}" + + +def main() -> int: + parser = argparse.ArgumentParser( + description=__doc__.splitlines()[0] if __doc__ else None, + ) + parser.add_argument( + "--ticks", + type=int, + default=120, + help="number of synthetic price ticks to stream (default: 120)", + ) + args = parser.parse_args() + if args.ticks <= 0: + parser.error("--ticks must be positive") + + print(f"Wickra {ta.__version__} — streaming indicator demo (Python)\n") + + sma = ta.SMA(20) + ema = ta.EMA(20) + rsi = ta.RSI(14) + macd = ta.MACD(12, 26, 9) + + prices = make_series(args.ticks) + signals = 0 + for t, price in enumerate(prices): + sma_v = sma.update(price) + ema_v = ema.update(price) + rsi_v = rsi.update(price) + macd_v = macd.update(price) # (macd, signal, histogram) or None + + # Only act once every indicator has produced a value. + if sma_v is None or ema_v is None or rsi_v is None or macd_v is None: + continue + + _macd_line, _signal, histogram = macd_v + overbought = rsi_v > 70 and histogram < 0 + oversold = rsi_v < 30 and histogram > 0 + tag = "SELL?" if overbought else "BUY? " if oversold else " " + if overbought or oversold: + signals += 1 + + print( + f"t={t:>3} price={fmt(price)} sma={fmt(sma_v)} ema={fmt(ema_v)} " + f"rsi={fmt(rsi_v)} macd_hist={fmt(histogram)} {tag}" + ) + + print(f"\nDone — {signals} candidate signal(s) over {len(prices)} ticks.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/rust/src/bin/streaming.rs b/examples/rust/src/bin/streaming.rs new file mode 100644 index 00000000..3e101ca0 --- /dev/null +++ b/examples/rust/src/bin/streaming.rs @@ -0,0 +1,109 @@ +//! Streaming indicators with the Wickra Rust crate. +//! +//! Feeds a synthetic price series through several indicators tick by tick — +//! the same O(1)-per-update model a live trading bot would use — and prints +//! a status line whenever every indicator has warmed up. The Rust +//! counterpart of `examples/python/streaming.py` and +//! `examples/node/streaming.js`. +//! +//! Build with: +//! ```text +//! cargo run --release -p wickra-examples --bin streaming +//! ``` + +use std::env; + +use wickra::{Ema, Indicator, MacdIndicator, Rsi, Sma}; + +const DEFAULT_TICKS: usize = 120; + +/// Deterministic synthetic series matching the Node sibling example's +/// seeded LCG, so a side-by-side run produces visibly comparable streams. +fn make_series(n: usize) -> Vec { + let mut seed: u64 = 1_234_567; + (0..n) + .map(|t| { + seed = (seed.wrapping_mul(1_103_515_245).wrapping_add(12_345)) & 0x7FFF_FFFF; + let rand = seed as f64 / 0x7FFF_FFFF_u64 as f64; + let tf = t as f64; + 100.0 + tf * 0.05 + (tf * 0.07).sin() * 8.0 + (tf * 0.21).cos() * 3.0 + (rand - 0.5) + }) + .collect() +} + +fn fmt(v: Option) -> String { + match v { + Some(x) if x.is_finite() => format!("{x:7.2}"), + _ => " -- ".to_string(), + } +} + +fn parse_ticks() -> Result> { + let mut args = env::args().skip(1); + match args.next().as_deref() { + None => Ok(DEFAULT_TICKS), + Some("--ticks") => match args.next() { + Some(n) => n + .parse::() + .map_err(|e| format!("--ticks: {e}").into()), + None => Err("--ticks requires a value".into()), + }, + Some(other) => Err(format!("unexpected argument: {other}").into()), + } +} + +fn main() -> Result<(), Box> { + let ticks = parse_ticks()?; + if ticks == 0 { + return Err("--ticks must be positive".into()); + } + + println!("Wickra streaming indicator demo (Rust)\n"); + + let mut sma = Sma::new(20)?; + let mut ema = Ema::new(20)?; + let mut rsi = Rsi::new(14)?; + let mut macd = MacdIndicator::new(12, 26, 9)?; + + let prices = make_series(ticks); + let mut signals = 0usize; + for (t, &price) in prices.iter().enumerate() { + let sma_v = sma.update(price); + let ema_v = ema.update(price); + let rsi_v = rsi.update(price); + let macd_v = macd.update(price); + + // Only act once every indicator has produced a value. + let (Some(sv), Some(ev), Some(rv), Some(m)) = (sma_v, ema_v, rsi_v, macd_v) else { + continue; + }; + + let overbought = rv > 70.0 && m.histogram < 0.0; + let oversold = rv < 30.0 && m.histogram > 0.0; + let tag = if overbought { + "SELL?" + } else if oversold { + "BUY? " + } else { + " " + }; + if overbought || oversold { + signals += 1; + } + + println!( + "t={t:>3} price={} sma={} ema={} rsi={} macd_hist={} {tag}", + fmt(Some(price)), + fmt(Some(sv)), + fmt(Some(ev)), + fmt(Some(rv)), + fmt(Some(m.histogram)), + ); + } + + println!( + "\nDone — {signals} candidate signal(s) over {} ticks.", + prices.len() + ); + Ok(()) +}