examples: add streaming demos for Python and Rust
Python and Rust both lacked a standalone "streaming indicators" example that mirrors examples/node/streaming.js — the quickstart docs cover the pattern, but a runnable file makes the parity visible across all four languages. * examples/python/streaming.py — argparse-driven synthetic streaming demo feeding SMA(20) / EMA(20) / RSI(14) / MACD(12,26,9), tagging BUY?/SELL? candidates when RSI extremes and MACD-histogram direction agree. * examples/rust/src/bin/streaming.rs — same demo as a wickra-examples binary, reusing the seeded LCG so its first 40 rows are bit-identical to the Python (and Node) sibling — a strong cross-language consistency signal verified by running both side by side. * examples/README.md gains a `streaming` row in the Rust and Python tables.
This commit is contained in:
@@ -11,6 +11,7 @@ The Rust examples live in the `wickra-examples` workspace member crate.
|
|||||||
|
|
||||||
| Example | What it does | Run |
|
| 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 -- <ohlcv.csv>` |
|
| `backtest.rs` | Compute a basket of indicators over an OHLCV CSV and print a summary. | `cargo run -p wickra-examples --bin backtest -- <ohlcv.csv>` |
|
||||||
| `fetch_btcusdt.rs` | Download real BTCUSDT klines from the Binance REST API into `examples/data/`. | `cargo run -p wickra-examples --bin fetch_btcusdt` |
|
| `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` |
|
| `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 |
|
| 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 <ohlcv.csv>` |
|
| `backtest.py` | Basket of indicators over an OHLCV CSV. | `python -m examples.python.backtest <ohlcv.csv>` |
|
||||||
| `live_trading.py` | Live Binance feed → RSI / MACD / Bollinger → signals. | `python -m examples.python.live_trading --symbol BTCUSDT --interval 1m` |
|
| `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>` |
|
| `multi_timeframe.py` | Resample a 1-minute CSV to coarser timeframes and compare. | `python -m examples.python.multi_timeframe <1m.csv>` |
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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<f64> {
|
||||||
|
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<f64>) -> String {
|
||||||
|
match v {
|
||||||
|
Some(x) if x.is_finite() => format!("{x:7.2}"),
|
||||||
|
_ => " -- ".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ticks() -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
|
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::<usize>()
|
||||||
|
.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<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user