QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
QUANT TRADER
PYTHON PROJECTS BIBLE
Build Every Tool a Quant Trader Needs — From Scratch
────────────────────────────────────
Fair Value Gaps • Candle Crossovers • Interactive Charts
Sharpe Ratio Dashboard • Volatility Heatmaps • Live Backtesting
Automated Trade Execution • Options Greeks • Risk Management
8 Complete Projects • 2025 Edition
HOW TO USE THIS GUIDE
Every project in this guide is self-contained: you can read and build it independently. Each one starts
from the absolute basics of its topic and builds to a fully working system. No prior Python knowledge
is assumed — the first chapter covers setup from zero. All code is production-quality and fully
commented. By the end you will have 8 real projects on your GitHub, a live paper-trading system,
and interactive dashboards you can run on your own computer.
© 2025 Quant Projects Guide 1
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
CHAPTE
R0
Setting Up Your Python Trading Environment
Installing Everything You Need
Before writing a single line of trading code you need a working Python environment. This chapter
walks you through the complete setup — from downloading Python to having a working Jupyter
notebook with every library pre-installed.
Step 1 — Install Anaconda (Python + All Science Libraries)
Anaconda is a free distribution of Python that comes pre-bundled with hundreds of scientific
libraries. It is the industry standard for quantitative research.
1. Go to [Link]/download and download Anaconda for your operating system
(Windows, Mac, or Linux).
2. Run the installer — accept all defaults. This installs Python 3.11+ and creates a base
environment.
3. Open 'Anaconda Navigator' from your applications — you will see a launcher with various
tools.
4. Click 'Launch' under JupyterLab — your browser opens with a notebook environment.
5. Create a new notebook: click the blue '+' button, then 'Python 3 (ipykernel)' under Notebook.
Step 2 — Install Trading-Specific Libraries
Open a Terminal (Mac/Linux) or Anaconda Prompt (Windows) and run these commands one at a
time:
# Install all libraries needed for this guide
pip install yfinance # Download stock market data from Yahoo Finance
pip install pandas # Data manipulation and time series
pip install numpy # Numerical computing
pip install plotly # Interactive charts (key for dashboards)
pip install matplotlib # Static charts
pip install ta # Technical analysis indicators (RSI, MACD, etc.)
pip install alpaca-trade-api # Paper trading execution via Alpaca
pip install scipy # Scientific computing
pip install dash # Build web dashboards in Python
pip install statsmodels # Statistical models
pip install jupyter # Notebook environment
# Verify installation by importing them
python -c "import yfinance, pandas, numpy, plotly, ta, alpaca_trade_api;
print('All OK')"
# Output:
# All OK
© 2025 Quant Projects Guide 2
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
Step 3 — Understanding the Notebook
A Jupyter notebook is a document that mixes text and executable code. Each 'cell' can be run
independently.
• Press Shift+Enter to run a cell and move to the next one
• Press Ctrl+Enter to run a cell and stay on it
• Press Escape then 'b' to add a new cell below
• Press Escape then 'dd' to delete a cell
• A cell with [ ] has not been run. A cell with [*] is running. A cell with [1] has been run.
Step 4 — Your First Trading Script (Verify Everything Works)
# Run this in a Jupyter cell to verify all libraries work
import yfinance as yf
import pandas as pd
import numpy as np
import plotly.graph_objects as go
# Download Apple data
data = [Link]('AAPL', start='2023-01-01', end='2024-01-01', progress=False)
# Print the last 5 rows
print([Link]())
print(f"\nTotal rows: {len(data)}")
# Output (approximate):
# --- Open High Low Close Volume
# 2023-12-25 193.06 193.07 191.73 193.58 28919300
# 2023-12-26 193.35 193.75 191.77 192.49 28963400
# ...
# Total rows: 251
If you prefer a dedicated code editor over JupyterLab, install VS Code
VS Code ([Link]) and the Python + Jupyter extensions. VS Code gives you
Alternative better code completion, debugging, and file management while supporting
notebooks natively.
Understanding Pandas DataFrames — Your Data Container
Every piece of market data you will work with lives in a Pandas DataFrame. Understanding
DataFrames is the most important programming concept in this guide.
© 2025 Quant Projects Guide 3
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
What Is a DataFrame?
A DataFrame is like a spreadsheet inside Python. It has rows (indexed by date for market data) and
columns (Open, High, Low, Close, Volume). You can slice, filter, compute, and plot it.
import yfinance as yf
import pandas as pd
# Download 1 year of SPY (S&P 500 ETF)
df = [Link]('SPY', start='2023-01-01', end='2024-01-01', progress=False)
# ── Basic exploration ──────────────────────────────────────────
print([Link]) # (251, 5) — 251 rows, 5 columns
print([Link]())# ['Open', 'High', 'Low', 'Close', 'Volume']
print([Link]) # Shows data types of each column
print([Link](3)) # First 3 rows
print([Link](3)) # Last 3 rows
print([Link]()) # Summary stats: mean, std, min, max, quartiles
# ── Accessing columns ──────────────────────────────────────────
close_prices = df['Close'] # A Series (single column)
ohlc = df[['Open','High','Low','Close']] # A DataFrame (multiple columns)
# ── Accessing rows by date ─────────────────────────────────────
row = [Link]['2023-06-15'] # Get specific date
range_ = [Link]['2023-06-01':'2023-06-30'] # Date range
# ── Creating new columns ───────────────────────────────────────
df['Return'] = df['Close'].pct_change() # Daily % return
df['Range'] = df['High'] - df['Low'] # Candle range
df['MA20'] = df['Close'].rolling(20).mean() # 20-day moving average
df['MA50'] = df['Close'].rolling(50).mean()
# ── Filtering rows ─────────────────────────────────────────────
positive_days = df[df['Return'] > 0] # Only positive days
big_range_days = df[df['Range'] > 5] # Large candle days
print(f"Positive days: {len(positive_days)} out of {len(df)}")
© 2025 Quant Projects Guide 4
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
CHAPTE
R1
Getting Market Data — Every Timeframe
Downloading Daily, Hourly, and 5-Minute Data
Different trading strategies need different timeframes. A fair value gap strategy may use 1-hour or
5-minute candles. A long-term trend strategy uses daily. Here is how to download any timeframe.
import yfinance as yf
import pandas as pd
# ── Valid interval values ──────────────────────────────────────
# '1m' = 1 minute (last 7 days only)
# '2m' = 2 minutes (last 60 days)
# '5m' = 5 minutes (last 60 days) ← most useful for intraday
# '15m' = 15 minutes (last 60 days)
# '30m' = 30 minutes (last 60 days)
# '60m' = 1 hour (last 730 days ~2 years)
# '1h' = 1 hour (last 730 days)
# '1d' = 1 day (any start date)
# '1wk' = 1 week (any start date)
# ── 5-minute data (last 30 days) ──────────────────────────────
df_5m = [Link]('SPY', period='30d', interval='5m', progress=False)
print(df_5m.head())
print(f'5m rows: {len(df_5m)}') # ~1560 rows for 30 days
# ── 1-hour data (last 2 years) ─────────────────────────────────
df_1h = [Link]('SPY', period='730d', interval='1h', progress=False)
print(f'1h rows: {len(df_1h)}') # ~3900 rows for 2 years
# ── Daily data (5 years) ───────────────────────────────────────
df_1d = [Link]('SPY', start='2019-01-01', end='2024-01-01', interval='1d',
progress=False)
print(f'Daily rows: {len(df_1d)}') # ~1260 rows for 5 years
# ── Multiple tickers at once ───────────────────────────────────
tickers = ['AAPL', 'MSFT', 'NVDA', 'SPY', 'QQQ']
multi = [Link](tickers, start='2023-01-01', end='2024-01-01',
progress=False)
close_all = multi['Close'] # DataFrame: each column is one ticker
print(close_all.tail())
# ── Save to CSV so you don't re-download every time ───────────
df_1d.to_csv('SPY_daily.csv')
df_loaded = pd.read_csv('SPY_daily.csv', index_col='Date', parse_dates=True)
Handling Missing Data and Market Hours
import pandas as pd
© 2025 Quant Projects Guide 5
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
import yfinance as yf
df = [Link]('SPY', period='30d', interval='5m', progress=False)
# ── Remove pre-market and after-hours data ─────────────────────
# US market hours: 9:30am - 4:00pm Eastern
[Link] = pd.to_datetime([Link]) # Ensure datetime index
# Filter to regular trading hours only
market_hours = df.between_time('09:30', '16:00')
print(f'All rows: {len(df)}, Market hours only: {len(market_hours)}')
# ── Check for and remove NaN values ───────────────────────────
print([Link]().sum()) # Count NaN in each column
df_clean = [Link]() # Remove any row with NaN
# ── Check for duplicate timestamps ────────────────────────────
dupes = [Link]().sum()
print(f'Duplicate timestamps: {dupes}')
df = df[~[Link](keep='first')] # Keep first occurrence
# ── Resample: convert 5m to 15m candles ──────────────────────
df_15m = [Link]('15min').agg({
'Open': 'first', # First 5m open = 15m open
'High': 'max', # Highest high of the 3 x 5m candles
'Low': 'min', # Lowest low
'Close': 'last', # Last 5m close = 15m close
'Volume':'sum', # Sum of all volume
}).dropna()
print(f'5m rows: {len(df)}, 15m rows: {len(df_15m)}')
© 2025 Quant Projects Guide 6
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
CHAPTE Interactive Charts with Plotly — Candlesticks,
R2 Indicators & Dashboards
Why Plotly Instead of Matplotlib
Matplotlib makes static images. Plotly makes interactive charts you can zoom, pan, hover over, and
add/remove traces — like TradingView inside Python. For quant work, interactive charts are
essential because you need to inspect specific candles and signals closely.
Your First Interactive Candlestick Chart
import yfinance as yf
import plotly.graph_objects as go
from [Link] import make_subplots
# ── Download data ─────────────────────────────────────────────
df = [Link]('SPY', period='60d', interval='5m', progress=False)
df = df.between_time('09:30', '16:00') # Market hours only
# ── Build candlestick chart ────────────────────────────────────
fig = [Link](data=[[Link](
x=[Link],
open=df['Open'],
high=df['High'],
low=df['Low'],
close=df['Close'],
name='SPY',
increasing_line_color='#00b894', # Green candles
decreasing_line_color='#d63031', # Red candles
)])
# ── Layout styling ────────────────────────────────────────────
fig.update_layout(
title='SPY — 5-Minute Chart',
xaxis_title='Date & Time',
yaxis_title='Price ($)',
xaxis_rangeslider_visible=False, # Hide the tiny range slider
template='plotly_dark', # Dark background
height=600,
font=dict(family='Arial', size=12),
)
[Link]() # Opens in browser — fully interactive!
# You can: zoom in/out, pan, hover for exact OHLC values
Adding Moving Averages to the Chart
import yfinance as yf
© 2025 Quant Projects Guide 7
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
import plotly.graph_objects as go
import pandas as pd
df = [Link]('SPY', period='90d', interval='1h', progress=False)
df = df.between_time('09:30','16:00')
# ── Calculate indicators ──────────────────────────────────────
df['EMA9'] = df['Close'].ewm(span=9, adjust=False).mean()
df['EMA21'] = df['Close'].ewm(span=21, adjust=False).mean()
df['SMA50'] = df['Close'].rolling(50).mean()
# ── Build chart with multiple traces ──────────────────────────
fig = [Link]()
# Candlesticks
fig.add_trace([Link](
x=[Link], open=df['Open'], high=df['High'],
low=df['Low'], close=df['Close'], name='SPY',
increasing_line_color='#00b894',
decreasing_line_color='#e17055',
))
# EMA 9 line
fig.add_trace([Link](
x=[Link], y=df['EMA9'],
line=dict(color='#fdcb6e', width=1.5),
name='EMA 9',
))
# EMA 21 line
fig.add_trace([Link](
x=[Link], y=df['EMA21'],
line=dict(color='#74b9ff', width=1.5),
name='EMA 21',
))
# SMA 50 line
fig.add_trace([Link](
x=[Link], y=df['SMA50'],
line=dict(color='#a29bfe', width=2, dash='dash'),
name='SMA 50',
))
fig.update_layout(
title='SPY Hourly — EMA 9/21 + SMA 50',
template='plotly_dark',
xaxis_rangeslider_visible=False,
height=650,
)
[Link]()
Multi-Panel Chart: Price + Volume + RSI
import yfinance as yf
import plotly.graph_objects as go
© 2025 Quant Projects Guide 8
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
from [Link] import make_subplots
import ta
df = [Link]('SPY', period='90d', interval='1d', progress=False)
# ── Calculate indicators ──────────────────────────────────────
df['RSI'] = [Link](df['Close'], window=14).rsi()
df['EMA20']= df['Close'].ewm(span=20,adjust=False).mean()
# ── Create 3-row subplot layout ───────────────────────────────
fig = make_subplots(
rows=3, cols=1,
shared_xaxes=True, # All charts share the same x-axis
vertical_spacing=0.04,
row_heights=[0.55, 0.20, 0.25], # Price gets 55%, Vol 20%, RSI 25%
subplot_titles=['Price & EMA20', 'Volume', 'RSI (14)'],
)
# ── Row 1: Candlesticks + EMA ─────────────────────────────────
fig.add_trace([Link](
x=[Link], open=df['Open'], high=df['High'],
low=df['Low'], close=df['Close'], name='SPY',
increasing_line_color='#00b894',
decreasing_line_color='#e17055',
), row=1, col=1)
fig.add_trace([Link](
x=[Link], y=df['EMA20'],
line=dict(color='#fdcb6e', width=2),
name='EMA 20',
), row=1, col=1)
# ── Row 2: Volume bars ─────────────────────────────────────────
colors = ['#00b894' if c>=o else '#e17055'
for c,o in zip(df['Close'], df['Open'])]
fig.add_trace([Link](
x=[Link], y=df['Volume'],
marker_color=colors, name='Volume',
), row=2, col=1)
# ── Row 3: RSI + overbought/oversold lines ────────────────────
fig.add_trace([Link](
x=[Link], y=df['RSI'],
line=dict(color='#a29bfe', width=2),
name='RSI',
), row=3, col=1)
# Horizontal reference lines on RSI panel
fig.add_hline(y=70, line_dash='dash', line_color='#e17055',
annotation_text='Overbought 70', row=3, col=1)
fig.add_hline(y=30, line_dash='dash', line_color='#00b894',
annotation_text='Oversold 30', row=3, col=1)
fig.update_layout(
title='SPY — Complete Technical Dashboard',
template='plotly_dark',
xaxis_rangeslider_visible=False,
height=800, showlegend=True,
© 2025 Quant Projects Guide 9
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
)
[Link]()
Saving Charts as HTML Files (Shareable Interactive Charts)
# Save as interactive HTML — anyone can open it in a browser
fig.write_html('spy_chart.html')
# Save as static image (requires kaleido: pip install kaleido)
fig.write_image('spy_chart.png', width=1400, height=800, scale=2)
# Save as PDF
fig.write_image('spy_chart.pdf')
© 2025 Quant Projects Guide 10
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
CHAPTE
R3
Technical Indicators from Scratch
Building Every Indicator You Need
Technical indicators are mathematical transformations of price and volume data. For a quant trader
they serve as raw features for trading signals. Here we build the most important ones from scratch
in Python, then also show the library shortcut.
Moving Averages — SMA and EMA
import pandas as pd
import numpy as np
import yfinance as yf
df = [Link]('SPY', period='1y', interval='1d', progress=False)
# ── Simple Moving Average (SMA) ────────────────────────────────
# SMA(n) = average of last n closing prices
# All periods weighted equally
def sma(series, period):
return [Link](window=period).mean()
df['SMA_20'] = sma(df['Close'], 20)
df['SMA_50'] = sma(df['Close'], 50)
df['SMA_200'] = sma(df['Close'], 200)
# ── Exponential Moving Average (EMA) ──────────────────────────
# EMA gives more weight to recent prices
# Multiplier = 2 / (period + 1)
# EMA today = (Close today * multiplier) + (EMA yesterday * (1 - multiplier))
def ema(series, period):
return [Link](span=period, adjust=False).mean()
df['EMA_9'] = ema(df['Close'], 9)
df['EMA_21'] = ema(df['Close'], 21)
df['EMA_50'] = ema(df['Close'], 50)
# ── Crossover signal ──────────────────────────────────────────
# 1 when EMA9 crosses above EMA21 (bullish)
# -1 when EMA9 crosses below EMA21 (bearish)
df['EMA_above'] = df['EMA_9'] > df['EMA_21']
df['EMA_cross_up'] = (df['EMA_above']) &
(~df['EMA_above'].shift(1).fillna(False))
df['EMA_cross_down'] = (~df['EMA_above']) &
(df['EMA_above'].shift(1).fillna(True))
crosses_up = df[df['EMA_cross_up']].[Link]()
crosses_down = df[df['EMA_cross_down']].[Link]()
print(f'Bullish crosses: {len(crosses_up)}')
print(f'Bearish crosses: {len(crosses_down)}')
© 2025 Quant Projects Guide 11
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
RSI — Relative Strength Index
import pandas as pd
import numpy as np
def calculate_rsi(close, period=14):
'''
RSI = 100 - (100 / (1 + RS))
RS = Average Gain / Average Loss over last 'period' bars
RSI > 70 = overbought (potential sell signal)
RSI < 30 = oversold (potential buy signal)
'''
delta = [Link]() # Price changes
gain = [Link](lower=0) # Only positive changes
loss = (-delta).clip(lower=0) # Only negative changes (as
positive)
# Initial averages (simple mean for first period)
avg_gain = [Link](window=period).mean()
avg_loss = [Link](window=period).mean()
# Subsequent averages (smoothed)
for i in range(period, len(close)):
avg_gain.iloc[i] = (avg_gain.iloc[i-1] * (period-1) + [Link][i]) /
period
avg_loss.iloc[i] = (avg_loss.iloc[i-1] * (period-1) + [Link][i]) /
period
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
# ── Using the ta library (faster, same result) ─────────────────
import ta
df['RSI'] = [Link](df['Close'], window=14).rsi()
# ── RSI signals ───────────────────────────────────────────────
df['RSI_oversold'] = df['RSI'] < 30 # Potential buy
df['RSI_overbought']= df['RSI'] > 70 # Potential sell
# RSI crossing out of oversold: RSI went from <30 to >30
df['RSI_cross_30'] = (df['RSI'] > 30) & (df['RSI'].shift(1) <= 30)
df['RSI_cross_70'] = (df['RSI'] < 70) & (df['RSI'].shift(1) >= 70)
MACD — Moving Average Convergence Divergence
import pandas as pd
import ta
# MACD = EMA(12) - EMA(26)
# Signal Line = EMA(9) of MACD
# Histogram = MACD - Signal (bars showing momentum)
© 2025 Quant Projects Guide 12
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
def calculate_macd(close, fast=12, slow=26, signal=9):
ema_fast = [Link](span=fast, adjust=False).mean()
ema_slow = [Link](span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line= macd_line.ewm(span=signal, adjust=False).mean()
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
df['MACD'], df['MACD_signal'], df['MACD_hist'] = calculate_macd(df['Close'])
# ── MACD crossover signal ────────────────────────────────────
# Bullish: MACD line crosses ABOVE signal line
# Bearish: MACD line crosses BELOW signal line
df['MACD_bull'] = (df['MACD'] > df['MACD_signal']) & (df['MACD'].shift(1) <=
df['MACD_signal'].shift(1))
df['MACD_bear'] = (df['MACD'] < df['MACD_signal']) & (df['MACD'].shift(1) >=
df['MACD_signal'].shift(1))
# ── Or use the ta library shortcut ───────────────────────────
macd_obj = [Link](df['Close'], window_fast=12, window_slow=26,
window_sign=9)
df['MACD'] = macd_obj.macd()
df['MACD_sig'] = macd_obj.macd_signal()
df['MACD_hist']= macd_obj.macd_diff()
Bollinger Bands
import pandas as pd
def bollinger_bands(close, period=20, std_dev=2):
'''
Middle Band = SMA(20)
Upper Band = SMA(20) + 2 * StdDev(20)
Lower Band = SMA(20) - 2 * StdDev(20)
Price at upper band = potentially overbought
Price at lower band = potentially oversold
Bandwidth narrowing = low volatility, breakout coming
'''
middle = [Link](period).mean()
std = [Link](period).std()
upper = middle + std_dev * std
lower = middle - std_dev * std
return upper, middle, lower
df['BB_upper'], df['BB_mid'], df['BB_lower'] = bollinger_bands(df['Close'])
# ── Bandwidth: how wide the bands are (low = low vol) ─────────
df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / df['BB_mid']
# ── %B: where price sits within the bands ─────────────────────
# %B = 0 means price is at lower band
# %B = 1 means price is at upper band
df['BB_pct'] = (df['Close'] - df['BB_lower']) / (df['BB_upper'] - df['BB_lower'])
# Touch of lower band (potential mean-reversion buy signal)
df['BB_lower_touch'] = df['Close'] <= df['BB_lower']
© 2025 Quant Projects Guide 13
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
df['BB_upper_touch'] = df['Close'] >= df['BB_upper']
ATR — Average True Range (Volatility Measure)
import pandas as pd
import numpy as np
def calculate_atr(high, low, close, period=14):
'''
True Range = max of:
1. High - Low (today's range)
2. |High - Previous Close| (gap up)
3. |Low - Previous Close| (gap down)
ATR = smoothed average of True Range
ATR is the most important volatility measure for position sizing
'''
prev_close = [Link](1)
tr1 = high - low
tr2 = (high - prev_close).abs()
tr3 = (low - prev_close).abs()
true_range = [Link]([tr1, tr2, tr3], axis=1).max(axis=1)
atr = true_range.ewm(span=period, adjust=False).mean()
return atr, true_range
df['ATR'], df['TR'] = calculate_atr(df['High'], df['Low'], df['Close'])
# ── ATR-based stop loss ──────────────────────────────────────
# A common approach: stop = entry - 1.5 * ATR
atr_multiple = 1.5
df['Stop_Long'] = df['Close'] - atr_multiple * df['ATR']
df['Stop_Short'] = df['Close'] + atr_multiple * df['ATR']
# ── ATR as % of price (normalised volatility) ────────────────
df['ATR_pct'] = (df['ATR'] / df['Close']) * 100
print(df[['Close','ATR','ATR_pct']].tail())
© 2025 Quant Projects Guide 14
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T1
Fair Value Gap (FVG) Detection & Trading System
What Is a Fair Value Gap?
A Fair Value Gap (FVG) is a three-candle pattern from Smart Money Concepts (ICT) trading. It
identifies areas where price moved so quickly that it left an 'imbalance' — a gap between candles
where price did not fully trade. These gaps act as magnets: price often returns to fill them before
continuing in the original direction.
FVG DEFINITION: Bullish FVG: Candle 1's HIGH is BELOW Candle 3's LOW. There is a gap
between C1 high and C3 low that candle 2's body skipped over on the way up. The gap zone =
[C1 high, C3 low]. Bearish FVG: Candle 1's LOW is ABOVE Candle 3's HIGH. There is a gap
between C3 high and C1 low that price skipped over on the way down. The gap zone = [C3
high, C1 low].
Step 1 — Detect All FVGs in Historical Data
import yfinance as yf
import pandas as pd
import numpy as np
def detect_fvg(df, min_gap_pct=0.05):
'''
Detect all Bullish and Bearish Fair Value Gaps.
min_gap_pct: minimum gap size as % of price (filter tiny gaps)
Returns: list of dicts with gap info
'''
gaps = []
for i in range(1, len(df) - 1): # Candle 2 index
c1 = [Link][i - 1] # Candle 1 (left)
c2 = [Link][i] # Candle 2 (middle — the impulse)
c3 = [Link][i + 1] # Candle 3 (right)
# ── Bullish FVG: gap between C1 high and C3 low ────────
if c3['Low'] > c1['High']: # Gap exists
gap_size = c3['Low'] - c1['High']
gap_pct = gap_size / c1['High'] * 100
if gap_pct >= min_gap_pct: # Filter tiny gaps
[Link]({
'type' : 'bullish',
'time_c2' : [Link][i], # Time of the impulse candle
'top' : c3['Low'], # Upper boundary of gap
'bottom' : c1['High'], # Lower boundary of gap
'midpoint' : (c3['Low'] + c1['High']) / 2,
'size_pct' : round(gap_pct, 3),
'filled' : False,
© 2025 Quant Projects Guide 15
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
'fill_time' : None,
})
# ── Bearish FVG: gap between C3 high and C1 low ────────
if c3['High'] < c1['Low']: # Gap exists
gap_size = c1['Low'] - c3['High']
gap_pct = gap_size / c1['Low'] * 100
if gap_pct >= min_gap_pct:
[Link]({
'type' : 'bearish',
'time_c2' : [Link][i],
'top' : c1['Low'], # Upper boundary of gap
'bottom' : c3['High'], # Lower boundary of gap
'midpoint' : (c1['Low'] + c3['High']) / 2,
'size_pct' : round(gap_pct, 3),
'filled' : False,
'fill_time' : None,
})
return gaps
# ── Test on SPY 5-minute data ─────────────────────────────────
df = [Link]('SPY', period='30d', interval='5m', progress=False)
df = df.between_time('09:30', '16:00').dropna()
gaps = detect_fvg(df, min_gap_pct=0.03)
print(f'Total FVGs found: {len(gaps)}')
bullish = [g for g in gaps if g['type']=='bullish']
bearish = [g for g in gaps if g['type']=='bearish']
print(f'Bullish FVGs: {len(bullish)}')
print(f'Bearish FVGs: {len(bearish)}')
# Print first 3
for g in gaps[:3]:
print(f" {g['type'].upper()} at {g['time_c2']} | ",
f"Zone: {g['bottom']:.2f} – {g['top']:.2f} | ",
f"Size: {g['size_pct']:.2f}%")
Step 2 — Track Which Gaps Are Filled
def mark_filled_gaps(df, gaps):
'''
After detecting gaps, scan forward to see which ones price returned to fill.
A gap is considered 'filled' when price trades back into the gap zone.
'''
for gap in gaps:
# Find the index of the gap's candle
gap_idx = [Link].get_loc(gap['time_c2'])
# Search all subsequent candles
for j in range(gap_idx + 1, len(df)):
candle = [Link][j]
© 2025 Quant Projects Guide 16
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
if gap['type'] == 'bullish':
# Bullish gap is filled when price dips back into the zone
# i.e., candle low reaches below the gap top (upper boundary)
if candle['Low'] <= gap['top']:
gap['filled'] = True
gap['fill_time'] = [Link][j]
break
elif gap['type'] == 'bearish':
# Bearish gap is filled when price rallies back into the zone
if candle['High'] >= gap['bottom']:
gap['filled'] = True
gap['fill_time'] = [Link][j]
break
return gaps
gaps = mark_filled_gaps(df, gaps)
filled = [g for g in gaps if g['filled']]
unfilled = [g for g in gaps if not g['filled']]
fill_rate = len(filled) / len(gaps) * 100 if gaps else 0
print(f'Total gaps: {len(gaps)}')
print(f'Filled gaps: {len(filled)} ({fill_rate:.1f}%)')
print(f'Unfilled gaps: {len(unfilled)}')
# ── Time to fill (in candles) ─────────────────────────────────
for g in filled:
start_idx = [Link].get_loc(g['time_c2'])
end_idx = [Link].get_loc(g['fill_time'])
g['candles_to_fill'] = end_idx - start_idx
if filled:
avg_fill = sum(g['candles_to_fill'] for g in filled) / len(filled)
print(f'Average candles to fill: {avg_fill:.1f}')
Step 3 — Visualise FVGs on the Interactive Chart
import plotly.graph_objects as go
def plot_fvg_chart(df, gaps, last_n_candles=200):
'''Plot candlestick chart with FVG zones highlighted as rectangles.'''
df_plot = [Link][-last_n_candles:]
fig = [Link]()
# ── Candlesticks ──────────────────────────────────────────
fig.add_trace([Link](
x=df_plot.index,
open=df_plot['Open'], high=df_plot['High'],
low=df_plot['Low'], close=df_plot['Close'],
name='Price',
increasing_line_color='#00b894',
decreasing_line_color='#e17055',
© 2025 Quant Projects Guide 17
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
))
# ── Draw FVG zones as shaded rectangles ───────────────────
for gap in gaps:
# Only draw gaps that appear in the visible range
if gap['time_c2'] < df_plot.index[0]:
continue
color = 'rgba(0,184,148,0.15)' if gap['type']=='bullish' else
'rgba(225,112,85,0.15)'
border= 'rgba(0,184,148,0.6)' if gap['type']=='bullish' else
'rgba(225,112,85,0.6)'
# Rectangle from gap creation to end of chart
x_end = df_plot.index[-1] if not gap['filled'] else gap['fill_time']
fig.add_shape(
type='rect',
x0=gap['time_c2'], x1=x_end,
y0=gap['bottom'], y1=gap['top'],
fillcolor=color,
line=dict(color=border, width=1, dash='dot'),
)
# Label the gap
fig.add_annotation(
x=gap['time_c2'],
y=gap['midpoint'],
text=f"{'Bull' if gap['type']=='bullish' else 'Bear'} FVG
{gap['size_pct']:.2f}%",
showarrow=False,
font=dict(size=9, color=border),
bgcolor='rgba(0,0,0,0.5)',
)
fig.update_layout(
title='Fair Value Gaps — SPY 5-min',
template='plotly_dark',
xaxis_rangeslider_visible=False,
height=700,
)
[Link]()
plot_fvg_chart(df, gaps)
Step 4 — The FVG Trading Strategy (Entry Logic)
import pandas as pd
import numpy as np
def fvg_strategy(df, gaps, risk_reward=2.0, atr_stop_mult=1.0):
'''
FVG Re-entry Strategy:
- When price returns to a BULLISH FVG zone from above → BUY
- When price returns to a BEARISH FVG zone from below → SELL SHORT
- Stop Loss: 1 ATR below entry (for longs)
© 2025 Quant Projects Guide 18
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
- Take Profit: Risk * risk_reward above entry
'''
trades = []
# Calculate ATR for stop sizing
df = [Link]()
prev_close = df['Close'].shift(1)
tr = [Link]([
df['High'] - df['Low'],
(df['High'] - prev_close).abs(),
(df['Low'] - prev_close).abs()
], axis=1).max(axis=1)
df['ATR'] = [Link](span=14, adjust=False).mean()
in_trade = False
current_trade = None
for i in range(1, len(df)):
candle = [Link][i]
prev = [Link][i - 1]
ts = [Link][i]
# ── Manage open trade ─────────────────────────────────
if in_trade:
t = current_trade
if t['side'] == 'long':
if candle['Low'] <= t['stop']: # Stop hit
t['exit_price'] = t['stop']
t['exit_time'] = ts
t['exit_type'] = 'stop'
t['pnl'] = t['exit_price'] - t['entry_price']
[Link](t)
in_trade = False
elif candle['High'] >= t['target']: # Target hit
t['exit_price'] = t['target']
t['exit_time'] = ts
t['exit_type'] = 'target'
t['pnl'] = t['exit_price'] - t['entry_price']
[Link](t)
in_trade = False
elif t['side'] == 'short':
if candle['High'] >= t['stop']:
t['exit_price'] = t['stop']
t['exit_time'] = ts
t['exit_type'] = 'stop'
t['pnl'] = t['entry_price'] - t['exit_price']
[Link](t)
in_trade = False
elif candle['Low'] <= t['target']:
t['exit_price'] = t['target']
t['exit_time'] = ts
t['exit_type'] = 'target'
t['pnl'] = t['entry_price'] - t['exit_price']
[Link](t)
in_trade = False
© 2025 Quant Projects Guide 19
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# ── Look for new entries ───────────────────────────────
if not in_trade:
atr = candle['ATR']
for gap in gaps:
if gap['filled'] or gap['time_c2'] >= ts:
continue # Skip already-filled or future gaps
if gap['type'] == 'bullish':
# Price enters the gap zone from above = buy
if candle['Low'] <= gap['top'] and candle['Close'] >=
gap['bottom']:
entry = gap['top'] # Enter at top of gap
stop = entry - atr_stop_mult * atr
risk = entry - stop
target = entry + risk * risk_reward
current_trade = {
'side' : 'long',
'entry_time' : ts,
'entry_price': entry,
'stop' : stop,
'target' : target,
'risk' : risk,
'gap_type' : 'bullish',
}
in_trade = True
gap['filled'] = True # Mark as used
break
elif gap['type'] == 'bearish':
# Price enters the gap zone from below = short
if candle['High'] >= gap['bottom'] and candle['Close'] <=
gap['top']:
entry = gap['bottom']
stop = entry + atr_stop_mult * atr
risk = stop - entry
target = entry - risk * risk_reward
current_trade = {
'side' : 'short',
'entry_time' : ts,
'entry_price': entry,
'stop' : stop,
'target' : target,
'risk' : risk,
'gap_type' : 'bearish',
}
in_trade = True
gap['filled'] = True
break
return [Link](trades)
# ── Run the strategy ──────────────────────────────────────────
results = fvg_strategy(df, gaps, risk_reward=2.0, atr_stop_mult=1.0)
print(f'Total trades: {len(results)}')
if len(results) > 0:
print(results[['side','entry_time','entry_price','exit_type','pnl']].head(10))
© 2025 Quant Projects Guide 20
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
© 2025 Quant Projects Guide 21
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T2
5-Minute Candle Crossover Trade Executor
The Strategy: EMA Cross on 5-Minute Candles
This project builds a complete system that monitors 5-minute candles and executes a trade the
moment a specific candle condition is met — in this case, when the EMA9 crosses above or below
the EMA21. The same structure can be adapted to any condition you want.
Step 1 — Define the Crossover Condition Precisely
import pandas as pd
import numpy as np
def generate_crossover_signals(df, fast_period=9, slow_period=21):
'''
Generate buy/sell signals when fast EMA crosses slow EMA.
Signal = +1 at the CLOSE of the crossover candle
Trade executes at the OPEN of the NEXT candle
This avoids look-ahead bias.
'''
df = [Link]()
# Calculate EMAs
df[f'EMA{fast_period}'] = df['Close'].ewm(span=fast_period,
adjust=False).mean()
df[f'EMA{slow_period}'] = df['Close'].ewm(span=slow_period,
adjust=False).mean()
# Is fast EMA above slow EMA?
df['fast_above'] = df[f'EMA{fast_period}'] > df[f'EMA{slow_period}']
# Crossover detection:
# Cross UP: fast was below (or equal) last bar, now above
# Cross DOWN: fast was above (or equal) last bar, now below
df['cross_up'] = df['fast_above'] &
~df['fast_above'].shift(1).fillna(False)
df['cross_down'] = ~df['fast_above'] & df['fast_above'].shift(1).fillna(True)
# Raw signal on this bar (we know this at close)
df['signal_raw'] = 0
[Link][df['cross_up'], 'signal_raw'] = 1 # Buy signal
[Link][df['cross_down'], 'signal_raw'] = -1 # Sell signal
# Actual position: shifted by 1 (execute on next open)
df['position'] = df['signal_raw'].shift(1).fillna(0)
return df
# ── Test ──────────────────────────────────────────────────────
import yfinance as yf
df = [Link]('SPY', period='30d', interval='5m', progress=False)
© 2025 Quant Projects Guide 22
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
df = df.between_time('09:30','16:00').dropna()
df = generate_crossover_signals(df)
# Show rows where a signal fired
signals = df[df['signal_raw'] != 0][['Close','EMA9','EMA21','signal_raw']]
print(f'Total signals: {len(signals)}')
print([Link](10))
Step 2 — Add Filters to Improve Signal Quality
def add_filters(df, rsi_period=14, atr_period=14, volume_lookback=20):
'''
Add filters to only take crossover signals that meet additional conditions:
1. RSI filter: only buy when RSI > 50 (in uptrend), sell when RSI < 50
2. Volume filter: only trade when volume is above average (confirming move)
3. ATR filter: avoid trading when ATR is very low (no volatility = no move)
'''
df = [Link]()
import ta
# ── RSI ────────────────────────────────────────────────────
df['RSI'] = [Link](df['Close'], window=rsi_period).rsi()
# ── Volume filter ──────────────────────────────────────────
df['vol_avg'] = df['Volume'].rolling(volume_lookback).mean()
df['high_vol'] = df['Volume'] > df['vol_avg']
# ── ATR ────────────────────────────────────────────────────
prev_close = df['Close'].shift(1)
tr = [Link]([
df['High'] - df['Low'],
(df['High'] - prev_close).abs(),
(df['Low'] - prev_close).abs()
], axis=1).max(axis=1)
df['ATR'] = [Link](span=atr_period, adjust=False).mean()
df['atr_pct'] = df['ATR'] / df['Close'] * 100
# ── Filtered signal ────────────────────────────────────────
# Buy only if: cross up AND RSI > 50 AND high volume
# Sell only if: cross down AND RSI < 50 AND high volume
df['signal_filtered'] = 0
buy_cond = (df['signal_raw'] == 1) & (df['RSI'] > 50) & (df['high_vol'])
sell_cond = (df['signal_raw'] == -1) & (df['RSI'] < 50) & (df['high_vol'])
[Link][buy_cond, 'signal_filtered'] = 1
[Link][sell_cond, 'signal_filtered'] = -1
print(f'Raw signals: {(df["signal_raw"] != 0).sum()}')
print(f'Filtered signals: {(df["signal_filtered"] != 0).sum()}')
return df
df = add_filters(df)
© 2025 Quant Projects Guide 23
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
Step 3 — Visualise Signals on the Chart
import plotly.graph_objects as go
from [Link] import make_subplots
def plot_crossover_signals(df, n=200):
d = [Link][-n:].copy()
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
row_heights=[0.7,0.3], vertical_spacing=0.04)
# Candles
fig.add_trace([Link](
x=[Link], open=d['Open'], high=d['High'],
low=d['Low'], close=d['Close'],
name='SPY',
increasing_line_color='#00b894',
decreasing_line_color='#e17055',
), row=1, col=1)
# EMA lines
fig.add_trace([Link](x=[Link], y=d['EMA9'],
line=dict(color='#fdcb6e',width=1.5), name='EMA9'), row=1, col=1)
fig.add_trace([Link](x=[Link], y=d['EMA21'],
line=dict(color='#74b9ff',width=1.5), name='EMA21'), row=1, col=1)
# Buy signals (green triangles pointing up)
buys = d[d['signal_filtered'] == 1]
fig.add_trace([Link](
x=[Link], y=buys['Low'] * 0.999,
mode='markers',
marker=dict(symbol='triangle-up', size=12, color='#00b894'),
name='Buy Signal',
), row=1, col=1)
# Sell signals (red triangles pointing down)
sells = d[d['signal_filtered'] == -1]
fig.add_trace([Link](
x=[Link], y=sells['High'] * 1.001,
mode='markers',
marker=dict(symbol='triangle-down', size=12, color='#e17055'),
name='Sell Signal',
), row=1, col=1)
# RSI
fig.add_trace([Link](x=[Link], y=d['RSI'],
line=dict(color='#a29bfe',width=1.5), name='RSI'), row=2, col=1)
fig.add_hline(y=50, line_dash='dash', line_color='white', row=2, col=1)
fig.update_layout(
title='5-min EMA Crossover Signals (Filtered)',
template='plotly_dark',
xaxis_rangeslider_visible=False,
height=750,
)
[Link]()
© 2025 Quant Projects Guide 24
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
plot_crossover_signals(df)
Step 4 — Condition Builder: Any Condition You Want
The following function is a reusable 'condition engine.' You describe your entry condition in code
and it applies it to any DataFrame. Here are several common conditions:
def check_conditions(df):
'''
A library of trading conditions.
Each returns a boolean Series: True where condition is met.
'''
conditions = {}
# ── PRICE CONDITIONS ──────────────────────────────────────
# Price closes above a specific level
conditions['above_500'] = df['Close'] > 500
# Price crosses ABOVE a 20-period SMA
sma20 = df['Close'].rolling(20).mean()
conditions['cross_above_sma20'] = (df['Close'] > sma20) &
(df['Close'].shift(1) <= [Link](1))
# Price makes a new 5-period high (breakout)
conditions['new_5bar_high'] = df['Close'] == df['Close'].rolling(5).max()
# Inside bar: today's range is inside yesterday's range
conditions['inside_bar'] = (
(df['High'] < df['High'].shift(1)) &
(df['Low'] > df['Low'].shift(1))
)
# ── INDICATOR CONDITIONS ──────────────────────────────────
import ta
rsi = [Link](df['Close'], window=14).rsi()
# RSI crosses up through 30 (exit oversold)
conditions['rsi_cross_30'] = (rsi > 30) & ([Link](1) <= 30)
# RSI between 40 and 60 (neutral zone — avoid)
conditions['rsi_neutral'] = (rsi >= 40) & (rsi <= 60)
macd = [Link](df['Close'])
conditions['macd_cross_up'] = (
([Link]() > macd.macd_signal()) &
([Link]().shift(1) <= macd.macd_signal().shift(1))
)
# ── CANDLE PATTERN CONDITIONS ─────────────────────────────
# Bullish engulfing: green candle that engulfs prior red candle
is_green = df['Close'] > df['Open']
is_red = df['Close'] < df['Open']
conditions['bullish_engulf'] = (
© 2025 Quant Projects Guide 25
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
is_green &
is_red.shift(1) &
(df['Open'] < df['Close'].shift(1)) & # Opens below prior close
(df['Close'] > df['Open'].shift(1)) # Closes above prior open
)
# Doji: open ≈ close (body less than 10% of total range)
body = (df['Close'] - df['Open']).abs()
range_= df['High'] - df['Low']
conditions['doji'] = (body / range_.replace(0, [Link])) < 0.1
# ── VOLUME CONDITIONS ─────────────────────────────────────
conditions['volume_spike'] = df['Volume'] > df['Volume'].rolling(20).mean() *
1.5
# ── COMBINATION ───────────────────────────────────────────
# Custom combo: RSI cross out of oversold + MACD cross up + high volume
conditions['high_prob_buy'] = (
conditions['rsi_cross_30'] &
conditions['macd_cross_up'] &
conditions['volume_spike']
)
return conditions
# Apply all conditions
conds = check_conditions(df)
for name, series in [Link]():
count = [Link]()
print(f'{name:25s}: {count} signals')
© 2025 Quant Projects Guide 26
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC Complete Backtesting Engine with Sharpe,
T3 Drawdown & Interactive Results
Building a Professional Backtester from Zero
A backtester simulates how a strategy would have performed historically. This project builds a
complete, reusable backtesting class that handles any strategy, calculates all key metrics, and
produces interactive performance dashboards.
The Backtester Class
import pandas as pd
import numpy as np
from typing import Callable
class Backtester:
'''
A complete backtesting engine.
Usage:
bt = Backtester(df, initial_capital=10000)
[Link](signal_series)
bt.print_stats()
[Link]()
'''
def __init__(self, df: [Link], initial_capital: float = 10_000,
commission: float = 0.0005, slippage: float = 0.0002):
'''
df : OHLCV DataFrame with DatetimeIndex
initial_capital: Starting portfolio value in dollars
commission : Per-trade cost as fraction of trade value (0.0005 =
0.05%)
slippage : Assumed fill slippage as fraction of price
'''
[Link] = [Link]()
[Link] = initial_capital
[Link] = commission
[Link] = slippage
[Link] = None
def run(self, signals: [Link]):
'''
signals: Series aligned with df, values: +1 (long), -1 (short), 0 (flat)
Signals must already be shifted (i.e. today's signal = tomorrow's
position)',
'''
df = [Link]()
df['signal'] = [Link]([Link]).fillna(0)
# ── Calculate returns ─────────────────────────────────
© 2025 Quant Projects Guide 27
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
df['mkt_ret'] = df['Close'].pct_change() # Market return
df['position'] = df['signal'].shift(1).fillna(0) # Yesterday signal =
today position
# Raw strategy return = position * market return
df['raw_ret'] = df['position'] * df['mkt_ret']
# ── Transaction costs ─────────────────────────────────
# A cost applies when position changes
df['trade'] = df['position'].diff().abs() # 0=no trade, non-
zero=trade
df['cost'] = df['trade'] * ([Link] + [Link])
df['strat_ret']= df['raw_ret'] - df['cost']
# ── Equity curves ────────────────────────────────────
df['equity'] = [Link] * (1 + df['strat_ret']).cumprod()
df['mkt_equity']= [Link] * (1 + df['mkt_ret']).cumprod()
# ── Drawdown ──────────────────────────────────────────
peak = df['equity'].cummax()
df['drawdown'] = (df['equity'] - peak) / peak
[Link] = df
return self
def get_stats(self) -> dict:
'''Calculate all performance statistics.'''
df = [Link]
rets = df['strat_ret'].dropna()
n_days_per_year = 252 if len(rets) > 100 else 252
# ── Returns ───────────────────────────────────────────
total_ret = df['equity'].iloc[-1] / [Link] - 1
n_years = len(rets) / n_days_per_year
ann_ret = (1 + total_ret) ** (1 / n_years) - 1 if n_years > 0 else 0
ann_vol = [Link]() * [Link](n_days_per_year)
# ── Risk-adjusted ─────────────────────────────────────
rf_daily = 0.05 / n_days_per_year # 5% risk-free rate
excess_ret = rets - rf_daily
sharpe = (excess_ret.mean() / [Link]()) *
[Link](n_days_per_year) if [Link]() > 0 else 0
sortino_denom= rets[rets < 0].std() * [Link](n_days_per_year)
sortino = (ann_ret - 0.05) / sortino_denom if sortino_denom > 0 else
0
# ── Drawdown ──────────────────────────────────────────
max_dd = df['drawdown'].min()
calmar = ann_ret / abs(max_dd) if max_dd != 0 else 0
# ── Trade stats ───────────────────────────────────────
trade_rets = rets[df['trade'].shift(1).fillna(0) != 0]
win_rate = (trade_rets > 0).mean() if len(trade_rets) > 0 else 0
profit_factor= trade_rets[trade_rets>0].sum() /
abs(trade_rets[trade_rets<0].sum()) \
if len(trade_rets[trade_rets<0]) > 0 else [Link]
n_trades = int(df['trade'].sum())
© 2025 Quant Projects Guide 28
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# ── Benchmark comparison ──────────────────────────────
mkt_total = df['mkt_equity'].iloc[-1] / [Link] - 1
mkt_ann = (1 + mkt_total) ** (1/n_years) - 1 if n_years > 0 else 0
mkt_sharpe = (df['mkt_ret'].mean() / df['mkt_ret'].std()) *
[Link](n_days_per_year)
return {
'Total Return' : f'{total_ret:.2%}',
'Ann. Return' : f'{ann_ret:.2%}',
'Ann. Volatility' : f'{ann_vol:.2%}',
'Sharpe Ratio' : f'{sharpe:.3f}',
'Sortino Ratio' : f'{sortino:.3f}',
'Calmar Ratio' : f'{calmar:.3f}',
'Max Drawdown' : f'{max_dd:.2%}',
'Win Rate' : f'{win_rate:.2%}',
'Profit Factor' : f'{profit_factor:.2f}',
'N Trades' : n_trades,
'Mkt Total Ret' : f'{mkt_total:.2%}',
'Mkt Ann. Return' : f'{mkt_ann:.2%}',
'Mkt Sharpe' : f'{mkt_sharpe:.3f}',
}
def print_stats(self):
stats = self.get_stats()
print('\n' + '='*45)
print(' BACKTEST PERFORMANCE REPORT')
print('='*45)
for k, v in [Link]():
print(f' {k:22s}: {v}')
print('='*45)
return self
Interactive Performance Dashboard
import plotly.graph_objects as go
from [Link] import make_subplots
import numpy as np
def plot_backtest(bt: Backtester, title='Strategy Performance'):
'''
Full interactive dashboard showing:
- Equity curve vs buy-and-hold
- Drawdown chart
- Rolling Sharpe ratio
- Monthly returns heatmap
'''
df = [Link]
stats = bt.get_stats()
# ── Calculate rolling Sharpe (63-day = ~3 months) ─────────
roll_window = 63
roll_sharpe = (df['strat_ret'].rolling(roll_window).mean() /
df['strat_ret'].rolling(roll_window).std()) * [Link](252)
fig = make_subplots(
© 2025 Quant Projects Guide 29
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
rows=3, cols=1,
shared_xaxes=True,
vertical_spacing=0.06,
row_heights=[0.50, 0.25, 0.25],
subplot_titles=[
f'Equity Curve | Sharpe: {stats["Sharpe Ratio"]} | Max DD:
{stats["Max Drawdown"]}',
'Drawdown (%)',
'Rolling 63-Day Sharpe Ratio',
],
)
# ── Row 1: Equity curves ──────────────────────────────────
fig.add_trace([Link](
x=[Link], y=df['equity'],
line=dict(color='#00b894', width=2.5),
name=f'Strategy ({stats["Ann. Return"]} ann)',
fill='tonexty' if False else None,
), row=1, col=1)
fig.add_trace([Link](
x=[Link], y=df['mkt_equity'],
line=dict(color='#74b9ff', width=1.5, dash='dash'),
name=f'Buy & Hold ({stats["Mkt Ann. Return"]} ann)',
), row=1, col=1)
# ── Row 2: Drawdown ───────────────────────────────────────
fig.add_trace([Link](
x=[Link], y=df['drawdown'] * 100,
line=dict(color='#e17055', width=1.5),
fill='tozeroy', fillcolor='rgba(225,112,85,0.2)',
name='Drawdown',
), row=2, col=1)
# ── Row 3: Rolling Sharpe ─────────────────────────────────
colors_sharpe = ['#00b894' if v >= 0 else '#e17055' for v in
roll_sharpe.fillna(0)]
fig.add_trace([Link](
x=[Link], y=roll_sharpe,
marker_color=colors_sharpe,
name='Rolling Sharpe',
), row=3, col=1)
fig.add_hline(y=0, line_dash='dash', line_color='white', row=3, col=1)
fig.add_hline(y=1, line_dash='dot', line_color='#fdcb6e', row=3, col=1)
fig.update_layout(
title=title,
template='plotly_dark',
height=850,
showlegend=True,
legend=dict(yanchor='top', y=0.99, xanchor='left', x=0.01),
)
[Link]()
# ── Run everything together ───────────────────────────────────
import yfinance as yf
df = [Link]('SPY', start='2020-01-01', end='2024-01-01', progress=False)
© 2025 Quant Projects Guide 30
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# Generate EMA crossover signals
ema9 = df['Close'].ewm(span=9, adjust=False).mean()
ema21 = df['Close'].ewm(span=21, adjust=False).mean()
fast_above = ema9 > ema21
signals = [Link](0, index=[Link])
signals[fast_above] = 1
signals[~fast_above] = -1
# Run backtest
bt = Backtester(df, initial_capital=10_000, commission=0.0005)
[Link](signals)
bt.print_stats()
plot_backtest(bt, 'SPY — EMA 9/21 Crossover Strategy')
Monthly Returns Heatmap
import plotly.graph_objects as go
import pandas as pd
import numpy as np
def plot_monthly_heatmap(bt: Backtester):
'''
Monthly returns heatmap: rows = years, columns = months.
Green = positive month, Red = negative month.',
'''
df = [Link]()
# Resample to monthly returns
monthly = df['strat_ret'].resample('ME').apply(
lambda x: (1 + x).prod() - 1
)
# Pivot into year x month matrix
monthly_df = [Link]({
'year' : [Link],
'month': [Link],
'ret' : [Link] * 100, # as percentage
})
pivot = monthly_df.pivot(index='year', columns='month', values='ret')
[Link] = ['Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec']
# ── Heatmap ──────────────────────────────────────────────
z_text = [[f'{v:.1f}%' if not [Link](v) else '' for v in row]
for row in [Link]]
fig = [Link](data=[Link](
z=[Link],
x=[Link](),
y=[str(y) for y in [Link]],
text=z_text,
texttemplate='%{text}',
colorscale=[
[0.0, '#c0392b'], # Dark red for worst
© 2025 Quant Projects Guide 31
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
[0.4, '#e17055'], # Light red
[0.5, '#dfe6e9'], # White/grey for zero
[0.6, '#00b894'], # Light green
[1.0, '#00695c'], # Dark green for best
],
zmid=0,
showscale=True,
colorbar=dict(title='Return (%)'),
))
fig.update_layout(
title='Monthly Returns Heatmap',
template='plotly_dark',
height=400,
xaxis=dict(side='top'),
)
[Link]()
plot_monthly_heatmap(bt)
© 2025 Quant Projects Guide 32
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T4
Live Sharpe Ratio & Volatility Interactive Dashboard
Building a Full Analytics Dashboard
This project creates a comprehensive, interactive dashboard showing Sharpe ratio, rolling volatility,
and return distribution — all updating from live data. This is the kind of tool you use daily to monitor
your strategy.
import yfinance as yf
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from [Link] import make_subplots
class StrategyDashboard:
'''Interactive analytics dashboard for any strategy.'''
def __init__(self, ticker='SPY', period='2y', interval='1d'):
[Link] = [Link](ticker, period=period, interval=interval,
progress=False)
[Link] = ticker
self._compute()
def _compute(self):
df = [Link]
df['Return'] = df['Close'].pct_change()
# ── Volatility metrics ────────────────────────────────
# Realised vol = rolling std of daily returns, annualised
df['Vol_20'] = df['Return'].rolling(20).std() * [Link](252) * 100
df['Vol_60'] = df['Return'].rolling(60).std() * [Link](252) * 100
df['Vol_252'] = df['Return'].rolling(252).std() * [Link](252) * 100
# ── Rolling Sharpe ratio ──────────────────────────────
# Annualised Sharpe over rolling windows
rf_daily = 0.05 / 252
excess = df['Return'] - rf_daily
df['Sharpe_21'] = ([Link](21).mean() /
df['Return'].rolling(21).std()) * [Link](252)
df['Sharpe_63'] = ([Link](63).mean() /
df['Return'].rolling(63).std()) * [Link](252)
df['Sharpe_252'] = ([Link](252).mean() /
df['Return'].rolling(252).std()) * [Link](252)
# ── Drawdown ──────────────────────────────────────────
equity = (1 + df['Return'].fillna(0)).cumprod()
df['Equity'] = equity
peak = [Link]()
df['DD'] = (equity - peak) / peak * 100
# ── Cumulative return ─────────────────────────────────
df['CumRet'] = (equity - 1) * 100
© 2025 Quant Projects Guide 33
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
[Link] = [Link](subset=['Vol_20','Sharpe_21'])
def plot(self):
df = [Link]
fig = make_subplots(
rows=4, cols=2,
shared_xaxes=False,
vertical_spacing=0.08,
horizontal_spacing=0.10,
subplot_titles=[
f'{[Link]} Price',
'Return Distribution',
'Rolling Volatility (%)',
'Rolling Sharpe Ratio',
'Drawdown (%)',
'Cumulative Return (%)',
'Volume',
'Volatility Regime (20d vs 252d)',
],
specs=[
[{'type':'xy'}, {'type':'xy'}],
[{'type':'xy'}, {'type':'xy'}],
[{'type':'xy'}, {'type':'xy'}],
[{'type':'xy'}, {'type':'xy'}],
],
)
# ── (1,1) Price ───────────────────────────────────────
fig.add_trace([Link](
x=[Link], y=df['Close'],
line=dict(color='#74b9ff',width=1.5), name='Close'), row=1, col=1)
# ── (1,2) Return Distribution ─────────────────────────
rets = df['Return'].dropna() * 100
fig.add_trace([Link](
x=rets, nbinsx=80,
marker_color='#a29bfe',
opacity=0.75, name='Returns'), row=1, col=2)
# Normal distribution overlay
x_norm = [Link]([Link](), [Link](), 200)
norm_y = (1/([Link]()*[Link](2*[Link]))) * [Link](-0.5*((x_norm-
[Link]())/[Link]())**2)
norm_y *= len(rets) * ([Link]()-[Link]()) / 80
fig.add_trace([Link](
x=x_norm, y=norm_y,
line=dict(color='#fdcb6e',width=2), name='Normal Fit'), row=1, col=2)
# ── (2,1) Rolling Volatility ──────────────────────────
fig.add_trace([Link](
x=[Link], y=df['Vol_20'],
line=dict(color='#fd79a8',width=1.5), name='Vol 20d'), row=2, col=1)
fig.add_trace([Link](
x=[Link], y=df['Vol_60'],
line=dict(color='#fdcb6e',width=1.5), name='Vol 60d'), row=2, col=1)
fig.add_trace([Link](
x=[Link], y=df['Vol_252'],
© 2025 Quant Projects Guide 34
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
line=dict(color='#a29bfe',width=2,dash='dash'), name='Vol 252d'),
row=2, col=1)
# ── (2,2) Rolling Sharpe ──────────────────────────────
for col_name, color, name in [('Sharpe_21','#fd79a8','Sharpe 1mo'),
('Sharpe_63','#74b9ff','Sharpe 3mo'),
('Sharpe_252','#00b894','Sharpe 1yr')]:
fig.add_trace([Link](
x=[Link], y=df[col_name],
line=dict(color=color,width=1.5), name=name), row=2, col=2)
fig.add_hline(y=0, line_dash='dash', line_color='white', row=2, col=2)
fig.add_hline(y=1, line_dash='dot', line_color='#fdcb6e', row=2, col=2)
fig.add_hline(y=2, line_dash='dot', line_color='#00b894', row=2, col=2)
# ── (3,1) Drawdown ────────────────────────────────────
fig.add_trace([Link](
x=[Link], y=df['DD'],
line=dict(color='#e17055',width=1.5),
fill='tozeroy', fillcolor='rgba(225,112,85,0.15)',
name='Drawdown'), row=3, col=1)
# ── (3,2) Cumulative Return ───────────────────────────
fig.add_trace([Link](
x=[Link], y=df['CumRet'],
line=dict(color='#00b894',width=2),
fill='tozeroy', fillcolor='rgba(0,184,148,0.1)',
name='Cum. Return'), row=3, col=2)
# ── (4,1) Volume ──────────────────────────────────────
vol_colors = ['#00b894' if r>=0 else '#e17055' for r in
df['Return'].fillna(0)]
fig.add_trace([Link](
x=[Link], y=df['Volume'],
marker_color=vol_colors, name='Volume'), row=4, col=1)
# ── (4,2) Volatility regime ───────────────────────────
fig.add_trace([Link](
x=df['Vol_252'], y=df['Vol_20'],
mode='markers',
marker=dict(color=range(len(df)), colorscale='Viridis', size=4,
opacity=0.6),
name='Vol Regime'), row=4, col=2)
fig.add_shape(type='line', x0=0, x1=80, y0=0, y1=80,
line=dict(color='white',dash='dash'), row=4, col=2)
# ── Layout ───────────────────────────────────────────
fig.update_layout(
title=f'{[Link]} — Complete Analytics Dashboard',
template='plotly_dark',
height=1200,
showlegend=True,
)
[Link]()
return fig
# ── Run it ───────────────────────────────────────────────────
dash = StrategyDashboard(ticker='SPY', period='2y', interval='1d')
[Link]()
© 2025 Quant Projects Guide 35
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# Try different tickers:
# StrategyDashboard('AAPL').plot()
# StrategyDashboard('QQQ', period='5y').plot()
© 2025 Quant Projects Guide 36
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T5
Options Greeks Calculator & Interactive Pricer
Building a Black-Scholes Options Pricer from Scratch
This project builds a complete options pricing and Greeks calculator. You can input any option and
instantly see its price and all Greeks — then visualise how they change as the underlying moves.
import numpy as np
from [Link] import norm
import pandas as pd
import plotly.graph_objects as go
from [Link] import make_subplots
class BlackScholes:
'''
Complete Black-Scholes options pricer with all Greeks.
S = Current stock price
K = Strike price
T = Time to expiry in YEARS (e.g. 30 days = 30/365 = 0.0822)
r = Risk-free rate (annualised, e.g. 0.05 = 5%)
sigma = Implied volatility (annualised, e.g. 0.20 = 20%)
'''
def __init__(self, S, K, T, r, sigma, option_type='call'):
self.S = S
self.K = K
self.T = max(T, 1e-6) # Prevent division by zero at expiry
self.r = r
[Link]= sigma
[Link] = option_type.lower()
# ── d1 and d2: the core inputs to the formula ────────
self.d1 = ([Link](S/K) + (r + 0.5*sigma**2)*self.T) / (sigma *
[Link](self.T))
self.d2 = self.d1 - sigma * [Link](self.T)
def price(self) -> float:
'''Option price.'''
if [Link] == 'call':
return (self.S * [Link](self.d1) -
self.K * [Link](-self.r*self.T) * [Link](self.d2))
else: # put
return (self.K * [Link](-self.r*self.T) * [Link](-self.d2) -
self.S * [Link](-self.d1))
def delta(self) -> float:
'''Rate of change of price vs underlying.'''
if [Link] == 'call':
return [Link](self.d1)
else:
© 2025 Quant Projects Guide 37
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
return [Link](self.d1) - 1
def gamma(self) -> float:
'''Rate of change of delta vs underlying (same for call and put).'''
return [Link](self.d1) / (self.S * [Link] * [Link](self.T))
def theta(self) -> float:
'''Daily time decay (divide annual by 365).'''
term1 = -(self.S * [Link](self.d1) * [Link]) / (2 *
[Link](self.T))
if [Link] == 'call':
theta_annual = term1 - self.r * self.K * [Link](-self.r*self.T) *
[Link](self.d2)
else:
theta_annual = term1 + self.r * self.K * [Link](-self.r*self.T) *
[Link](-self.d2)
return theta_annual / 365 # Per day
def vega(self) -> float:
'''Change in price per 1% change in vol (divide by 100 for %).'''
return self.S * [Link](self.d1) * [Link](self.T) / 100
def rho(self) -> float:
'''Change in price per 1% change in interest rates.'''
if [Link] == 'call':
return self.K * self.T * [Link](-self.r*self.T) * [Link](self.d2) /
100
else:
return -self.K * self.T * [Link](-self.r*self.T) * [Link](-self.d2)
/ 100
def implied_vol_approx(self) -> float:
'''Brenner-Subrahmanyam approximation (useful estimate).'''
return ([Link]() / self.S) * [Link](2*[Link]/self.T)
def summary(self) -> dict:
return {
'Type' : [Link](),
'Price' : round([Link](), 4),
'Delta' : round([Link](), 4),
'Gamma' : round([Link](), 6),
'Theta' : round([Link](), 4),
'Vega' : round([Link](), 4),
'Rho' : round([Link](), 4),
'Intrinsic': round(max(self.S-self.K,0) if [Link]=='call' else
max(self.K-self.S,0), 4),
'Time Val': round([Link]()-max(self.S-self.K,0 if
[Link]=='call' else self.K-self.S), 4),
}
# ── Example: SPY ATM Call ─────────────────────────────────────
opt = BlackScholes(S=450, K=450, T=30/365, r=0.05, sigma=0.18,
option_type='call')
for k, v in [Link]().items():
print(f' {k:12s}: {v}')
# Output:
# --- Type : CALL
# --- Price : 8.2341
© 2025 Quant Projects Guide 38
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# --- Delta : 0.5264
# --- Gamma : 0.0143
# --- Theta : -0.1897
# --- Vega : 0.7645
# --- Rho : 0.1823
# --- Intrinsic : 0.0
# --- Time Val : 8.2341
Visualise How Greeks Change With Stock Price
def plot_greeks_vs_price(K=450, T=30/365, r=0.05, sigma=0.18,
option_type='call'):
'''Show all Greeks across a range of stock prices.'''
stock_prices = [Link](K*0.80, K*1.20, 200) # 80% to 120% of strike
prices, deltas, gammas, thetas, vegas = [], [], [], [], []
for S in stock_prices:
opt = BlackScholes(S=S, K=K, T=T, r=r, sigma=sigma,
option_type=option_type)
[Link]([Link]())
[Link]([Link]())
[Link]([Link]())
[Link]([Link]())
[Link]([Link]())
fig = make_subplots(
rows=2, cols=3,
subplot_titles=['Option Price','Delta','Gamma','Theta (daily)','Vega (per
1% vol)','Payoff at Expiry'],
)
pairs = [
(prices, '#74b9ff', 'Price', 1, 1),
(deltas, '#00b894', 'Delta', 1, 2),
(gammas, '#fdcb6e', 'Gamma', 1, 3),
(thetas, '#e17055', 'Theta', 2, 1),
(vegas, '#a29bfe', 'Vega', 2, 2),
]
for y, color, name, row, col in pairs:
fig.add_trace([Link](
x=stock_prices, y=y,
line=dict(color=color, width=2),
name=name), row=row, col=col)
fig.add_vline(x=K, line_dash='dash', line_color='white', row=row,
col=col)
# Payoff at expiry
payoff = [max(S-K,0) for S in stock_prices] if option_type=='call' else
[max(K-S,0) for S in stock_prices]
# Subtract premium for P&L
premium = BlackScholes(S=K, K=K, T=T, r=r, sigma=sigma,
option_type=option_type).price()
pnl = [p - premium for p in payoff]
© 2025 Quant Projects Guide 39
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
fig.add_trace([Link](
x=stock_prices, y=pnl,
line=dict(color='#00b894', width=2),
fill='tozeroy', fillcolor='rgba(0,184,148,0.15)',
name='P&L at Expiry'), row=2, col=3)
fig.add_hline(y=0, line_dash='dash', line_color='white', row=2, col=3)
fig.add_vline(x=K, line_dash='dash', line_color='#fdcb6e', row=2, col=3)
fig.update_layout(
title=f'{option_type.upper()} Option Greeks — Strike {K}, {T*365:.0f}d,
IV {sigma*100:.0f}%',
template='plotly_dark', height=700,
)
[Link]()
plot_greeks_vs_price(K=450, T=30/365, sigma=0.18, option_type='call')
plot_greeks_vs_price(K=450, T=30/365, sigma=0.18, option_type='put')
© 2025 Quant Projects Guide 40
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T6
Paper Trading Backend with Alpaca API
What Is Paper Trading?
Paper trading means running your strategy with real market data and real order execution logic —
but using fake money. It is the final test before risking real capital. Alpaca provides a free paper
trading API that connects directly to live market data.
Setting Up Your Alpaca Account
6. Go to [Link] and create a free account.
7. In the dashboard, click 'Paper Trading' in the top-left to switch to paper mode.
8. Click 'Generate New Key' under 'API Keys' — save your API Key and Secret Key
somewhere safe.
9. The paper trading base URL is: [Link]
Connecting to Alpaca in Python
import alpaca_trade_api as tradeapi
import pandas as pd
# ── Your API credentials (replace with your own) ──────────────
API_KEY = 'YOUR_API_KEY_HERE'
API_SECRET = 'YOUR_SECRET_KEY_HERE'
BASE_URL = '[Link] # Paper trading
# ── Connect ───────────────────────────────────────────────────
api = [Link](API_KEY, API_SECRET, BASE_URL, api_version='v2')
# ── Check account status ──────────────────────────────────────
account = api.get_account()
print(f'Account Status : {[Link]}')
print(f'Portfolio Value: ${float(account.portfolio_value):,.2f}')
print(f'Cash Available : ${float([Link]):,.2f}')
print(f'Buying Power : ${float(account.buying_power):,.2f}')
# Output:
# --- Account Status : ACTIVE
# --- Portfolio Value: $100,000.00
# --- Cash Available : $100,000.00
# --- Buying Power : $100,000.00
Getting Live Market Data from Alpaca
import alpaca_trade_api as tradeapi
© 2025 Quant Projects Guide 41
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
import pandas as pd
api = [Link](API_KEY, API_SECRET, BASE_URL, api_version='v2')
# ── Get historical bars ───────────────────────────────────────
bars = api.get_bars(
'SPY',
[Link], # 1-minute bars
start='2024-01-15',
end='2024-01-16',
adjustment='raw',
).df
print([Link]())
# ── Get 5-minute bars ─────────────────────────────────────────
bars_5m = api.get_bars(
'SPY',
[Link](5, [Link]),
start='2024-01-10',
end='2024-01-15',
).df
print(f'5-minute bars: {len(bars_5m)} rows')
# ── Get current quote (live bid/ask) ─────────────────────────
quote = api.get_latest_quote('SPY')
print(f'Bid: ${[Link]:.2f} x {[Link]}')
print(f'Ask: ${[Link]:.2f} x {quote.as_}')
print(f'Spread: ${[Link] - [Link]:.2f}')
Placing Orders
# ── Market order (executes immediately at best price) ─────────
def market_buy(ticker, qty):
order = api.submit_order(
symbol=ticker,
qty=qty,
side='buy',
type='market',
time_in_force='day', # Order expires at end of day if not filled
)
print(f'BUY {qty} {ticker} | Order ID: {[Link]} | Status: {[Link]}')
return order
def market_sell(ticker, qty):
order = api.submit_order(
symbol=ticker,
qty=qty,
side='sell',
type='market',
time_in_force='day',
)
print(f'SELL {qty} {ticker} | Order ID: {[Link]} | Status: {[Link]}')
return order
© 2025 Quant Projects Guide 42
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# ── Limit order (only fills at your price or better) ──────────
def limit_buy(ticker, qty, limit_price):
order = api.submit_order(
symbol=ticker,
qty=qty,
side='buy',
type='limit',
limit_price=limit_price,
time_in_force='gtc', # Good Till Cancelled
)
return order
# ── Bracket order (entry + stop + target in one order) ────────
def bracket_buy(ticker, qty, take_profit, stop_loss):
'''A bracket order automatically manages your trade.'''
order = api.submit_order(
symbol=ticker,
qty=qty,
side='buy',
type='market',
time_in_force='day',
order_class='bracket',
take_profit={'limit_price': take_profit},
stop_loss={'stop_price': stop_loss},
)
print(f'BRACKET BUY {qty} {ticker} | TP: {take_profit} | SL: {stop_loss}')
return order
# ── Check existing positions ──────────────────────────────────
def get_positions():
positions = api.list_positions()
if not positions:
print('No open positions.')
return
for p in positions:
print(f'{[Link]:6s} | Qty: {[Link]:6s} | Avg: $
{float(p.avg_entry_price):.2f}'
f' | Market: ${float(p.market_value):8,.2f}'
f' | P&L: ${float(p.unrealized_pl):+,.2f}
({float(p.unrealized_plpc)*100:.2f}%)')
# ── Cancel all orders ────────────────────────────────────────
def cancel_all():
api.cancel_all_orders()
print('All orders cancelled.')
# ── Liquidate everything (emergency) ─────────────────────────
def close_all_positions():
api.close_all_positions()
print('All positions closed.')
The Automated Strategy Loop
import time
© 2025 Quant Projects Guide 43
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
import datetime
import yfinance as yf
import pandas as pd
import numpy as np
def is_market_open():
'''Check if US stock market is currently open.'''
clock = api.get_clock()
return clock.is_open
def get_latest_bars(ticker, n=50, interval='5m'):
'''Get the last n 5-minute bars for signal calculation.'''
df = [Link](ticker, period='5d', interval=interval, progress=False)
df = df.between_time('09:30','16:00').dropna()
return [Link][-n:]
def run_strategy_loop(ticker='SPY', qty=10, check_interval_seconds=300):
'''
Main strategy loop.
Runs every check_interval_seconds (default: every 5 minutes).
Checks the EMA9/EMA21 crossover signal and trades accordingly.
'''
print(f'Starting strategy on {ticker} | Checking every
{check_interval_seconds}s')
position_side = None # Track our current position: None, 'long', 'short'
while True:
try:
now = [Link]().strftime('%H:%M:%S')
if not is_market_open():
print(f'[{now}] Market closed. Waiting...')
[Link](60)
continue
# ── Get latest data ───────────────────────────────
df = get_latest_bars(ticker, n=50)
# ── Calculate signal ──────────────────────────────
ema9 = df['Close'].ewm(span=9, adjust=False).mean()
ema21 = df['Close'].ewm(span=21, adjust=False).mean()
fast_above_now = [Link][-1] > [Link][-1]
fast_above_prev = [Link][-2] > [Link][-2]
cross_up = fast_above_now and not fast_above_prev
cross_down = not fast_above_now and fast_above_prev
current_price = df['Close'].iloc[-1]
print(f'[{now}] {ticker}: ${current_price:.2f} | EMA9: {[Link][-
1]:.2f} | EMA21: {[Link][-1]:.2f}', end=' ')
# ── Execute trades ────────────────────────────────
if cross_up and position_side != 'long':
print('→ CROSS UP detected!')
if position_side == 'short':
# Close short first
market_buy(ticker, qty) # Buy to cover
© 2025 Quant Projects Guide 44
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# Open long
market_buy(ticker, qty)
position_side = 'long'
elif cross_down and position_side != 'short':
print('→ CROSS DOWN detected!')
if position_side == 'long':
# Close long first
market_sell(ticker, qty)
# Open short
market_sell(ticker, qty)
position_side = 'short'
else:
print(f'→ No signal. Position: {position_side}')
# ── Wait for next check ───────────────────────────
[Link](check_interval_seconds)
except KeyboardInterrupt:
print('\nStopping strategy. Closing all positions...')
close_all_positions()
cancel_all()
break
except Exception as e:
print(f'Error: {e}')
[Link](30) # Wait 30s and retry on error
# ── Start it (PAPER TRADING ONLY) ────────────────────────────
# run_strategy_loop('SPY', qty=10)
# Press Ctrl+C to stop and close all positions
© 2025 Quant Projects Guide 45
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T7
Portfolio Risk Manager with Live Position Monitoring
Real-Time Risk Monitoring System
This project builds a risk management layer that sits on top of any strategy. It monitors all positions
in real time and automatically enforces daily loss limits, position size limits, and stop-losses.
import alpaca_trade_api as tradeapi
import pandas as pd
import numpy as np
from datetime import datetime
class RiskManager:
'''
Monitors positions and enforces risk limits.
Should be run in parallel with your trading strategy.
'''
def __init__(self, api,
max_position_size = 5000, # Max $ per position
daily_loss_limit = -500, # Stop trading if P&L < this
max_drawdown_pct = 0.05, # Max drawdown as % of portfolio
position_stop_pct = 0.02, # Per-position stop loss (2%)
verbose=True):
[Link] = api
self.max_pos = max_position_size
[Link] = daily_loss_limit
self.max_dd = max_drawdown_pct
self.pos_stop = position_stop_pct
[Link] = verbose
self.trading_halted = False
self.start_equity = float(api.get_account().equity)
def log(self, msg):
if [Link]:
print(f'[RISK {[Link]().strftime("%H:%M:%S")}] {msg}')
def get_daily_pnl(self) -> float:
account = [Link].get_account()
return float([Link]) - float(account.last_equity)
def check_daily_loss_limit(self) -> bool:
'''Returns True if trading should be halted.'''
pnl = self.get_daily_pnl()
if pnl < [Link]:
[Link](f'DAILY LOSS LIMIT BREACHED: P&L = ${pnl:,.2f} < limit $
{[Link]:,.2f}')
return True
return False
def check_drawdown(self) -> bool:
© 2025 Quant Projects Guide 46
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
'''Returns True if max drawdown breached.'''
equity = float([Link].get_account().equity)
dd_pct = (equity - self.start_equity) / self.start_equity
if dd_pct < -self.max_dd:
[Link](f'MAX DRAWDOWN BREACHED: {dd_pct:.2%}')
return True
return False
def check_position_sizes(self):
'''Reduce any position that exceeds max size.'''
positions = [Link].list_positions()
for p in positions:
mkt_val = abs(float(p.market_value))
if mkt_val > self.max_pos:
excess_val = mkt_val - self.max_pos
excess_qty = int(excess_val / float(p.current_price))
if excess_qty > 0:
[Link](f'SIZE LIMIT: {[Link]} ${mkt_val:,.0f} > $
{self.max_pos:,.0f}. Reducing by {excess_qty}.')
side = 'sell' if float([Link]) > 0 else 'buy'
[Link].submit_order(
symbol=[Link], qty=excess_qty,
side=side, type='market', time_in_force='day'
)
def check_stop_losses(self):
'''Exit any position that has lost more than stop%.'''
positions = [Link].list_positions()
for p in positions:
pnl_pct = float(p.unrealized_plpc) # as decimal, e.g. -0.025 = -
2.5%
if pnl_pct < -self.pos_stop:
[Link](f'STOP LOSS: {[Link]} {pnl_pct:.2%}. Closing
position.')
[Link].close_position([Link])
def run_checks(self) -> bool:
'''
Run all risk checks. Returns False if trading should halt.
Call this every few minutes in your main loop.
'''
if self.trading_halted:
[Link]('Trading already halted.')
return False
# Order matters: check kill conditions first
if self.check_daily_loss_limit() or self.check_drawdown():
self.trading_halted = True
[Link]('HALTING TRADING — closing all positions.')
[Link].close_all_positions()
[Link].cancel_all_orders()
return False
# Non-halting checks
self.check_position_sizes()
self.check_stop_losses()
return True # OK to continue trading
© 2025 Quant Projects Guide 47
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
def status_report(self):
'''Print current risk status.'''
account = [Link].get_account()
daily_pnl= self.get_daily_pnl()
equity = float([Link])
dd_pct = (equity - self.start_equity) / self.start_equity * 100
positions= [Link].list_positions()
print('\n' + '='*50)
print(' RISK STATUS REPORT')
print('='*50)
print(f' Portfolio Value : ${equity:,.2f}')
print(f' Daily P&L : ${daily_pnl:+,.2f}')
print(f' Drawdown : {dd_pct:+.2f}%')
print(f' Daily P&L Limit : ${[Link]:,.2f}')
print(f' Max Drawdown : {-self.max_dd*100:.1f}%')
print(f' Trading Halted : {self.trading_halted}')
print(f' Open Positions : {len(positions)}')
for p in positions:
print(f' {[Link]}: {[Link]} @ ${float(p.avg_entry_price):.2f}',
f'| P&L: ${float(p.unrealized_pl):+,.2f}
({float(p.unrealized_plpc)*100:.2f}%)')
print('='*50)
# ── Usage ─────────────────────────────────────────────────────
# rm = RiskManager(api, max_position_size=5000, daily_loss_limit=-300)
# In your main loop:
# if not rm.run_checks():
# break # Stop trading, risk limits breached
© 2025 Quant Projects Guide 48
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
PROJEC
T8
Walk-Forward Optimiser — Avoiding Overfitting
The Most Important Project: Honest Strategy Testing
This is the project that separates real quant research from amateur curve-fitting. Walk-forward
testing is the only way to know if your strategy has genuine edge — it tests the strategy on data it
never saw during development.
import pandas as pd
import numpy as np
import yfinance as yf
from itertools import product
import plotly.graph_objects as go
from [Link] import make_subplots
class WalkForwardOptimiser:
'''
Walk-forward testing framework.
Process:
1. Split data into rolling windows
2. In each window: optimise parameters on first 70% (in-sample)
3. Test best parameters on last 30% (out-of-sample)
4. Concatenate all out-of-sample results = honest performance estimate
'''
def __init__(self, df, window_size=252, oos_size=63, step=63):
'''
df : Price DataFrame
window_size : Total window length in bars (252 = 1 year daily)
oos_size : Out-of-sample portion in bars (63 = 3 months)
step : Move forward this many bars each iteration
'''
[Link] = df
self.window_size = window_size
self.oos_size = oos_size
self.is_size = window_size - oos_size
[Link] = step
[Link] = []
def _run_single(self, df_is, df_oos, fast, slow):
'''Run EMA crossover strategy on a DataFrame. Return Sharpe.'''
for df in [df_is, df_oos]:
ema_fast = df['Close'].ewm(span=fast, adjust=False).mean()
ema_slow = df['Close'].ewm(span=slow, adjust=False).mean()
signal = [Link]([Link](ema_fast > ema_slow, 1, -1),
index=[Link])
ret = df['Close'].pct_change() * [Link](1)
ret = [Link]()
pass
© 2025 Quant Projects Guide 49
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
# Run on in-sample
ema_fast = df_is['Close'].ewm(span=fast, adjust=False).mean()
ema_slow = df_is['Close'].ewm(span=slow, adjust=False).mean()
sig_is = [Link]([Link](ema_fast > ema_slow, 1, -1),
index=df_is.index)
ret_is = (df_is['Close'].pct_change() * sig_is.shift(1)).dropna()
sharpe_is= ret_is.mean()/ret_is.std()*[Link](252) if ret_is.std()>0 else
0
# Run on out-of-sample
ema_fast = df_oos['Close'].ewm(span=fast, adjust=False).mean()
ema_slow = df_oos['Close'].ewm(span=slow, adjust=False).mean()
sig_oos = [Link]([Link](ema_fast > ema_slow, 1, -1),
index=df_oos.index)
ret_oos = (df_oos['Close'].pct_change() * sig_oos.shift(1)).dropna()
sharpe_oos= ret_oos.mean()/ret_oos.std()*[Link](252) if ret_oos.std()>0
else 0
return sharpe_is, sharpe_oos, ret_oos
def run(self, fast_range=range(5,25,5), slow_range=range(20,80,10)):
'''
Run full walk-forward optimisation.
For each window: find best (fast,slow) on IS, apply to OOS.',
'''
all_param_combos = [(f,s) for f,s in product(fast_range, slow_range) if f
< s]
all_oos_returns = []
window_summary = []
n_windows = (len([Link]) - self.window_size) // [Link] + 1
print(f'Running {n_windows} walk-forward windows...')
for i in range(0, len([Link]) - self.window_size + 1, [Link]):
window = [Link][i : i + self.window_size]
df_is = [Link][:self.is_size]
df_oos = [Link][self.is_size:]
if len(df_oos) < 20:
continue
# ── Optimise on in-sample ────────────────────────
best_sharpe_is = -[Link]
best_params = None
for fast, slow in all_param_combos:
try:
sh_is, _, _ = self._run_single(df_is, df_oos, fast, slow)
if sh_is > best_sharpe_is:
best_sharpe_is = sh_is
best_params = (fast, slow)
except:
pass
if best_params is None:
continue
# ── Apply best params to out-of-sample ───────────
© 2025 Quant Projects Guide 50
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
fast, slow = best_params
sh_is, sh_oos, ret_oos = self._run_single(df_is, df_oos, fast, slow)
all_oos_returns.append(ret_oos)
window_summary.append({
'window_start' : df_is.index[0],
'oos_start' : df_oos.index[0],
'oos_end' : df_oos.index[-1],
'best_fast' : fast,
'best_slow' : slow,
'sharpe_is' : round(sh_is, 3),
'sharpe_oos' : round(sh_oos, 3),
})
print(f' Window {len(window_summary)}: params=({fast},{slow}) | IS
Sharpe={sh_is:.2f} | OOS Sharpe={sh_oos:.2f}')
# ── Concatenate OOS returns ───────────────────────────
if all_oos_returns:
combined_oos = [Link](all_oos_returns).sort_index()
combined_oos = combined_oos[~combined_oos.[Link]()]
else:
combined_oos = [Link](dtype=float)
self.summary_df = [Link](window_summary)
self.combined_oos= combined_oos
self._print_summary()
return self
def _print_summary(self):
oos = self.combined_oos.dropna()
if len(oos) == 0:
print('No OOS data.'); return
ann_ret = [Link]() * 252
ann_vol = [Link]() * [Link](252)
sharpe = ann_ret / ann_vol if ann_vol > 0 else 0
equity = (1 + oos).cumprod()
max_dd = ((equity - [Link]()) / [Link]()).min()
avg_is_sharpe = self.summary_df['sharpe_is'].mean()
avg_oos_sharpe = self.summary_df['sharpe_oos'].mean()
degrade_pct = (avg_is_sharpe - avg_oos_sharpe) / abs(avg_is_sharpe) *
100 if avg_is_sharpe != 0 else 0
print('\n' + '='*55)
print(' WALK-FORWARD RESULTS')
print('='*55)
print(f' Windows Tested : {len(self.summary_df)}')
print(f' Avg IS Sharpe : {avg_is_sharpe:.3f}')
print(f' Avg OOS Sharpe : {avg_oos_sharpe:.3f}')
print(f' Performance Decay : {degrade_pct:.1f}% (< 50% is acceptable)')
print(f' Combined OOS Ann. Return : {ann_ret:.2%}')
print(f' Combined OOS Ann. Vol : {ann_vol:.2%}')
print(f' Combined OOS Sharpe : {sharpe:.3f}')
print(f' Combined OOS Max DD : {max_dd:.2%}')
print('='*55)
print()
© 2025 Quant Projects Guide 51
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
print(' INTERPRETATION:')
if avg_oos_sharpe > 0.5:
print(' ✓ OOS Sharpe > 0.5: REAL EDGE detected in out-of-sample.')
else:
print(' ✗ OOS Sharpe < 0.5: Likely overfitting — edge not
confirmed.')
if degrade_pct < 50:
print(' ✓ Performance decay < 50%: Parameters are stable.')
else:
print(' ✗ Performance decay > 50%: Strategy is overfit to in-
sample.')
print('='*55)
# ── Run the walk-forward test ─────────────────────────────────
df = [Link]('SPY', start='2015-01-01', end='2024-01-01', progress=False)
wfo = WalkForwardOptimiser(df, window_size=252, oos_size=63, step=63)
[Link](fast_range=range(5, 30, 5), slow_range=range(25, 80, 10))
# View the summary table
print(wfo.summary_df.to_string())
© 2025 Quant Projects Guide 52
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
CHAPTE
R9
Putting It All Together — Your Complete System
The Full Architecture of an Automated Trading System
Now that you have built each component individually, here is how they connect into a single,
production-quality automated trading system. This is the architecture used in professional trading
firms, scaled down for an individual trader.
Layer Component Project What It Does
Data Market Data Fetcher Ch.1 Downloads 5-min OHLCV from yfinance
or Alpaca API continuously
Signal Indicator Engine Ch.3 Computes EMAs, RSI, MACD, ATR,
Bollinger Bands on incoming data
Signal Strategy Logic Project 1 & 2 Detects FVGs, crossovers, or any custom
condition — outputs +1/-1/0 signal
Risk Risk Manager Project 7 Checks position limits, daily P&L limit,
max drawdown before each trade
Execution Order Manager Project 6 Converts signal into Alpaca bracket order
(entry + TP + SL in one)
Analytics Backtester Project 3 Tests strategy historically before
deploying live
Analytics Walk-Forward Project 8 Validates the strategy was not overfit to
historical data
Monitoring Dashboard Project 4 & 5 Interactive Sharpe/Vol/Greeks charts
updated from live data
# ── Complete system template ──────────────────────────────────
import time, datetime
import yfinance as yf
import pandas as pd
import alpaca_trade_api as tradeapi
# 1. Config
TICKER = 'SPY'
QTY = 10
API_KEY = 'YOUR_KEY'
API_SECRET = 'YOUR_SECRET'
BASE_URL = '[Link]
# 2. Initialise
api = [Link](API_KEY, API_SECRET, BASE_URL, api_version='v2')
# 3. Main loop
def main():
© 2025 Quant Projects Guide 53
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
print(f'System starting. Trading {TICKER}.')
rm = RiskManager(api, max_position_size=5000, daily_loss_limit=-300)
position = None
while True:
if not is_market_open():
[Link](60); continue
# A) Risk check first — always
if not rm.run_checks():
print('Risk limits hit. Stopping.'); break
# B) Get data
df = [Link](TICKER, period='5d', interval='5m', progress=False)
df = df.between_time('09:30','16:00').dropna()
# C) Generate signals (replace with your strategy)
ema9 = df['Close'].ewm(span=9, adjust=False).mean()
ema21 = df['Close'].ewm(span=21, adjust=False).mean()
cross_up = ([Link][-1] > [Link][-1]) and ([Link][-2] <=
[Link][-2])
cross_down = ([Link][-1] < [Link][-1]) and ([Link][-2] >=
[Link][-2])
# D) Execute
atr = df['ATR'].iloc[-1] if 'ATR' in [Link] else df['Close'].iloc[-
1] * 0.005
price = df['Close'].iloc[-1]
if cross_up and position != 'long':
if position == 'short': market_buy(TICKER, QTY)
bracket_buy(TICKER, QTY,
take_profit=round(price + 2*atr, 2),
stop_loss =round(price - 1*atr, 2))
position = 'long'
elif cross_down and position != 'short':
if position == 'long': market_sell(TICKER, QTY)
position = 'short'
[Link](300) # Check every 5 minutes
if __name__ == '__main__':
main()
Your GitHub Portfolio Structure
Every project you build should be on GitHub with clean, documented code. Here is the
recommended folder structure:
quant_trader/
├── [Link] # Overview of all projects
├── [Link] # pip install -r [Link]
© 2025 Quant Projects Guide 54
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
│
├── 01_data/
│ ├── data_fetcher.py # Multi-timeframe data download functions
│ └── data_cleaner.py # Handle missing data, splits, dividends
│
├── 02_indicators/
│ ├── moving_averages.py # SMA, EMA, VWAP
│ ├── [Link] # RSI, MACD, Stochastic
│ └── [Link] # ATR, Bollinger, Keltner
│
├── 03_strategies/
│ ├── fvg_strategy.py # Fair Value Gap detector + trader
│ ├── ema_crossover.py # 5-min crossover strategy
│ └── base_strategy.py # Abstract base class for all strategies
│
├── 04_backtesting/
│ ├── [Link] # Backtester class with all metrics
│ ├── walk_forward.py # Walk-forward optimiser
│ └── [Link] # Sharpe, drawdown, all stats functions
│
├── 05_execution/
│ ├── alpaca_broker.py # Order management via Alpaca API
│ ├── risk_manager.py # Position & portfolio risk controls
│ └── main_loop.py # Main automated trading loop
│
├── 06_analytics/
│ ├── strategy_dashboard.py # Sharpe/Vol interactive dashboard
│ ├── monthly_heatmap.py # Monthly returns visualisation
│ └── options_pricer.py # Black-Scholes + Greeks
│
└── notebooks/
├── 01_exploration.ipynb # Data exploration notebooks
├── 02_signal_research.ipynb
└── 03_backtest_results.ipynb
What to Put on Your Jane Street Application
When you complete all 8 projects, here is exactly what to write about them:
Application Field What to Write
Project Title Automated Quant Trading System: FVG & EMA Crossover Strategies
Technical Skills Python, pandas, NumPy, Plotly, Black-Scholes, Alpaca API, walk-
Demonstrated forward testing
Quantitative Skills Sharpe ratio optimisation, drawdown analysis, walk-forward validation,
Demonstrated Kelly criterion position sizing
Key Results to Mention Backtest Sharpe ratio (cite the number), max drawdown, % of FVGs
that fill, OOS vs IS Sharpe comparison
GitHub Link Link directly to your quant_trader repository — make sure README is
clear and code is documented
What It Proves You can: write quantitative trading code, backtest honestly, manage
© 2025 Quant Projects Guide 55
QUANT TRADER: Python Projects Bible From Zero to Automated Trading Systems
risk programmatically, and execute automated orders — all core quant
trader skills
Checklist: Am I Ready to Apply?
Skill How to Verify
FVG Detection Can you explain the three-candle pattern and show code that
detects it correctly?
Backtest Realism Does your backtest use next-bar execution? Does it account for
transaction costs? Do you have a walk-forward result?
Interactive Charts Can you load data and produce a candlestick chart with indicators
and signals in under 10 minutes?
Sharpe Dashboard Can you load any ticker and see rolling Sharpe, volatility, and
drawdown instantly?
Alpaca Integration Can you place a bracket order in paper trading with a stop and
target attached?
Risk Management Does your strategy automatically stop trading if the daily loss limit
is hit?
Options Greeks Can you calculate the delta, gamma, theta, and vega of any option
and explain what each number means?
Walk-Forward Test Have you compared your strategy's in-sample vs out-of-sample
Sharpe ratio and can you explain what the difference tells you?
────────────────────────────────────────
The code you write is the proof you understand.
Build every project. Push every commit. Show your work.
The GitHub repository is your trading floor.
© 2025 Quant Projects Guide 56