Your First Trading Bot in ~30 Lines of Python
This is a complete, runnable strategy: fetch ten years of SPY daily data, compute two moving averages, generate signals where they cross, backtest with realistic transaction costs, and read the output. The point is not that this bot wins — it underperforms a simple buy-and-hold position by a wide margin — but that you can measure exactly how it behaves before risking anything.
"""AlgoDrill — your first trading bot in ~30 lines.
A complete, runnable strategy: fetch data → compute signals → backtest →
read the stats. Backtest only — this script never touches a broker.
Versions: vectorbt 1.0.0 · yfinance 1.4.1 · pandas 2.3.3 (run 2026-06-05)
Install: pip install vectorbt yfinance
"""
import vectorbt as vbt
# 1. Data — ten years of SPY daily closes (fixed range = reproducible)
price = vbt.YFData.download(
"SPY", start="2015-01-01", end="2024-12-31"
).get("Close")
# 2. Signals — 10-day fast SMA crossing a 50-day slow SMA
fast = vbt.MA.run(price, 10)
slow = vbt.MA.run(price, 50)
entries = fast.ma_crossed_above(slow) # golden cross -> buy
exits = fast.ma_crossed_below(slow) # death cross -> sell
# 3. Backtest — $10,000 start, 5 bps fees per side (cost realism matters),
# freq="d" so Sharpe annualizes daily bars correctly
pf = vbt.Portfolio.from_signals(
price, entries, exits, init_cash=10_000, fees=0.0005, freq="d"
)
# 4. The numbers that matter
print(f"Total Return {pf.total_return():>8.1%}")
print(f"Sharpe Ratio {pf.sharpe_ratio():>8.2f}")
print(f"Max Drawdown {pf.max_drawdown():>8.1%}")
print(f"Trades {pf.trades.count():>5}")
print(f"Win Rate {pf.trades.win_rate():>8.1%}")Line-by-Line Walkthrough
Lines 1–8: Docstring and import
The docstring pins the library versions and run date. This is not decoration — it is a reproducibility contract. yfinance serves dividend- and split-adjusted historical closes, and adjustments change slightly as corporate actions are processed. Pinning versions lets anyone re-run the script and know whether differences from the recorded output are drift (normal) or a data-source change (investigate). The single import vectorbt as vbt brings in vectorbt, which wraps yfinance, pandas, and numba automatically.
Lines 11–14: Data
vbt.YFData.download fetches SPY daily OHLCV from Yahoo Finance and returns a vectorbt Data object. .get("Close") extracts the closing-price series as a pandas Series. The fixed date range — 2015-01-01 to 2024-12-31 — is intentional: floating endpoints like “last 10 years” make backtests irreproducible as time passes. The closing prices are dividend- and split-adjusted (Yahoo Finance default), which is correct for backtesting: unadjusted prices create phantom return discontinuities at dividend ex-dates. For more on adjusted vs unadjusted closes, see the Fetch Market Data walkthrough.
Lines 16–20: Signals
vbt.MA.run(price, 10) computes a simple 10-day moving average. The golden cross (fast.ma_crossed_above(slow)) fires when the 10-day MA rises above the 50-day MA — a classic trend-entry signal indicating short-term momentum is stronger than medium-term. The death cross (fast.ma_crossed_below(slow)) is the symmetric exit. Both return boolean Series that vectorbt uses as entry and exit masks.
The choice of 10 and 50 is arbitrary. The parameter sensitivity section in the SMA Backtest in Depth walkthrough shows how the return changes from +106.6% to +134.4% by switching to 5/20 — same data, same method, different windows. That range is the overfitting risk in a single sentence.
Lines 22–26: Backtest
vbt.Portfolio.from_signals simulates trading the signal series against the price series. Key parameters:
- init_cash=10_000: the portfolio starts with $10,000. All return metrics are proportional, so this is cosmetic.
- fees=0.0005: 5 basis points (0.05%) per side. At $10 per $100k traded, this is below retail commission rates but above zero — it removes the fantasy of costless trading. See the fee sensitivity analysis in the SMA walkthrough: at 20 bps, return drops from +106.6% to +89.1%.
- freq="d": tells vectorbt the bars are daily. Without this, Sharpe Ratio cannot be annualized. Never omit it for daily data.
Lines 28–34: Output
The five printed metrics are the minimum viable dashboard for any systematic strategy backtest:
Total Return 106.6%
Sharpe Ratio 0.87
Max Drawdown -15.1%
Trades 30
Win Rate 46.7%
Reading the Numbers Honestly
The strategy returned +106.6% over 2015–2024. A simple buy-and-hold position in SPY returned +240.8% over the same window. The strategy underperformed by 134 percentage points — roughly the difference between doubling and tripling your money.
That is not a reason to give up. It is the whole point of the exercise. This simple bot does one thing the raw SPY position does not: it reduces maximum drawdown from roughly −34% (SPY’s 2020 peak-to-trough) to −15.1%. You earn less, but you lose less at the worst moment — and that trade-off is real and quantifiable before you risk any capital. Whether that trade-off is worth it is a personal risk-tolerance question, not a technical one.
The Sharpe Ratio of 0.87 (vs 0.95 for buy-and-hold) confirms the return-per-unit-of-volatility is slightly inferior. The 46.7% win rate across 30 trades means more individual trades lost than won — typical for trend-following. The winners were large enough to overcome the losers, producing the positive return. This is the mechanical reason momentum strategies work when they work: positive skew more than compensates for a sub-50% win rate.
Before reading further into momentum or SMA mechanics, two pages are essential background:
- Backtesting Pitfalls — look-ahead bias, survivorship bias, overfitting, and why this backtest already has one structural problem (in-sample optimization of the 10/50 windows).
- Kelly Criterion — given the 46.7% win rate and payoff ratio, how much capital should each trade risk? The answer is a fraction, never a large position.
Stack Setup
Install dependencies into a virtual environment (one-time; takes ~2 minutes due to numba compilation on first import):
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install vectorbt yfinance # installs pandas + numba automatically
python first_trading_bot.py
The script prints the five statistics above and exits. No broker connection, no authentication, no persistent state. Everything runs client-side from historical data.
Frequently Asked Questions
- Does this simple SMA bot beat buy and hold?
- No. SPY 10/50 SMA crossover with 5 bps fees returned +106.6% over 2015-2024, versus +240.8% for buy-and-hold over the same window (vectorbt 1.0.0, run 2026-06-05). The strategy's Sharpe ratio was 0.87 vs buy-and-hold's 0.95. It did reduce max drawdown from -34% (B&H approximate peak-to-trough in 2020) to -15.1%, so it carries roughly half the drawdown risk at roughly half the return. For many quant traders, that drawdown reduction is the reason to explore systematic strategies further rather than a reason to abandon them.
- How do I install vectorbt?
- pip install vectorbt installs the open-source version (Apache-2.0 + Commons Clause, v1.0.0 as of April 2026). The Commons Clause means you cannot sell vectorbt itself as a service, but internal research and personal trading are fully permitted. vectorbt requires Python 3.8+ and installs numpy, pandas, and numba as dependencies. On some systems numba compilation on first run can take 30-60 seconds; this is normal. A paid vectorbt PRO version exists with additional features and official support — the open-source version is sufficient for all walkthroughs on this page.
- Why does the backtest use freq='d'?
- vectorbt needs the bar frequency to annualize statistics correctly. Sharpe Ratio is computed as mean daily return / std daily return, then multiplied by sqrt(252) to convert to annualized terms (252 trading days per year). Without freq='d', vectorbt cannot infer the annualization factor and either raises an error or returns a non-annualized Sharpe. For weekly bars you would pass freq='w'; for monthly bars, freq='m'. Always set freq explicitly to avoid silent misannualization.
- What does a 46.7% win rate mean for an SMA strategy?
- The 10/50 SMA crossover produced 30 trades over 2015-2024, of which 46.7% were profitable (roughly 14 winners, 16 losers). This is typical for trend-following strategies: most individual trades lose small amounts, but the winning trades catch larger moves. The strategy's positive total return despite a sub-50% win rate is possible because the average winning trade is larger than the average losing trade (positive payoff ratio). This is the fundamental mechanics of momentum strategies: low win rate, high payoff ratio.
See how fees, frequency, and parameter choices change every result — and why that spread is the overfitting risk.
SMA Backtest in Depth → All Code Walkthroughs →