This repo contains a simple but realistic backtesting script for a moving-average (SMA) crossover strategy.
Key features:
- Uses yfinance to download historical daily data.
- Runs a grid search over short/long SMA windows on NVDA (train period: 2020–2022).
- Evaluates the selected parameters on a test period (2023–2025).
- Executes trades at the next day’s open to avoid lookahead bias.
- Includes transaction costs via a configurable fee rate.
- Computes key risk metrics:
- Total return
- CAGR
- Max drawdown
- Sharpe ratio
- Volatility
- Produces a trade-level log (date, side, price, size, fee, cash).
- Compares the strategy vs Buy & Hold.
- Applies the same SMA parameters to multiple tickers (AAPL, MSFT, AMD, META, TSLA) to check out-of-sample robustness.
- Plots price with SMAs and portfolio equity curve.
⚠️ Disclaimer: This project is for research and educational purposes only.
It is not financial advice and should not be used directly for live trading.
Take a very simple long-only moving-average crossover strategy,
make the backtest less fake (no lookahead, trading at next open, fees, train/test split),
and see how it behaves on NVDA and a few other large-cap names.
- Main asset:
NVDA(2020-01-01 → 2025-01-01) - Additional tickers for out-of-sample sanity check:
AAPL,MSFT,AMD,META,TSLA
- Source: yfinance
- Adjusted for splits/dividends (
auto_adjust=True) - Used columns:
Open,Close
We compute two simple moving averages on the Close price:
- Short SMA:
Short_SMA(e.g. 50-day) - Long SMA:
Long_SMA(e.g. 100-day)
Daily trading signal:
Signal = 1ifShort_SMA > Long_SMA→ longSignal = 0otherwise → flat
- Signals are generated on day t close.
- Orders are executed on day t+1 open:
- Avoids lookahead bias.
- Long-only, “all-in / all-out” behavior:
- If we are flat and
Signalflips from 0 → 1
→ buy as many shares as possible with available cash. - If we are long and
Signalflips from 1 → 0
→ sell the entire position.
- If we are flat and
- Per-trade fee rate:
fee_rate = 0.001(0.1% of trade notional) - Applied on both buys and sells.
At each day we compute:
portfolio_value = cash + shares * close_price
The equity curve is built from these daily portfolio values.
For any equity curve, the script computes:
Total Return
Total=VfinalV0−1
Total=V0Vfinal−1
CAGR (Annualized Growth)
Uses calendar days between first and last observations.
Max Drawdown
Based on rolling peak of the equity curve.
Annualized Volatility
Sharpe Ratio
Daily returns → annualized, risk-free rate assumed 0.
Number of Trades
Count of long-only entries.
Backtest window: 2020-01-01 → 2025-01-01
Train period: up to 2023-01-01 (yaklaşık 2020–2022)
Test period: from 2023-01-01 onward (yaklaşık 2023–2024)
Grid search over:
Short SMA candidates: [20, 50, 100]
Long SMA candidates: [100, 150, 200, 250]
with constraint short < long.
For each (short, long) pair:
Run the strategy on the train slice (NVDA 2020–2022).
Compute performance metrics.
Select configuration with highest Sharpe on train.
Top 5 configs on NVDA (Train 2020–2022): short long Sharpe CAGR Total MaxDD 50 100 1.43 63.12% 332.42% −34.27% 50 150 1.42 63.54% 335.77% −36.01% 50 200 1.37 63.14% 332.58% −36.29% 50 250 1.18 52.35% 252.46% −49.21% 100 150 0.88 30.39% 121.22% −44.33%
Chosen parameters:
short = 50
long = 100
These are then fixed and used for both the test period and all other tickers.
Initial capital: 100,000 USD
Fee rate: 0.1% per trade
Strategy (SMA 50/100):
Total Return: 332.42%
CAGR: 63.12%
Max Drawdown: −34.27%
Sharpe: 1.43
Volatility: 39.64%
Same SMA(50, 100), no re-tuning:
Total Return: 354.61%
CAGR: 113.77%
Max Drawdown: −27.05%
Sharpe: 1.88
Volatility: 46.13%
Initial capital: 100,000 USD Strategy – SMA(50, 100), long-only
Final Value: 3,500,651 USD
Total Return: 3,400.65%
CAGR: 103.72%
Max Drawdown: −34.27%
Sharpe: 1.84
Volatility: 44.03%
Number of trades: 5
Buy & Hold – NVDA
Final Value: 2,248,333 USD
Total Return: 2,148.33%
CAGR: 86.45%
Max Drawdown: −66.33%
Sharpe: 1.43
Volatility: 53.88%
Interpretation (kısaca):
Strategy still captures the huge NVDA trend.
Drawdown is roughly halved vs pure buy-and-hold.
Only 5 trades over 5 years → extremely low turnover.
The code records each trade with:
date
side (BUY / SELL)
price
shares
fee
cash_after (cash in account immediately after trade)
First few trades:
2020-03-17 – BUY
Price ≈ 5.00
Shares ≈ 19,985
Fee ≈ 100
Cash after trade ≈ −99 USD (practically fully invested)
2021-02-03 – SELL
Price ≈ 13.60
Shares ≈ 19,985
Fee ≈ 272
Cash after trade ≈ 271k
2021-02-09 – BUY
Price ≈ 14.30
Shares ≈ 18,976
Fee ≈ 271
Cash after trade ≈ −257
2022-02-23 – SELL
Price ≈ 23.76
Shares ≈ 18,976
Fee ≈ 451
Cash after trade ≈ 450k
2022-12-23 – BUY
Price ≈ 15.18
Shares ≈ 29,648
Fee ≈ 450
Cash after trade ≈ −445
And that’s basically it: 5 trades managing the full 5-year horizon.
To test how curve-fitted these parameters might be, we reuse SMA(50, 100) on several other large-cap names over the same period (2020–2025).
For each ticker:
Initial capital: 100,000 USD
Same execution & cost assumptions.
All metrics are strategy vs buy & hold. AAPL (Apple)
Strategy
Final: 219,717 USD
Total Return: 119.72%
CAGR: 17.06%
Max Drawdown: −24.43%
Sharpe: 0.81
Volatility: 22.49%
Buy & Hold
Final: 343,841 USD
Total Return: 243.84%
CAGR: 28.04%
Max Drawdown: −31.41%
Sharpe: 0.94
Volatility: 31.66%
On AAPL, buy-and-hold wins clearly on return; SMA helps a bit on drawdown/vol.
MSFT (Microsoft)
Strategy
Final: 258,626 USD
Total Return: 158.63%
CAGR: 20.95%
Max Drawdown: −28.73%
Sharpe: 0.98
Volatility: 21.96%
Buy & Hold
Final: 274,170 USD
Total Return: 174.17%
CAGR: 22.37%
Max Drawdown: −37.13%
Sharpe: 0.82
Volatility: 30.47%
On MSFT, returns are similar; SMA reduces drawdown & vol, slightly better Sharpe.
AMD
Strategy
Final: 303,254 USD
Total Return: 203.25%
CAGR: 24.86%
Max Drawdown: −39.70%
Sharpe: 0.78
Volatility: 37.43%
Buy & Hold
Final: 245,961 USD
Total Return: 145.96%
CAGR: 19.74%
Max Drawdown: −65.44%
Sharpe: 0.61
Volatility: 52.45%
On AMD, SMA both increases return and dramatically reduces drawdown.
META (Meta Platforms)
Strategy
Final: 417,144 USD
Total Return: 317.14%
CAGR: 33.09%
Max Drawdown: −21.32%
Sharpe: 1.19
Volatility: 27.00%
Buy & Hold
Final: 279,937 USD
Total Return: 179.94%
CAGR: 22.88%
Max Drawdown: −76.68%
Sharpe: 0.69
Volatility: 44.84%
META is where the trend-following setup really shines vs buy-and-hold.
TSLA (Tesla)
Strategy
Final: 1,358,754 USD
Total Return: 1,258.75%
CAGR: 68.57%
Max Drawdown: −55.69%
Sharpe: 1.27
Volatility: 51.49%
Buy & Hold
Final: 1,407,794 USD
Total Return: 1,307.79%
CAGR: 69.77%
Max Drawdown: −73.63%
Sharpe: 1.12
Volatility: 67.18%
On TSLA both are insane; SMA is slightly less explosive but a bit more controlled.
SMA(50, 100) is not globally optimal, but:
It’s not a total fluke either.
It often improves drawdown and volatility.
On strongly trending or crashy names (META, AMD, TSLA), it can be quite helpful.
Dependencies
yfinance
numpy
pandas
matplotlib
Installation
pip install yfinance numpy pandas matplotlib
How to Run
python quant_trading_simulation.py
The script will:
Download NVDA data.
Run SMA grid search on 2020–2022 (train).
Evaluate train / test / full period on NVDA.
Log all NVDA trades.
Run the same SMA(50, 100) on AAPL, MSFT, AMD, META, TSLA.
Print all metrics and show NVDA price + equity curve plots.
Limitations
Single-name, single-factor strategy (pure trend).
Grid search on a relatively short window → overfitting risk.
Simple fee model; no:
slippage,
borrowing costs,
market impact.
Only daily data; intraday volatility and overnight gaps are abstracted away.
No portfolio-level risk (no stop-loss / risk-parity / position sizing rules).
Possible Extensions
Try other trend filters: EMA, MACD, Donchian channels, breakout rules.
Add regime filters (e.g. trade only when volatility / VIX is in certain zones).
Move from single-name to portfolio construction:
dynamic weights,
volatility targeting,
correlation-aware position sizing.
Implement walk-forward or rolling re-optimization instead of a single fixed split.
Compare vs more traditional quant baselines (e.g. equal-weight buy-and-hold basket, risk-parity, etc.).