Welcome to the MA Crossover Backtesting Project 👋
This guide assumes you already have uv installed and all dependencies are defined in pyproject.toml.
git clone https://github.com/priyanshupatwari/ma-backtesting.git
cd ma-backtestingUse uv to automatically create and activate the project’s virtual environment:
uv venv
.venv\Scripts\activate # Windows
# or
source .venv/bin/activate # macOS/LinuxAlternatively, run commands directly inside the environment:
uv run python main.pyIf needed, sync the dependencies listed in pyproject.toml:
uv syncThis ensures your environment matches the project’s dependency configuration exactly.
ma_crossover_backtest/
│
├── data/ # Raw or downloaded market data
├── notebooks/ # Jupyter experiments
├── src/ # Core code modules
│ ├── data_loader.py
│ ├── indicators.py
│ ├── backtester.py
│ ├── analyzer.py
│ └── visualizer.py
├── tests/ # Unit tests
├── pyproject.toml
└── README.md
Run the dashboard:
streamlit run app/streamlit_app.py Run your main script:
uv run python main.pyOr start Jupyter Lab (if installed):
uv run jupyter labThen select the project’s kernel (ma_crossover_backtest).
Run unit tests:
uv run pytest| Action | Command |
|---|---|
| Update dependencies | uv sync |
| Export requirements.txt | uv export --format requirements.txt |
| Clean cache | uv cache clean |
| Remove environment | rm -rf .venv |
- Always activate the virtual environment before running scripts.
- Keep your code in
src/and data indata/. - Avoid committing large data files or
.envfiles (use.gitignore).
- Core Question: Is a Moving Average (MA) Crossover strategy more effective than a simple Buy-and-Hold approach?
- Project Overview: We built an interactive dashboard to analyze 80 backtests — covering 10 stocks, 4 time intervals, and 2 strategies — to answer this question.
This page allows a one-on-one comparison between the two strategies.
-
Sidebar Filters: Select any stock (e.g.,
AAPL) and any interval (e.g.,1d). -
Winner Title: Instantly displays which strategy achieved the higher final equity.
-
Report Card: Provides a side-by-side comparison of key metrics — CAGR, Max Drawdown, and Sharpe Ratio.
-
Equity Curve: Shows how each portfolio’s value evolved over time.
-
Drill-Down Section ("The Proof"):
- Displays a candlestick price chart with volume.
- Dynamic MAs: The moving averages adjust automatically to the parameters used in that specific backtest (e.g.,
10/40for weekly,50/200for daily). - Buy/Sell Signals: Marked by green (buy) and red (sell) triangles at the exact points where the algorithm traded.
This page provides the big-picture analysis across all 80 backtests.
-
Executive Summary: Highlights the main takeaway (e.g., “MA Crossover outperformed Buy-and-Hold in 87.5% of tests”) and identifies the best-performing setup.
-
Comparison Charts: Visual summaries showing:
- Average CAGR
- Average Sharpe Ratio (risk-adjusted performance)
- Average Max Drawdown (overall risk)
-
Risk–Return Scatter Plot (Key Visualization):
- Each of the 80 backtests is plotted.
- Y-axis: Return (higher = better)
- X-axis: Risk (left = safer)
- Ideal Zone: Top-left quadrant (high return, low risk)
- Color Gradient: Green = strong Sharpe ratio, Red = weak.
-
Key Insights (Auto-Generated Section):
- Computed dynamically from all results.
- Provides human-readable insights, such as the best-performing interval (
1d) and top 3 stocks for the MA Crossover strategy.
import yfinance as yf
import pandas as pd
# Define parameters
symbol = "AAPL" # Example: Replace with your stock symbol
startdate = "2005-10-01"
enddate = "2025-10-01"
# Download historical data
data = yf.download(symbol, start=startdate, end=enddate)
# Flatten multi-level columns (if any)
data.columns = data.columns.get_level_values(0)
# Save as clean CSV file
data.to_csv(f"../data/{symbol}_2005-2025_1d.csv", index=True)
print("✓ Data downloaded and saved successfully!")
data.head()# Load the clean CSV
data = pd.read_csv(f"../data/{symbol}_2005-2025_1d.csv", index_col=0, parse_dates=True)
# Access columns directly
data['Close'] # ✓ Works
data.Close # ✓ Works too
# Plot example
data['Close'].plot(title=f"{symbol} Closing Prices (2005–2025)")# Load CSV with potential multi-level headers
data = pd.read_csv(f"../data/{symbol}_2005-2025_1d.csv",
index_col=0, parse_dates=True, header=[0, 1])
# Flatten headers (keep only top level: Close, High, Low, Open, Volume)
data.columns = data.columns.get_level_values(0)
# Save back the cleaned version
data.to_csv(f"../data/{symbol}_2005-2025_1d.csv", index=True)
print("✓ CSV cleaned and saved successfully!")
print(data.head())Date,Close,High,Low,Open,Volume
2005-10-03,1.634...,1.637...,1.611...,1.625...,507553200
2005-10-04,1.613...,1.661...,1.610...,1.649...,539459200
...