Files
wickra/examples/python/strategy_rsi_mean_reversion.py
T
kingchenc 677ea37402 examples: migrate to the native data layer (drop ws/coder-websocket/jackson/jsonlite) (#316)
Stacked on #315 (the native Binance REST fetcher). Retarget to `main` once #315 merges.

Migrates the runnable examples off third-party data-I/O packages onto Wickra's
native data layer (`CandleReader`, `Resampler`, `BinanceFeed`, `fetch_*klines`).

## Third-party packages removed (the zero-dep selling point)
- **Node**: `ws` (live feed → BinanceFeed) — dropped from package.json + lockfile
- **Go**: `github.com/coder/websocket` — dropped from go.mod / go.sum (`go mod tidy`)
- **Java**: `jackson-databind` (live feed + REST fetch) — dropped from pom.xml
- **R**: `jsonlite` + `websocket` + `later` — dropped from the README notes

Each language's CSV loading now goes through `CandleReader`, manual resampling
through `Resampler`, the live feed through `BinanceFeed`, and (Java/R) the REST
download through the native fetcher.

## Verification
Ran the offline examples per language against the bundled data — backtest and
multi_timeframe produce identical output across Python / Node / Go / Java / R
(e.g. ATR(14) last 345.1010; 1h→5m resamples to 240 bars, →15m to 80 bars).

C# / C / WASM (stdlib-only, no third-party deps to remove) follow in this branch.

Note: the streaming `strategy_*` examples have pre-existing candle-indicator
runtime bugs (CI only syntax-smokes them); the CSV migration preserves their
shape and leaves those bugs for a separate fix.
2026-06-17 01:49:11 +02:00

144 lines
4.4 KiB
Python

"""Strategy example: RSI mean-reversion on hourly BTCUSDT data.
Goes long when RSI(14) crosses below 30 (oversold), exits when RSI
crosses above 70 (overbought). Position is binary (full-in / full-out),
fees are 0.1% per trade (Binance maker tier), no stop-loss.
Educational example. NOT a recommended trading strategy in real markets.
The point is to show how Wickra streaming indicators wire up into a
complete signal -> fill -> PnL -> equity loop in a single file.
Run with::
python -m examples.python.strategy_rsi_mean_reversion
Uses the checked-in ``examples/data/btcusdt-1h.csv`` dataset.
"""
from __future__ import annotations
import math
from pathlib import Path
import wickra as ta
FEE = 0.001
RSI_PERIOD = 14
OVERSOLD = 30.0
OVERBOUGHT = 70.0
def load_candles(path: Path) -> list[dict[str, float]]:
# Native CandleReader: validates the header, tolerates a UTF-8 BOM and field
# whitespace, and raises ValueError on a malformed row. No third-party CSV.
candles = ta.CandleReader(path.read_text(encoding="utf-8")).read()
# CandleReader yields (open, high, low, close, volume, timestamp) tuples.
return [
{"open": o, "high": h, "low": l, "close": c, "volume": v}
for o, h, l, c, v, _ts in candles
]
def print_summary(
name: str,
first_price: float,
last_price: float,
bars: int,
closed_trades: list[float],
final_equity: float,
equity_curve: list[float],
) -> None:
buy_hold = last_price / first_price
strat_return = final_equity - 1.0
bh_return = buy_hold - 1.0
wins = sum(1 for r in closed_trades if r > 0)
losses = sum(1 for r in closed_trades if r < 0)
best = max(closed_trades) if closed_trades else 0.0
worst = min(closed_trades) if closed_trades else 0.0
n = len(closed_trades)
mean_ret = sum(closed_trades) / n if n else 0.0
var_ret = (
sum((r - mean_ret) ** 2 for r in closed_trades) / (n - 1) if n > 1 else 0.0
)
sharpe = mean_ret / math.sqrt(var_ret) if var_ret > 0 else 0.0
peak = equity_curve[0] if equity_curve else 1.0
max_dd = 0.0
for eq in equity_curve:
if eq > peak:
peak = eq
dd = (peak - eq) / peak
if dd > max_dd:
max_dd = dd
print(f"=== {name} ===")
print(f"Bars: {bars}")
print(f"Trades: {n} (W{wins} / L{losses})")
print(f"Strategy return: {strat_return * 100:+.2f}%")
print(f"Buy & Hold return: {bh_return * 100:+.2f}%")
print(f"Excess over BH: {(strat_return - bh_return) * 100:+.2f}%")
print(f"Max drawdown: {max_dd * 100:.2f}%")
print(
f"Per-trade Sharpe: {sharpe:.2f} "
f"(mean {mean_ret:+.4f}, stddev {math.sqrt(var_ret):.4f})"
)
print(f"Best / worst trade: {best * 100:+.2f}% / {worst * 100:+.2f}%")
print()
print(
"NOTE: Educational example — fees, slippage, funding costs and tax "
"effects are simplified or omitted. Past performance is not "
"indicative of future results."
)
def main() -> None:
path = Path(__file__).resolve().parents[1] / "data" / "btcusdt-1h.csv"
candles = load_candles(path)
if len(candles) < RSI_PERIOD * 4:
raise SystemExit(f"dataset too small: {len(candles)}")
rsi = ta.RSI(RSI_PERIOD)
in_position = False
entry_price = 0.0
closed_trades: list[float] = []
equity = 1.0
equity_curve: list[float] = []
for c in candles:
rsi_val = rsi.update(c["close"])
price = c["close"]
mtm = equity * (price / entry_price) if in_position else equity
equity_curve.append(mtm)
if rsi_val is None:
continue
if not in_position and rsi_val < OVERSOLD:
entry_price = price
equity *= 1.0 - FEE
in_position = True
elif in_position and rsi_val > OVERBOUGHT:
trade_ret = price / entry_price - 1.0
closed_trades.append(trade_ret)
equity *= (1.0 + trade_ret) * (1.0 - FEE)
in_position = False
if in_position:
last_price = candles[-1]["close"]
trade_ret = last_price / entry_price - 1.0
closed_trades.append(trade_ret)
equity *= (1.0 + trade_ret) * (1.0 - FEE)
print_summary(
"RSI Mean-Reversion (1h, BTCUSDT)",
candles[0]["close"],
candles[-1]["close"],
len(candles),
closed_trades,
equity,
equity_curve,
)
if __name__ == "__main__":
main()