Your First Backtest in 30 Lines of Python
Most backtesting tutorials skip the hard part: realistic transaction costs. You build a strategy that looks great on paper, then watch it bleed money in production because spread, slippage, and market impact weren't modeled.
cobweb-py gives you a full execution simulator behind a simple API. In this post, we'll go from zero to a friction-aware backtest in about 30 lines.
Prerequisites
pip install cobweb-py yfinance plotly
That's it. No API key required—the free tier is open.
Step 1: Get the Data
We'll use yfinance to grab 5 years of SPY daily bars, then normalize timestamps for the API.
import yfinance as yf from cobweb_py import CobwebSim, BacktestConfig, fix_timestamps, print_signal from cobweb_py.plots import save_equity_plot, save_metrics_table df = yf.download("SPY", start="2020-01-01", end="2024-12-31") df.columns = df.columns.get_level_values(0) # flatten MultiIndex (yfinance quirk) df = df.reset_index().rename(columns={"Date": "timestamp"}) rows = df[["timestamp","Open","High","Low","Close","Volume"]].to_dict("records") data = fix_timestamps(rows)
fix_timestamps() normalizes any date format (including DD/MM/YYYY) into the ISO format the API expects. It also drops bad rows and sorts chronologically.
Step 2: Build a Signal
We'll use a classic momentum rule: go long when the close is above the 50-day simple moving average, stay flat otherwise.
close = df["Close"].values sma50 = df["Close"].rolling(50).mean().values signals = [1.0 if c > s else 0.0 for c, s in zip(close, sma50)] signals[:50] = [0.0] * 50 # flat during warmup
Signals are just a list of floats: 1.0 = fully long, 0.0 = flat, -1.0 = fully short. One value per bar, same length as your data.
Step 3: Run the Backtest
sim = CobwebSim("https://web-production-83f3e.up.railway.app") bt = sim.backtest( data, signals=signals, config=BacktestConfig(exec_horizon="swing", initial_cash=100_000), )
Behind the scenes, every trade goes through the execution simulator:
- Signals are shifted by 1 bar (signal at close, trade at next open)
- Fills include half-spread, base slippage, and sqrt market impact
- Each fill is capped at 8% of bar volume (swing preset)
- Unfilled quantity carries forward as a pending order
Step 4: Check Results
print(f"Return: {bt['metrics']['total_return']:.2%}") print(f"Sharpe: {bt['metrics']['sharpe_ann']:.2f}") print(f"Max DD: {bt['metrics']['max_drawdown']:.2%}") print(f"Trades: {bt['metrics']['trades']}") print_signal(bt)
Step 5: Save Charts
save_equity_plot(bt, out_html="equity.html") save_metrics_table(bt, out_html="metrics.html") print("Done — open equity.html in your browser.")
You'll get an interactive Plotly chart showing the equity curve and position sizes over time, plus a styled metrics table with Sharpe, Sortino, max drawdown, VaR, and the current signal.
Full Code
Here's everything in one block you can copy-paste:
import yfinance as yf from cobweb_py import CobwebSim, BacktestConfig, fix_timestamps, print_signal from cobweb_py.plots import save_equity_plot, save_metrics_table # 1. Grab SPY data df = yf.download("SPY", start="2020-01-01", end="2024-12-31") df.columns = df.columns.get_level_values(0) df = df.reset_index().rename(columns={"Date": "timestamp"}) rows = df[["timestamp","Open","High","Low","Close","Volume"]].to_dict("records") data = fix_timestamps(rows) # 2. Connect (free, no key needed) sim = CobwebSim("https://web-production-83f3e.up.railway.app") # 3. Simple momentum: long when price > 50-day SMA close = df["Close"].values sma50 = df["Close"].rolling(50).mean().values signals = [1.0 if c > s else 0.0 for c, s in zip(close, sma50)] signals[:50] = [0.0] * 50 # 4. Backtest with realistic friction bt = sim.backtest(data, signals=signals, config=BacktestConfig(exec_horizon="swing", initial_cash=100_000)) # 5. Results print(f"Return: {bt['metrics']['total_return']:.2%}") print(f"Sharpe: {bt['metrics']['sharpe_ann']:.2f}") print(f"Max DD: {bt['metrics']['max_drawdown']:.2%}") print(f"Trades: {bt['metrics']['trades']}") print_signal(bt) save_equity_plot(bt, out_html="equity.html") save_metrics_table(bt, out_html="metrics.html")
What's Next
- Try different tickers:
"AAPL","QQQ","BTC-USD" - Use the scoring engine to build composite signals from RSI, MACD, and Bollinger features
- Add a benchmark with
benchmark=to get rolling alpha/beta plots - Use the Pipeline class to iterate on weights without re-fetching data
- Explore all 27 plot types for deeper analysis