"""Strategy example: Bollinger-Squeeze breakout with ATR-based stop. Enters long when the Bollinger Bandwidth has just printed a fresh 6-month low (the squeeze) and price closes above the upper band (the release). Exits when price closes below entry minus 2 * ATR(14), or when the upper band rolls back under the entry price. 0.1% fees per trade. Educational example. NOT a live trading recommendation. Run with:: python -m examples.python.strategy_bollinger_squeeze Uses the checked-in ``examples/data/btcusdt-1d.csv`` dataset because daily bars give an interpretable 6-month-low lookback (~180 bars). """ from __future__ import annotations import math from collections import deque from pathlib import Path import wickra as ta FEE = 0.001 BB_PERIOD = 20 BB_K = 2.0 ATR_PERIOD = 14 ATR_STOP_MULT = 2.0 SQUEEZE_LOOKBACK = 180 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-1d.csv" candles = load_candles(path) if len(candles) < SQUEEZE_LOOKBACK + BB_PERIOD: raise SystemExit( f"dataset has only {len(candles)} bars; need at least " f"{SQUEEZE_LOOKBACK + BB_PERIOD}" ) bb = ta.BollingerBands(BB_PERIOD, BB_K) atr = ta.ATR(ATR_PERIOD) bw_window: deque[float] = deque(maxlen=SQUEEZE_LOOKBACK) in_position = False entry_price = 0.0 stop_level = 0.0 closed_trades: list[float] = [] equity = 1.0 equity_curve: list[float] = [] for c in candles: bb_out = bb.update(c["close"]) atr_val = atr.update(c) price = c["close"] mtm = equity * (price / entry_price) if in_position else equity equity_curve.append(mtm) if bb_out is None or atr_val is None: continue upper = bb_out[0] if isinstance(bb_out, tuple) else bb_out.upper middle = bb_out[1] if isinstance(bb_out, tuple) else bb_out.middle lower = bb_out[2] if isinstance(bb_out, tuple) else bb_out.lower bandwidth = (upper - lower) / middle if abs(middle) > 1e-12 else float("nan") if math.isnan(bandwidth): continue bw_window.append(bandwidth) if len(bw_window) < SQUEEZE_LOOKBACK: continue min_bw = min(bw_window) if in_position: stop_hit = price < stop_level upper_collapse = upper < entry_price if stop_hit or upper_collapse: trade_ret = price / entry_price - 1.0 closed_trades.append(trade_ret) equity *= (1.0 + trade_ret) * (1.0 - FEE) in_position = False else: is_new_low = abs(bandwidth - min_bw) < 1e-12 breakout = price > upper if is_new_low and breakout: entry_price = price stop_level = price - ATR_STOP_MULT * atr_val equity *= 1.0 - FEE in_position = True 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( "Bollinger Squeeze Breakout (1d, BTCUSDT)", candles[0]["close"], candles[-1]["close"], len(candles), closed_trades, equity, equity_curve, ) if __name__ == "__main__": main()