0% found this document useful (0 votes)
3 views33 pages

QuantPro Week5

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views33 pages

QuantPro Week5

Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

QUANT
PRO
Advanced Quant Finance Course
WEEK 5 — Days 31 to 39

9 3 1 LIVE
DAYS NEW LIBS PROJECT SYSTEM

Statistics · Implied Volatility · GARCH · Live Data Pipeline · Transaction Costs · Walk-Forward Testing ·
Project 3

You completed Weeks 1-4. Week 5 answers that question.


You have 10 quant models, a backtested Every model gets statistically validated.
strategy, and a GitHub portfolio. Now you are Every backtest gets realistic costs. Every
asking: "But does any of this actually work signal gets live data. Every result gets proven
with real money?" on unseen data.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 1


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

WEEK 5 AT A GLANCE
Week 5 is the bridge between a student project and a professional tool. You built 10 models in Weeks 1-4.
This week you make them work in the real world — with statistics to prove they are real, live data that
refreshes automatically, realistic costs applied to every trade, and validation on data the strategy never
saw.

Day Topic Key Skill Project Output

31 Why Strategies Fail — ADX filter, whipsaw, regime Regime-filtered vs raw


Regimes detection backtest

32 The Math Bridge Normal dist, log returns, Visual explainers with real
regression intuition examples

33 Statistical Validation p-values, Sharpe t-stat, Full stats report on your


bootstrap CI strategy

34 Implied Vol + Vol Surface IV extraction, vol smile, IVR Vol smile chart for NIFTY
options

35 GARCH — Volatility That GARCH(1,1), persistence, GARCH vol vs rolling std


Thinks dynamic VaR comparison

36 Live Data Pipeline yfinance, schedule, error Auto-download + signal +


handling, logs alert daily

37 Transaction Cost Modelling STT, slippage, bid-ask, net PnL Gross vs net vs stress PnL
chart

38 Walk-Forward Testing WFE, OOS validation, Robustness report on your


permutation test strategy

39 Project 3 — Live Dashboard Combine all Week 5 skills into GitHub: live dashboard +
one system README

What Week 4 Showed You What Week 5 Adds

Backtests with impressive-looking returns Statistical proof returns are real, not luck

ML model with "59% accuracy" Proper time-series CV so 59% actually means 59%

Bought/sold at exact close price STT + slippage + bid-ask — real net PnL

pd.read_csv("[Link]") — file missing yfinance pipeline — live data, no CSV ever

Best SMA from grid search on same data Walk-forward validation on unseen data only

Rolling std dev as volatility proxy GARCH — dynamic vol that updates every day

Quant Pro | Advanced Quant Finance Course | Week 5 Page 2


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Why Strategies Really Fail

31 Market regimes, whipsaw, and the ADX filter that fixes both

The Problem Nobody Told You About in Week 3


Your Week 3 SMA crossover strategy looked great on the backtest chart. Smooth equity curve, positive
Sharpe ratio, decent win rate. But there is a silent assumption buried in every backtest you have ever run:
the market behaved the same way throughout the entire test period. It did not. Markets cycle through
completely different regimes, and a strategy built for one regime fails badly in another.

> REAL-WORLD ANALOGY


Think of a cricket batsman who averages 60 runs on flat pitches in Mumbai but averages 12 on turning pitches
in Ahmedabad. His overall average looks decent. But if you send him to Ahmedabad without understanding
this — you lose the match. A trading strategy that crushes a trending market and destroys itself in a sideways
market has exactly the same problem. You need to know which pitch you are on before you play.

The Three Market Regimes


Every market is in one of three states at any given time. Most strategies are built for one specific regime
and fail in the others. Understanding which regime you are in is one of the most valuable skills in all of
quant finance.

THREE MARKET REGIMES

TRENDING (Bull or Bear) SIDEWAYS (Choppy) VOLATILE (Crisis)


■■■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■
Price moves clearly in Price oscillates up Wild swings both
one direction for weeks and down in a tight ways, no clear
or months at a time range for months direction at all

TCS Jan-Mar 2023: +18% NIFTY Aug-Oct 2023: COVID Mar 2020:
Clear uptrend arrow-up Flat between 19000-19500 -38% then +40%

Best strategy: Best strategy: Best strategy:


SMA crossover, Mean reversion, Reduce position
momentum signals RSI buy/sell size, sell vol

SMA crossover in trending: YES SMA crossover in sideways: NO (whipsaw!)

What is Whipsaw?
Whipsaw is what kills trend-following strategies in sideways markets. The price crosses your moving
average, triggers a buy signal, then immediately reverses back — triggering a sell signal. This happens five

Quant Pro | Advanced Quant Finance Course | Week 5 Page 3


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

times in two weeks, each time costing you transaction costs and a small loss. The strategy bleeds to death
while the market goes nowhere.

WHIPSAW IN A SIDEWAYS MARKET — THE DEATH OF TREND-FOLLOWING

Market: 100 -> 102 -> 99 -> 103 -> 98 -> 101 -> 97 -> 102
SMA(20) stays near 100 throughout — no clear direction

Day 1: Price crosses above SMA -> BUY at 102 Entry: 102
Day 3: Price crosses below SMA -> SELL at 99 Loss: -3
Day 5: Price crosses above SMA -> BUY at 103 Entry: 103
Day 7: Price crosses below SMA -> SELL at 98 Loss: -5
Day 9: Price crosses above SMA -> BUY at 101 Entry: 101
Day 11:Price crosses below SMA -> SELL at 97 Loss: -4

Total: 6 trades, -12 points, plus all transaction costs


The market went exactly nowhere. The strategy lost money.
This is whipsaw. This is why the ADX filter exists.

The Fix — ADX Filter (Average Directional Index)


ADX measures the strength of a trend — not the direction, just how strong any trend is. ADX above 25
means a real trend is in place. ADX below 20 means the market is going sideways — choppy, no direction.
The fix is simple: only take SMA crossover signals when ADX is above 25. In sideways markets, step aside
and do nothing.

ADX QUICK REFERENCE

ADX 0-20 -> No trend. Market is sideways. STAY OUT completely.


ADX 20-25 -> Developing trend. Proceed with caution.
ADX > 25 -> Real trend confirmed. SMA crossover signals are valid.
ADX > 40 -> Very strong trend. Often near exhaustion — be careful.

IMPORTANT: ADX does NOT tell you direction (up or down).


The +DI and -DI lines tell you direction.
ADX only measures STRENGTH. A high ADX in a downtrend is still high ADX.

Python Code — Day 31: ADX Filter


import pandas as pd
import numpy as np
import [Link] as plt

df = pd.read_csv("[Link]")
df = df[df["SYMBOL"] == "TCS"].sort_values("DATE1").reset_index(drop=True)
df["DATE1"] = pd.to_datetime(df["DATE1"])

# Calculate ADX from scratch


def calculate_adx(df, period=14):
high, low, close = df["HIGH"], df["LOW"], df["CLOSE"]
prev_close = [Link](1)

Quant Pro | Advanced Quant Finance Course | Week 5 Page 4


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

tr = [Link]([high-low, (high-prev_close).abs(),
(low-prev_close).abs()], axis=1).max(axis=1)
up_move = high - [Link](1)
down_move = [Link](1) - low
plus_dm = [Link]((up_move > down_move) & (up_move > 0), up_move, 0)
minus_dm = [Link]((down_move > up_move) & (down_move > 0), down_move, 0)
atr = [Link](tr).ewm(alpha=1/period, adjust=False).mean()
plus_di = 100*[Link](plus_dm).ewm(alpha=1/period,adjust=False).mean()/atr
minus_di= 100*[Link](minus_dm).ewm(alpha=1/period,adjust=False).mean()/atr
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
adx = [Link](alpha=1/period, adjust=False).mean()
return adx

df["ADX"] = calculate_adx(df)
df["SMA_20"] = df["CLOSE"].rolling(20).mean()
df["SMA_50"] = df["CLOSE"].rolling(50).mean()

# Raw signal vs ADX-filtered signal


df["Raw_Sig"] = (df["SMA_20"] > df["SMA_50"]).astype(int)
df["Filt_Sig"] = [Link](
(df["SMA_20"] > df["SMA_50"]) & (df["ADX"] > 25), 1, 0)

# Simulate both
def simulate(df, sig_col, cap=100_000):
df = [Link]()
df["Pos"] = df[sig_col].shift(1).diff()
c, sh = cap, 0
eq = []
for _, row in [Link]():
if row["Pos"]==1 and sh==0:
en=row["CLOSE"]; sh=int(c/en); c-=sh*en
elif row["Pos"]==-1 and sh>0:
c+=sh*row["CLOSE"]; sh=0
[Link](c + sh*row["CLOSE"])
n = (df["Pos"].abs()>0).sum()//2
ret = (eq[-1]/cap - 1)*100
return [Link](eq), ret, n

eq_raw, r_raw, n_raw = simulate(df, "Raw_Sig")


eq_filt, r_filt, n_filt = simulate(df, "Filt_Sig")

print(f"Raw : {r_raw:+.1f}% | {n_raw} trades (whipsaw included)")


print(f"Filtered: {r_filt:+.1f}% | {n_filt} trades (only in trends)")
print("Fewer trades + less whipsaw = better risk-adjusted return")

Day 31 Key Takeaways


• Every market cycles through three regimes: trending, sideways, volatile. No single strategy wins in all
three.
• Whipsaw bleeds trend-following strategies dry in sideways markets — repeated small losses from false
signals.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 5


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

• ADX above 25 confirms a real trend exists. Below 20 means the market is going nowhere — do not
trade.
• Adding an ADX filter typically cuts trade count by 30-50% but removes most losing low-quality signals.
• From today forward: always ask what regime your strategy was tested in before trusting any backtest
result.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 6


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

The Math Bridge

32 Normal distribution, log returns, regression — the building blocks of every quant model

Why This Day Exists


Week 4 used advanced formulas — Black-Scholes, Monte Carlo GBM, factor regression — without fully
explaining the mathematical building blocks underneath. This day fills that gap. No calculus required. Pure
intuition with real stock market examples. By the end you will understand why the formulas in Week 4 look
the way they do.

1. The Normal Distribution — The Bell Curve


A distribution is simply the answer to: if you collected every value of something and drew a bar chart of how
often each value occurs — what shape would it make? The Normal Distribution makes a bell shape. Most
values cluster near the centre. Values far from the centre become increasingly rare.

THE BELL CURVE OF STOCK RETURNS

Frequency
| xxxxxxxxxx
| xxxxxxxxxxxxxxxx
| xxxxxxxxxxxxxxxxxxxxxxxx
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|______________________________________________
-3% -2% -1% 0% +1% +2% +3%
^
Most common: small moves near zero

Reading this for TCS (daily vol = 1.2%, mean = +0.05%):


68% of days: move between -1.15% and +1.25% (1 sigma)
95% of days: move between -2.35% and +2.45% (2 sigma)
99.7% of days: move between -3.55% and +3.65% (3 sigma)

68-95-99.7 RULE: memorise this. You will use it every week.


VaR(95%) = mean - 1.645 x sigma comes directly from this rule.

* KEY INSIGHT
The 68-95-99.7 rule is the single most useful fact in statistics for traders. It directly gives you what "normal"
daily moves look like. A -4% day on TCS is a 3+ sigma event — happens roughly once every 2-3 years under
normal conditions. When you see it happen more often, that tells you something is wrong with the normal
assumption — fat tails are present.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 7


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

2. Real Returns Are NOT Perfectly Normal


Here is the uncomfortable truth that caused the 2008 financial crisis, the LTCM collapse, and countless
blown-up strategies. Real stock returns have two critical differences from a perfect bell curve:

NORMAL vs REAL STOCK RETURNS — THE TWO KEY DIFFERENCES

Normal bell curve: Real stock returns:


xxxxxx xxxxxx
xxxxxxxxxx xxxxxxxxxx
xxxxxxxxxxxxxx vs xxxxxxxxxxxxxx
xxxxxxxxxxxxxxxx x xxxxxxxxxxxxxxxx x
Tails fall off FAT TAILS: crashes happen
smoothly more often than predicted

DIFFERENCE 1: FAT TAILS (Excess Kurtosis greater than 0)


Extreme moves (crashes, spikes) occur MORE frequently than
the bell curve predicts. COVID crash was a 20-sigma event
under normal assumptions — theoretically impossible. It happened.

DIFFERENCE 2: NEGATIVE SKEW


The left tail (big losses) is LONGER than the right tail.
Crashes are bigger and faster than rallies.
"Stairs up, elevator down" — this is the skew you are seeing.

Why it matters: parametric VaR and Black-Scholes both assume


perfect normality. Both underestimate tail risk as a result.

3. Simple Returns vs Log Returns


You learned simple returns in Week 1. Quant models usually use log returns. Here is exactly why — and
when to use each.

SIMPLE vs LOG RETURNS

Simple: r = (Price_today - Price_yesterday) / Price_yesterday


Log: r = ln(Price_today / Price_yesterday)

Example: TCS yesterday Rs 3500, today Rs 3570


Simple return = (3570-3500)/3500 = 2.00%
Log return = ln(3570/3500) = 1.98% (very close for small moves)

WHY LOG RETURNS ARE USED IN QUANT MODELS:

1. They are ADDITIVE over time


Simple: multiply returns (1.02 x 0.97 x 1.03 x ...) messy
Log: just add them (+2% + -3% + +3% = +2%) clean

2. Stock prices cannot go below zero


log(price) is always well-defined. This is why GBM

Quant Pro | Advanced Quant Finance Course | Week 5 Page 8


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

(Monte Carlo from Day 24) models log prices, not raw prices.

3. Log returns are approximately normally distributed


Making them compatible with most statistical models.

RULE: Use simple returns for reporting and strategy comparison.


Use log returns inside mathematical models and formulas.

4. Linear Regression in 90 Seconds


Regression appears in beta calculation, factor models, and pairs trading hedge ratios. The core idea is
simple: find the best straight line through a cloud of data points.

REGRESSION — FITTING THE BEST LINE

NIFTY daily return (x) vs TCS daily return (y):

TCS% +4 | *
+3| * *
+2| * * * <- line of best fit
+1| * *
0| * *
-1| * *
-2| *
■■■■■■■■■■■■■■■■■■■■■■
-2 -1 0 +1 +2 NIFTY%

Slope of the line = BETA


Beta = 1.2 means: NIFTY rises 1% -> TCS typically rises 1.2%
Intercept = ALPHA (return not explained by market movement)

Python one-liner:
from [Link] import linregress
slope, intercept, r, p, se = linregress(nifty_ret, tcs_ret)
# slope = beta, intercept = alpha, r**2 = R-squared

Day 32 Key Takeaways


• The bell curve is the baseline model for returns. 68-95-99.7 rule: memorise it and use it every week.
• Real returns have fat tails (crashes more common) and negative skew (losses bigger than gains).
Always check for these.
• Log returns are additive over time and work inside mathematical models. Simple returns are for human
communication.
• Linear regression finds the best-fit line through data — giving you beta, alpha, and R-squared directly.
• Every advanced model in Weeks 6-10 builds on these four concepts. Understanding them here pays
dividends throughout.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 9


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Statistical Validation

33 Is your strategy real or did you just get lucky? The numbers that tell the truth

The Hardest Question in Quant Finance


Every backtest produces a number. Return +18%. Sharpe 1.4. Win rate 62%. These look convincing. But
they answer the wrong question. The right question is: could I have gotten this exact result by random
chance, even if my strategy has zero genuine edge? Statistical validation is the only honest answer.

> REAL-WORLD ANALOGY


You flip a coin 10 times and get 7 heads. Is the coin biased? The probability of getting 7+ heads from a fair
coin is about 17% — so there is a 17% chance this was pure luck. Not convincing. Now flip 1000 times and get
700 heads. That probability is essentially zero. That is statistically significant. The same logic applies to every
backtest result. Ten winning trades means almost nothing. One hundred winning trades starts to mean
something real.

The p-value — Your Lie Detector


A p-value answers: "If my strategy had zero real edge, what is the probability I would see a result this good
just by random chance?" The lower the p-value, the less likely the result is just luck.

p-value INTERPRETATION

p > 0.10 -> Weak evidence. Very plausibly just luck.


p = 0.05 -> The standard threshold. Borderline.
p < 0.05 -> Significant. Less than 5% chance of luck.
p < 0.01 -> Strong. Less than 1% chance of luck.

Applied to backtest returns:


"65% win rate over 8 trades" -> p = 0.29 NOT significant
"58% win rate over 80 trades" -> p < 0.05 SIGNIFICANT
"54% win rate over 300 trades"-> p < 0.01 VERY SIGNIFICANT

Same win rate. Completely different meaning depending on sample size.

The Sharpe t-statistic


Sharpe t-statistic = Sharpe Ratio x sqrt(Number of Trades)
Need: t-statistic > 1.96 for 95% significance

Sharpe = 1.5, Trades = 6 -> t = 1.5 x 2.45 = 3.67 (looks ok...)


...but 6 trades is statistically meaningless regardless. Reject.

Sharpe = 0.9, Trades = 80 -> t = 0.9 x 8.94 = 8.05 significant

Quant Pro | Advanced Quant Finance Course | Week 5 Page 10


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

Sharpe = 1.1, Trades = 30 -> t = 1.1 x 5.48 = 6.02 significant

Rule: Need at least 30 trades for any validity. 100+ is ideal.

Python Code — Day 33: Complete Statistical Report


import pandas as pd
import numpy as np
from scipy import stats

trades_df = pd.read_csv("[Link]")
returns = trades_df["Return_Pct"].values / 100

n = len(returns)
mean_r = [Link](returns)
std_r = [Link](returns)
skew = [Link](returns)
kurt = [Link](returns)

print("=" * 42)
print(" STRATEGY STATISTICAL REPORT")
print("=" * 42)
print(f" Trades : {n}")
print(f" Mean Return : {mean_r*100:.2f}%")
print(f" Std Deviation : {std_r*100:.2f}%")
print(f" Skewness : {skew:.2f} (neg=bad tail risk)")
print(f" Kurtosis : {kurt:.2f} (pos=fat tails)")

# Is mean return significantly different from zero?


t_stat, p_value = stats.ttest_1samp(returns, 0)
print(f" t-statistic : {t_stat:.2f}")
print(f" p-value : {p_value:.4f}")
print(f" Significant? : {'YES' if p_value < 0.05 else 'NO'}")

# Sharpe t-statistic
rf = 0.065 / 252
sharpe = (mean_r - rf) / std_r * [Link](252)
sr_t = sharpe * [Link](n)
print(f" Sharpe Ratio : {sharpe:.2f}")
print(f" Sharpe t-stat : {sr_t:.2f} (need >1.96)")

# 95% Confidence Interval


se = std_r / [Link](n)
ci_lo = (mean_r - 1.96*se)*100
ci_hi = (mean_r + 1.96*se)*100
print(f" 95% CI : [{ci_lo:.2f}%, {ci_hi:.2f}%]")
if ci_lo < 0:
print(" WARNING: CI includes 0 — may have no real edge")

# Bootstrap Sharpe confidence interval


boot = [[Link]([Link](returns,n,replace=True)) /
[Link]([Link](returns,n,replace=True)) * [Link](252)
for _ in range(1000)]
print(f" Bootstrap Sharpe 95% CI: [{[Link](boot,2.5):.2f},",

Quant Pro | Advanced Quant Finance Course | Week 5 Page 11


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

f"{[Link](boot,97.5):.2f}]")

# Normality test
_, p_norm = [Link](returns)
print(f" Shapiro-Wilk p : {p_norm:.4f}")
print(" Normal?" , "YES" if p_norm > 0.05 else "NO — use historical VaR")

# GOLDEN RULE
From this day forward, every backtest result you present must include: number of trades, p-value, Sharpe
t-statistic, and bootstrap confidence interval. Without these four numbers, no result — including your own —
can be trusted.

Day 33 Key Takeaways


• p-value < 0.05 means the result is statistically significant — less than 5% chance it is random luck.
• Always report trade count alongside every metric. A Sharpe of 2.0 from 5 trades is completely
meaningless.
• Bootstrap CI shows the true uncertainty in your Sharpe Ratio — a wide range from -0.3 to 2.1 should
scare you.
• Negative skewness and high kurtosis warn that real losses will be larger than your normal-distribution
model predicts.
• If the 95% CI for mean return includes zero, your strategy may have no genuine edge — be very
cautious.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 12


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Implied Volatility and the Vol Surface

34 What the options market is telling you about the future — if you know how to listen

Two Kinds of Volatility: Backward vs Forward


In Week 2 you calculated Historical Volatility — the rolling standard deviation of past returns. It looks
backward. Implied Volatility (IV) is completely different. It looks forward. It is the volatility that option buyers
and sellers are agreeing on today, embedded in option prices, reflecting their expectation of future risk.

> REAL-WORLD ANALOGY


Two weather forecasters. The first checks last month's temperature records: "average was 34 degrees — that
is historical vol." The second studies satellite imagery and monsoon patterns: "next week will hit 42 degrees —
unusual storm coming." That second forecast is implied volatility. The market's best forward-looking estimate
of uncertainty, right now, in today's option prices. When an RBI meeting is next week, IV spikes — even before
anything has happened.

How IV is Extracted — Reverse Black-Scholes


On Day 22 you used Black-Scholes forward: give it sigma, get a price. IV runs it backward: give it the
market price, find the sigma that makes Black-Scholes produce that exact price. This is done numerically
using a root-finding algorithm called Brent's method.

FORWARD Black-Scholes:
Input: S=22000, K=22500, T=30d, r=6.5%, sigma=18%
Output: Call price = Rs 312

REVERSE (Implied Volatility):


Input: S=22000, K=22500, T=30d, r=6.5%, Market price=Rs 380
Question: What sigma makes Black-Scholes give Rs 380?
Answer: sigma = 22.4% <-- This is the Implied Volatility

Why Rs 380 instead of Rs 312?


RBI policy meeting in 5 days. Options market is pricing in
MORE uncertainty than history alone suggests. IV captures this.

The Volatility Smile — Why Black-Scholes Is Incomplete


If Black-Scholes were perfect, all options on the same stock with the same expiry would show the same IV
regardless of strike. In reality this never happens. Different strikes show different IV — a pattern called the
volatility smile or skew.

NIFTY OPTIONS — VOLATILITY SKEW (typical equity pattern)

Quant Pro | Advanced Quant Finance Course | Week 5 Page 13


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

IV%
28 - * *
26 - * *
24 - * *
22 - * *
20 - * *
18 - * *
16 - * * * *
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Far OTM ATM OTM Far OTM
ITM Puts Spot Calls Calls

Left side (puts) is STEEPER than right side (calls)


= the equity volatility SKEW (not a symmetric smile)

Why? Institutional investors buy OTM puts as crash insurance.


This permanent demand keeps OTM put IV elevated.
Result: Black-Scholes systematically underprices downside risk.

* KEY INSIGHT
IV Rank (IVR) = (Current IV - 52-week Low) / (52-week High - 52-week Low) x 100. IVR above 70 means
options are expensive vs history — conditions favour selling premium. IVR below 30 means options are cheap
— conditions favour buying options or adding hedges. This single number tells you which side of the options
market has the statistical edge right now.

Python Code — Day 34: Extracting Implied Volatility


import numpy as np
from [Link] import norm
from [Link] import brentq
import [Link] as plt

def bs_price(S, K, T, r, sigma, opt="call"):


if T <= 0 or sigma <= 0:
return max(0, S-K) if opt=="call" else max(0, K-S)
d1 = ([Link](S/K) + (r + 0.5*sigma**2)*T) / (sigma*[Link](T))
d2 = d1 - sigma*[Link](T)
if opt=="call": return S*[Link](d1) - K*[Link](-r*T)*[Link](d2)
return K*[Link](-r*T)*[Link](-d2) - S*[Link](-d1)

# brentq finds the sigma where bs_price(sigma) = market_price


def implied_vol(mkt_px, S, K, T, r, opt="call"):
try:
return brentq(lambda s: bs_price(S,K,T,r,s,opt)-mkt_px,
0.001, 5.0, xtol=1e-6)
except ValueError:
return [Link]

S, r, T = 22000, 0.065, 30/365

Quant Pro | Advanced Quant Finance Course | Week 5 Page 14


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

strikes = [Link](20900, 23200, 200)

# Simulate market prices with realistic equity skew


# (In practice: fetch from NSE option chain via nsepython)
def mkt_px(S, K, T, r):
m = K/S
iv = 0.18 + 0.18*(1-m)**2 if m < 1 else 0.16 + 0.06*(m-1)**2
return bs_price(S, K, T, r, iv, "call")

ivs = [implied_vol(max(mkt_px(S,K,T,r),0.01),S,K,T,r)*100 for K in strikes]

[Link](figsize=(10, 5))
[Link](strikes, ivs, "o-", color="royalblue", lw=2, ms=5)
[Link](S, color="tomato", linestyle="--", lw=1.5, label=f"ATM = {S}")
[Link]("NIFTY Volatility Smile — 30-Day Options", fontweight="bold")
[Link]("Strike Price")
[Link]("Implied Volatility (%)")
[Link](); [Link](True, alpha=0.3)
[Link]("vol_smile.png", dpi=150, bbox_inches="tight")
[Link]()

Day 34 Key Takeaways


• Historical vol looks backward at past moves. Implied vol looks forward — the market's expectation of
future uncertainty.
• IV is extracted by reverse Black-Scholes: find the sigma that produces the observed market option price.
• The vol skew in equity markets is permanent — OTM puts always carry higher IV than equivalent OTM
calls.
• IV Rank (IVR) tells you whether options are cheap or expensive vs the past year. Use it to pick sides.
• IV spikes before known events like earnings, RBI meetings, and budget day — this event premium is
systematic.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 15


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

GARCH — Volatility That Thinks

35 Model volatility clustering the way professional risk desks actually do it

The Problem with Rolling Standard Deviation


In Week 2 you measured volatility using rolling(20).std(). It works. But it treats every day in the 20-day
window equally — the crash 19 days ago counts the same as yesterday. This means it reacts too slowly to
new shocks and is too slow to forget old ones.

> REAL-WORLD ANALOGY


Judging how dangerous a river is by averaging the water level over the last 20 days. If there was a flood 19
days ago but it has been calm since, your average still shows high danger — even though the flood is over.
Conversely, if a storm just hit yesterday but the previous 19 days were calm, your average barely reflects the
new danger. Rolling std dev makes exactly this mistake with stock volatility.

Volatility Clustering — The Fact GARCH Captures


Look at any stock over several years. You will see one pattern immediately: turbulent periods follow
turbulent periods. Calm periods follow calm periods. This is called volatility clustering — one of the most
reliable facts in all of finance.

VOLATILITY CLUSTERING IN DAILY RETURNS

+4% | | ||
+3% | | || | |||
+2% | | || | || ||||||| |
+1% |||||||||||||||| | |||||||||||||||
0% |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
-1% |||||||||||||||| ||||||||||||||||||
-2% | | || |||||||| |
-3% | |||
<-- CALM -------> <-- TURBULENT --> <-- CALM -->

Rolling std dev: treats the calm and turbulent the same
if both fall within the 20-day window.
GARCH: knows you are IN a volatile period right now.
GARCH reacts faster to new shocks. Calms down faster too.

GARCH(1,1) — The Formula in Plain English

Quant Pro | Advanced Quant Finance Course | Week 5 Page 16


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

GARCH says tomorrow's variance is a combination of three things: a constant baseline, how large
yesterday's actual shock was, and how large yesterday's estimated variance was. If yesterday was wild,
today will probably also be wild.

GARCH(1,1) FORMULA:
var(today) = omega + alpha x shock^2(yesterday) + beta x var(yesterday)
^ ^ ^
Baseline How much recent How much yesterday
(constant) SHOCK matters today VARIANCE persists

Typical values for a large Indian stock like TCS:


omega = 0.000002 (tiny constant baseline)
alpha = 0.09 (9% weight on recent shock — reactive)
beta = 0.90 (90% weight on yesterday variance — sticky)
alpha + beta = 0.99 (very persistent — shocks last many days)

MUST HAVE: alpha + beta < 1.0 for the model to be stable.

Long-run vol = sqrt(omega / (1 - alpha - beta)) x sqrt(252)


This is the annualised vol GARCH expects in normal calm conditions.

* KEY INSIGHT
Alpha controls how quickly GARCH reacts to a new shock. Beta controls how long the shock persists. For
Indian large-cap stocks, beta is typically 0.88-0.93, meaning a volatility spike on Monday still has 88-93% of its
force on Tuesday. This is why markets stay elevated for days after a crash — the nervousness does not
disappear overnight.

Python Code — Day 35: Fitting GARCH(1,1)


# Install: pip install arch --break-system-packages
import pandas as pd, numpy as np
import [Link] as plt
from arch import arch_model

df = pd.read_csv("[Link]")
df = df[df["SYMBOL"]=="TCS"].sort_values("DATE1").copy()
df["DATE1"] = pd.to_datetime(df["DATE1"])

# arch needs returns in PERCENT form — multiply by 100


df["Return"] = df["CLOSE"].pct_change() * 100
df = [Link]()

# Fit GARCH(1,1)
# dist="t" uses t-distribution for fat tails — always use this
model = arch_model(df["Return"], vol="Garch", p=1, q=1,
mean="Zero", dist="t")
result = [Link](disp="off")

omega = [Link]["omega"]
alpha = [Link]["alpha[1]"]

Quant Pro | Advanced Quant Finance Course | Week 5 Page 17


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

beta = [Link]["beta[1]"]
lr_vol = [Link](omega/(1-alpha-beta)) * [Link](252)

print("=== GARCH(1,1) PARAMETERS ===")


print(f"omega (baseline) : {omega:.6f}")
print(f"alpha (shock wt) : {alpha:.4f}")
print(f"beta (persistence): {beta:.4f}")
print(f"alpha + beta : {alpha+beta:.4f} (must be < 1)")
print(f"Long-run Ann. Vol : {lr_vol:.1f}%")

# Forecast next 5 days


fc = [Link](horizon=5)
fvols = [Link]([Link][-1])
print("\nForecasted Daily Vol (next 5 days):")
for i, v in enumerate(fvols, 1):
print(f" Day {i}: {v:.2f}% (ann: {v*[Link](252):.1f}%)")

# Compare GARCH vs rolling std


df["Rolling"] = df["Return"].rolling(20).std()
df["GARCH"] = result.conditional_volatility

fig, axes = [Link](2,1,figsize=(13,8),sharex=True)


c = ["seagreen" if r>=0 else "tomato" for r in df["Return"]]
axes[0].bar(df["DATE1"], df["Return"], color=c, alpha=0.6)
axes[0].set_title("TCS Daily Returns", fontweight="bold")
axes[0].grid(True, alpha=0.2)

axes[1].plot(df["DATE1"],df["GARCH"],color="royalblue",lw=2,
label="GARCH(1,1)")
axes[1].plot(df["DATE1"],df["Rolling"],color="tomato",lw=1.5,
linestyle="--",label="20-day Rolling Std")
axes[1].set_title("GARCH vs Rolling Std Dev", fontweight="bold")
axes[1].set_ylabel("Daily Vol (%)")
axes[1].legend(); axes[1].grid(True, alpha=0.2)
plt.tight_layout()
[Link]("garch_comparison.png", dpi=150, bbox_inches="tight")
[Link]()

Day 35 Key Takeaways


• Volatility clusters: turbulent follows turbulent. GARCH models this directly. Rolling std dev does not.
• GARCH(1,1) needs only three parameters: omega (baseline), alpha (shock weight), beta (persistence).
• alpha + beta must be less than 1. Near 0.99 means shocks take many days to fully fade away.
• Always use dist="t" in arch_model — real returns have fat tails that the normal distribution misses
completely.
• The GARCH next-day vol forecast is your best risk estimate — feed it directly into position sizing
calculations.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 18


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Live Data Pipeline — Build It Once, Run It Forever

36 The system that downloads, computes, alerts, and logs — automatically every market day

Why Automation Changes Everything


Every project in Weeks 1-4 started the same way: open Jupyter, manually download a CSV, run the
notebook. Fine for learning. Completely impractical for a real system. A professional quant pipeline runs
automatically at 4 PM every market day — download fresh data, compute signals, check alerts, save results
— all while you are doing something else entirely.

> REAL-WORLD ANALOGY


A security system at a bank vault. A bad system requires a guard to manually check the door each time
someone asks. A good system checks the door every 60 seconds, logs every event, sends an alert if anything
unusual happens, and files a daily report — automatically, every day, without anyone telling it to. Your quant
pipeline should work exactly the same way. Set it up once. Let it run indefinitely.

Pipeline Architecture — 7 Steps


DAILY QUANT PIPELINE — RUNS AT 4:05 PM EVERY MARKET DAY

Step 1: DOWNLOAD yfinance fetches fresh OHLCV for all stocks


|
Step 2: VALIDATE check for missing data, outliers, stale prices
|
Step 3: COMPUTE calculate SMA, RSI, ADX, GARCH vol, signals
|
Step 4: ALERT CHECK did any golden cross or RSI extreme fire today?
|
Step 5: STORE save signals to CSV/JSON for tomorrow
|
Step 6: REPORT print clean dashboard table to terminal
|
Step 7: LOG write timestamp and status to log file

If any step fails: log the error, skip that stock, continue.
Never let one failed stock crash the whole pipeline.

Python Code — Day 36: Complete Automated Pipeline


# Install: pip install yfinance schedule --break-system-packages
import pandas as pd, numpy as np
import yfinance as yf

Quant Pro | Advanced Quant Finance Course | Week 5 Page 19


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

import json, os, logging


from datetime import date, datetime
import schedule, time

SYMBOLS = ["[Link]","[Link]","[Link]","[Link]","[Link]"]
NAMES = [[Link](".NS","") for s in SYMBOLS]
DATA_DIR = "live_data/"
[Link](DATA_DIR, exist_ok=True)

# Logging writes to file AND shows in terminal


[Link](level=[Link],
format="%(asctime)s %(levelname)s %(message)s",
handlers=[[Link]("[Link]"),
[Link]()])
log = [Link]()

def download_data():
[Link]("Downloading...")
for sym, name in zip(SYMBOLS, NAMES):
try:
df = [Link](sym).history(period="2y")
[Link] = pd.to_datetime([Link]).tz_localize(None)
df.to_csv(f"{DATA_DIR}{name}.csv")
[Link](f" {name}: {len(df)} rows")
except Exception as e:
[Link](f" {name}: FAILED - {e}")

def rsi_calc(s, p=14):


d=[Link](); g=[Link](lower=0).ewm(com=p-1,min_periods=p).mean()
l=(-d).clip(lower=0).ewm(com=p-1,min_periods=p).mean()
return 100 - 100/(1+g/l)

def compute_signals():
signals = {}
for name in NAMES:
try:
df = pd.read_csv(f"{DATA_DIR}{name}.csv",
index_col=0, parse_dates=True).sort_index()
if len(df) < 60: continue
c = df["Close"]
df["SMA_20"] = [Link](20).mean()
df["SMA_50"] = [Link](50).mean()
df["RSI"] = rsi_calc(c)
t, p = [Link][-1], [Link][-2]
signals[name] = {
"price" : round(float(t["Close"]),2),
"rsi" : round(float(t["RSI"]),1),
"golden" : p["SMA_20"]<=p["SMA_50"] and t["SMA_20"]>t["SMA_50"],
"death" : p["SMA_20"]>=p["SMA_50"] and t["SMA_20"]<t["SMA_50"],
"ob" : float(t["RSI"]) > 70,
"os" : float(t["RSI"]) < 30,

Quant Pro | Advanced Quant Finance Course | Week 5 Page 20


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

}
except Exception as e:
[Link](f" {name}: compute failed - {e}")
return signals

def report_and_store(signals):
alerts = []
for n, s in [Link]():
if s["golden"]: [Link](f"GOLDEN CROSS: {n} @ {s['price']}")
if s["death"]: [Link](f"DEATH CROSS : {n} @ {s['price']}")
if s["ob"]: [Link](f"OVERBOUGHT : {n} RSI={s['rsi']}")
if s["os"]: [Link](f"OVERSOLD : {n} RSI={s['rsi']}")
print(f"\n{'='*50}")
print(f" QUANT DASHBOARD {[Link]()}")
print(f"{'='*50}")
for n, s in [Link]():
tag = "GOLDEN" if s["golden"] else "DEATH" if s["death"] else "HOLD"
print(f" {n:<12} Rs{s['price']:>8.0f} RSI={s['rsi']:>5.1f} {tag}")
if alerts:
print(f"\n {len(alerts)} ALERT(S):")
for a in alerts: print(f" -> {a}")
[Link]({"date":str([Link]()),"alerts":alerts,"signals":signals},
open("daily_signals.json","w"), indent=2)
[Link](f"Pipeline done. {len(alerts)} alerts.")

def daily_job():
download_data()
report_and_store(compute_signals())

if __name__ == "__main__":
daily_job() # Run immediately on start
[Link]().[Link]("16:05").do(daily_job)
[Link]("Scheduler active. Daily at 16:05.")
while True:
schedule.run_pending()
[Link](60)

Day 36 Key Takeaways


• yfinance gives 2 years of free OHLCV for NSE stocks — use the ".NS" suffix ([Link], [Link] etc).
• Wrap every stock in try/except — one failed ticker must never crash the entire pipeline.
• Use the logging module instead of print() — every event gets a timestamp and writes to a file
automatically.
• [Link]().[Link]("16:05").do(job) runs any function at a fixed time — no Linux cron knowledge
needed.
• Save signals to JSON daily — you build an automatic historical log of every alert your system has ever
fired.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 21


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Transaction Cost Modelling

37 The gap between backtest PnL and real money — and exactly how to close it

The Silent Killer of Backtests


Your Week 3 backtest bought and sold at the exact closing price, with zero cost, instantly. In real trading,
none of that is true. There is a government tax on every sell. A gap between the buy and sell price in the
order book. A price movement the moment your order hits the market. These seem small individually but
compound across many trades into something that can completely wipe out a profitable strategy.

COMPLETE COST BREAKDOWN — ONE ROUND-TRIP TRADE (NSE Delivery)

Trade: Buy TCS Rs 3,500 -> Sell Rs 3,600 Gross gain: Rs 100/share
Capital: Rs 98,000 | Shares: 28

BUY SIDE:
Bid-ask spread (buy at ask not mid) 0.05% = Rs 49
Slippage (price moves as order fills) 0.05% = Rs 49

SELL SIDE:
STT Securities Transaction Tax 0.10% = Rs 101 <- LARGEST COST
NSE Exchange + SEBI charges 0.0035%= Rs 3
Bid-ask spread on sell 0.05% = Rs 49
Slippage on sell 0.05% = Rs 49
GST on charges = Rs 1

TOTAL ROUND-TRIP COST: = Rs 252


Gross profit: Rs 2,800
NET profit after costs: Rs 2,548 (-9%)

For a strategy making 0.20% per trade: GROSS=Rs196 COST=Rs252


Result: LOSING after costs despite positive backtest returns.

! IMPORTANT WARNING
STT alone — 0.1% on the sell side — costs Rs 100 on every Rs 1 lakh sell order. For a strategy with 20 round
trips per year on Rs 1 lakh capital, STT alone costs Rs 2,000 per year. Many strategies that look profitable in
backtests are losing strategies after STT is applied. This is not a minor detail. It changes the conclusion
completely.

Python Code — Day 37: Realistic Cost Modelling


import pandas as pd, numpy as np
import [Link] as plt

COSTS = {

Quant Pro | Advanced Quant Finance Course | Week 5 Page 22


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

"stt_pct" : 0.001, # 0.1% on SELL side only


"exchange_pct" : 0.0000345, # NSE + SEBI
"gst_rate" : 0.18,
"slippage_pct" : 0.0005, # per side
"spread_pct" : 0.0005, # per side
}

def one_way_cost(c):
exch = c["exchange_pct"]
return (exch + exch*c["gst_rate"] + c["stt_pct"]/2
+ c["slippage_pct"] + c["spread_pct"])

oc = one_way_cost(COSTS)
print(f"One-way: {oc*100:.3f}% Round-trip: {oc*200:.3f}%")
print(f"On Rs 1 lakh: Rs {oc*2*100_000:.0f} per round trip")

df = pd.read_csv("[Link]")
df = df[df["SYMBOL"]=="TCS"].sort_values("DATE1").reset_index(drop=True)
df["DATE1"] = pd.to_datetime(df["DATE1"])
df["SMA_20"] = df["CLOSE"].rolling(20).mean()
df["SMA_50"] = df["CLOSE"].rolling(50).mean()
df["Signal"] = (df["SMA_20"] > df["SMA_50"]).astype(int)
df["Position"] = df["Signal"].shift(1).diff()

def simulate(df, buy_c=0.0, sell_c=0.0, cap=100_000):


sh, en, c = 0, 0, cap
eq, trades = [], 0
for _, row in [Link]():
if row["Position"]==1 and sh==0:
px=row["CLOSE"]*(1+buy_c); sh=int(c/px); c-=sh*px; trades+=1
elif row["Position"]==-1 and sh>0:
px=row["CLOSE"]*(1-sell_c); c+=sh*px; sh=0; trades+=1
[Link](c + sh*row["CLOSE"])
ret=(eq[-1]/cap-1)*100
npt=ret/(trades//2) if trades>0 else 0
return [Link](eq), ret, trades//2, npt

eq_g, rg, ng, npg = simulate(df, 0, 0)


eq_r, rr, nr, npr = simulate(df, oc, oc)
eq_s, rs, ns, nps = simulate(df, 0.003, 0.003)

print("\n=== PERFORMANCE COMPARISON ===")


for lbl, ret, n, npt in [
("Gross (zero costs)", rg, ng, npg),
("Net (realistic) ", rr, nr, npr),
("Stress (high slip)", rs, ns, nps),
]:
v = "VIABLE" if npt > oc*2*100 else "NOT VIABLE"
print(f"{lbl}: {ret:+.1f}% trades={n} net/trade={npt:+.2f}% {v}")

fig, ax = [Link](figsize=(13,5))
[Link](df["DATE1"],eq_g,color="seagreen",lw=2,label=f"Gross {rg:+.1f}%")
[Link](df["DATE1"],eq_r,color="royalblue",lw=2,label=f"Net {rr:+.1f}%")

Quant Pro | Advanced Quant Finance Course | Week 5 Page 23


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

[Link](df["DATE1"],eq_s,color="tomato",lw=1.5,linestyle="--",
label=f"Stress {rs:+.1f}%")
[Link](100_000,color="gray",linestyle=":",lw=1)
ax.set_title("Strategy: Gross vs Net vs Stress PnL",fontweight="bold")
[Link](); [Link](True,alpha=0.2)
[Link]("cost_analysis.png",dpi=150,bbox_inches="tight")
[Link]()

Day 37 Key Takeaways


• STT (0.1% on sell) is the largest single cost for NSE delivery traders — Rs 100 per lakh on every sell.
• Net profit per trade must comfortably exceed round-trip cost. If not, the strategy cannot survive in
production.
• Always run three scenarios: zero cost, realistic, and high slippage stress. Present all three.
• High-frequency strategies are destroyed by costs. Low-frequency monthly strategies survive much
better.
• Never present a backtest result without also showing the net-of-costs version. Gross PnL is marketing.
Net PnL is truth.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 24


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Walk-Forward Testing — The Truth Test

38 Prove your strategy works on data it has never seen before

Why Your Backtest Is Lying to You


On Day 19 you ran a grid search and found that SMA(15,40) produced the best backtest returns. But best
on what data? The exact data you used to find the parameters. This is like studying last year's exam
answers and claiming you understand the subject because you scored 95% on that same exam. The only
honest test is one where the data was never touched during development.

> REAL-WORLD ANALOGY


A cricket team selects its eleven based on their statistics against Team B specifically — then plays Team B in
the final. Of course the statistics look great. They were built on that exact opponent. Now imagine selecting the
eleven based on performance against completely different teams, then playing Team B for the first time. The
second approach gives you a genuine measure of selection quality. Walk-forward testing does exactly this for
trading strategies.

WALK-FORWARD TESTING — HOW IT WORKS

YOUR FULL DATA:


+-----------------------+-------------+
| TRAIN: Find params | TEST: Verify|
| 2019 -------- 2022 | 2023 - 2024 |
+-----------------------+-------------+
Simple split: ONE test period. Could be lucky or unlucky.

WALK-FORWARD: MULTIPLE rolling test periods


Window 1: Train 2019-2020 | Test Q1 2021
Window 2: Train 2019-2021 | Test Q2 2021
Window 3: Train 2019-2021 | Test Q3 2021
Window 4: Train 2019-2022 | Test Q1 2022
Final result = ALL test periods combined.

Walk-Forward Efficiency (WFE) = OOS Return / IS Return


WFE > 0.5 -> strategy generalises well
WFE < 0.2 -> strategy is memorising history (overfit)

Python Code — Day 38: Walk-Forward Engine


import pandas as pd, numpy as np, itertools

df = pd.read_csv("[Link]")
df = df[df["SYMBOL"]=="TCS"].sort_values("DATE1").reset_index(drop=True)
df["DATE1"] = pd.to_datetime(df["DATE1"])

Quant Pro | Advanced Quant Finance Course | Week 5 Page 25


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

def backtest(df, fast, slow, cap=100_000):


d=[Link]()
d["F"]=d["CLOSE"].rolling(fast).mean()
d["S"]=d["CLOSE"].rolling(slow).mean()
d["P"]=(d["F"]>d["S"]).astype(int).shift(1).diff()
c,sh=cap,0
for _,r in [Link]():
if r["P"]==1 and sh==0: sh=int(c/r["CLOSE"]); c-=sh*r["CLOSE"]
elif r["P"]==-1 and sh>0: c+=sh*r["CLOSE"]; sh=0
return ((c+sh*d["CLOSE"].iloc[-1])/cap-1)*100

def walk_forward(df, frange, srange, train=504, test=63):


results=[]
for start in range(0, len(df)-train-test, test):
tr=[Link][start:start+train]
te=[Link][start+train:start+train+test]
best, bf, bs = -999, 20, 50
for f, s in [Link](frange, srange):
if f>=s: continue
r=backtest(tr,f,s)
if r>best: best,bf,bs=r,f,s
oos=backtest(te,bf,bs)
d=df["DATE1"].iloc[start+train].date()
[Link]({"window":str(d),"fast":bf,"slow":bs,
"IS":round(best,2),"OOS":round(oos,2)})
print(f" {d} ({bf}/{bs}) IS={best:+.1f}% OOS={oos:+.1f}%")
return [Link](results)

print("Running Walk-Forward...")
wf=walk_forward(df,[10,15,20,25],[30,40,50,60])
print(wf.to_string(index=False))
wfe=wf["OOS"].mean()/wf["IS"].mean()
wr=(wf["OOS"]>0).mean()*100
print(f"\nMean IS : {wf['IS'].mean():+.2f}%")
print(f"Mean OOS : {wf['OOS'].mean():+.2f}%")
print(f"OOS WinRate: {wr:.0f}%")
print(f"WFE: {wfe:.2f}")
print("Excellent" if wfe>0.5 else "Acceptable" if wfe>0.2 else "WARNING: overfit")

# GOLDEN RULE
The test set is a one-time-use resource. The moment you look at test results and use them to improve your
strategy — even once — it is no longer a test set. It has become training data. Treat your OOS period like a
sealed envelope: open it only when you are completely done building.

Day 38 Key Takeaways


• In-sample results always look better than out-of-sample. The question is: by how much?

Quant Pro | Advanced Quant Finance Course | Week 5 Page 26


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

• Walk-Forward Efficiency (WFE) above 0.5 means the strategy generalises well to new data.
• OOS win rate across multiple windows is the most honest measure of whether your strategy is robust.
• A strategy that works consistently across all walk-forward windows is real. One that works in 2 of 6 is
not.
• The test set is one-time use only. Looking at it to improve the strategy converts it to training data.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 27


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

DAY

Project 3 — Live Quant Dashboard

39 Every Week 5 skill combined into one system that runs automatically every market day

Project 3 Overview
Today you build the system that brings everything from Week 5 together. ADX regime filter from Day 31.
Statistical validation from Day 33. Live data pipeline from Day 36. Realistic costs from Day 37. GARCH
volatility from Day 35. Walk-forward validated signals. All running automatically every day at 4 PM without
you touching anything.

Section 1 Live data download via yfinance — 5 NIFTY stocks, 2-year history, daily refresh

Section 2 ADX regime filter — only generate signals when trend strength confirms

Section 3 SMA crossover + RSI alerts — signal generation with regime awareness

Section 4 GARCH(1,1) next-day volatility forecast per stock

Section 5 Transaction cost model — net PnL after STT, slippage, spread

Section 6 Portfolio VaR (historical method) on equal-weighted positions

Section 7 Statistical check — is current 3-month period Sharpe significant?

Section 8 4-panel matplotlib dashboard saved as PNG daily

Section 9 Alerts log written to JSON — permanent record of every signal

Section 10 GitHub upload — week5/ folder, full README, dashboard PNG committed

Complete Project 3 Code


# PROJECT 3 — LIVE QUANT DASHBOARD — Quant Pro Week 5
import pandas as pd, numpy as np, yfinance as yf
import [Link] as plt
import [Link] as gridspec
from arch import arch_model
from scipy import stats
from datetime import date
import json, warnings
[Link]("ignore")

SYMBOLS = ["[Link]","[Link]","[Link]","[Link]","[Link]"]
NAMES = [[Link](".NS","") for s in SYMBOLS]
COST_RT = 0.0023 # round-trip cost fraction

# 1. Download fresh data


raw = {}

Quant Pro | Advanced Quant Finance Course | Week 5 Page 28


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

for sym, name in zip(SYMBOLS, NAMES):


df = [Link](sym).history(period="2y")
[Link] = pd.to_datetime([Link]).tz_localize(None)
raw[name] = df.sort_index()
print(f" {name}: {len(df)} rows")

# Helpers
def rsi(s,p=14):
d=[Link](); g=[Link](lower=0).ewm(com=p-1,min_periods=p).mean()
return 100-100/(1+g/(-d).clip(lower=0).ewm(com=p-1,min_periods=p).mean())

def adx_val(df,p=14):
h,l,c=df["High"],df["Low"],df["Close"]
tr=[Link]([h-l,([Link]()).abs(),([Link]()).abs()],axis=1).max(1)
up=[Link](); dn=[Link]()-l
pm=[Link]((up>dn)&(up>0),up,0); mm=[Link]((dn>up)&(dn>0),dn,0)
atr=[Link](tr).ewm(alpha=1/p,adjust=False).mean()
pd_=100*[Link](pm).ewm(alpha=1/p,adjust=False).mean()/atr
md_=100*[Link](mm).ewm(alpha=1/p,adjust=False).mean()/atr
return (100*(pd_-md_).abs()/(pd_+md_)).ewm(alpha=1/p,adjust=False).mean()

# 2. Signals + GARCH per stock


results = {}
for name, df in [Link]():
c=df["Close"]; ret=c.pct_change().dropna()*100
df["SMA_20"]=[Link](20).mean(); df["SMA_50"]=[Link](50).mean()
df["RSI"]=rsi(c); df["ADX"]=adx_val(df).values
gm=arch_model([Link](),vol="Garch",p=1,q=1,mean="Zero",dist="t")
gr=[Link](disp="off")
fv=float([Link]([Link](horizon=1).[Link][-1,0]))
t=[Link][-1]; p=[Link][-2]
g=(p["SMA_20"]<=p["SMA_50"]) and (t["SMA_20"]>t["SMA_50"])
d=(p["SMA_20"]>=p["SMA_50"]) and (t["SMA_20"]<t["SMA_50"])
trend=float(t["ADX"])>25
sig="GOLDEN" if g and trend else "DEATH" if d and trend else "HOLD"
results[name]={"df":df,"close":float(t["Close"]),"rsi":float(t["RSI"]),
"gv":gr.conditional_volatility,"fv":fv,"signal":sig}

# 3. Portfolio VaR
pivot=([Link]({n:raw[n]["Close"].pct_change() for n in NAMES})
.dropna())
w=[Link](len(NAMES))/len(NAMES)
pr=(pivot@w).dropna()
var95=[Link](pr,5)
cvar95=pr[pr<=var95].mean()

# 4. Statistical check
rec=[Link](63).values
_,pval=stats.ttest_1samp(rec,0)

# 5. Dashboard
fig=[Link](figsize=(16,12))

Quant Pro | Advanced Quant Finance Course | Week 5 Page 29


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

gs=[Link](2,2,hspace=0.4,wspace=0.35)

ax1=fig.add_subplot(gs[0,0])
for n,r in [Link]():
[Link](r["df"].index[-120:],r["gv"][-120:],lw=1.5,label=n)
ax1.set_title("GARCH Vol — Last 120 Days",fontweight="bold")
[Link](fontsize=7); [Link](True,alpha=0.2)

ax2=fig.add_subplot(gs[0,1]); [Link]("off")
td=[[n,f"{r['close']:.0f}",f"{r['rsi']:.1f}",f"{r['fv']:.2f}%",r["signal"]]
for n,r in [Link]()]
[Link](cellText=td,colLabels=["Stock","Price","RSI","Fcast Vol","Signal"],
loc="center",cellLoc="center")
ax2.set_title("Signal Dashboard",fontweight="bold",pad=20)

ax3=fig.add_subplot(gs[1,0])
[Link](pr*100,bins=60,color="steelblue",alpha=0.7,edgecolor="white")
[Link](var95*100,color="red",lw=2,label=f"VaR(95%): {var95*100:.2f}%")
[Link](cvar95*100,color="darkred",lw=2,linestyle="--",
label=f"CVaR: {cvar95*100:.2f}%")
ax3.set_title("Portfolio Return Distribution",fontweight="bold")
[Link](fontsize=8); [Link](True,alpha=0.2)

ax4=fig.add_subplot(gs[1,1])
tcs=results["TCS"]["df"].iloc[-252:]
[Link]([Link],tcs["Close"],color="lightgray",lw=1,label="Close")
[Link]([Link],tcs["SMA_20"],color="royalblue",lw=2,label="SMA 20")
[Link]([Link],tcs["SMA_50"],color="tomato",lw=2,label="SMA 50")
ax4.set_title("TCS — 1 Year",fontweight="bold")
[Link](fontsize=8); [Link](True,alpha=0.2)

[Link](f"Quant Dashboard {[Link]()} | p={pval:.3f} VaR={var95*100:.2f}%",


fontsize=13,fontweight="bold")
[Link]("week5_dashboard.png",dpi=150,bbox_inches="tight")
[Link]()
print("Saved: week5_dashboard.png")

Week 5 Completion Checklist


• I can explain p-value, Sharpe t-statistic, and bootstrap CI to a non-statistician without using jargon.
• I have extracted implied volatility from an option price and can explain why OTM puts always show
higher IV.
• I have fitted GARCH(1,1), read alpha and beta correctly, and used the forecast for next-day risk
estimation.
• My pipeline auto-downloads data daily via yfinance — no more manual CSV downloads ever again.
• Every backtest I run now shows gross PnL, net-of-costs PnL, and high-slippage stress scenario.
• I have run walk-forward testing and can report OOS win rate and Walk-Forward Efficiency (WFE).
• Project 3 dashboard is uploaded to GitHub with a complete README showing all metrics honestly.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 30


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

# GOLDEN RULE
You have now crossed a critical line. Weeks 1-4 were theory and demonstration. Week 5 is production.
Everything here runs on live data, with real costs, with statistical proof, validated on unseen data. That is the
difference between a student project and a professional tool.

Quant Pro | Advanced Quant Finance Course | Week 5 Page 31


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

WEEK 5 — MASTER CHEATSHEET


Statistics — [Link]
Function What It Does Example / Key Note

stats.ttest_1samp(r,0) Test if mean return != 0 t, p =


stats.ttest_1samp(returns, 0)

[Link](arr) Skewness — neg=left tail risk [Link](returns)

[Link](arr) Excess kurtosis — fat tails? [Link](returns)

[Link](arr) Normality test — p>0.05=normal stat, p = [Link](returns)

sharpe x sqrt(n) Sharpe t-stat — need >1.96 sr_t = sharpe *


[Link](n_trades)

[Link](arr,5) 5th percentile = VaR(95%) [Link](port_ret, 5)

GARCH — arch library


Function What It Does Key Note

arch_model(ret,vol="Garch") Define GARCH(1,1) Use dist="t" always for fat


tails

[Link](disp="off") Fit model silently Returns result object

result.conditional_volatility Fitted vol series In % units matching input

[Link](horizon=N) Forecast N days ahead .[Link][-1] to access

[Link]["alpha[1]"] Shock weight Typical: 0.09 for large-cap

[Link]["beta[1]"] Persistence Typical: 0.90 — very sticky

Live Data and Automation


Function What It Does Example

[Link](sym).history(period) Download OHLCV from Yahoo [Link]("[Link]").history(peri


od="2y")

[Link].tz_localize(None) Remove timezone from index Required after every yfinance


download

[Link]().[Link]("16:05") Schedule job at 4:05 PM .do(daily_job)

[Link](...) Setup file+screen logging handlers=[FileHandler,


StreamHandler]

[Link](data,f,indent=2) Save dict to readable JSON Saves alerts and signals daily

Quant Pro | Advanced Quant Finance Course | Week 5 Page 32


QUANT PRO WEEK 5 | Days 31-39

Advanced Quant Finance Course Production Foundation

Week 5 Concepts Summary


Concept Plain-English Meaning Key Number Tool

Market Regime Trending/sideways/volat ADX > 25 = trend Custom ADX function


ile — which one now?

p-value Probability result is < 0.05 = real [Link].ttest_1samp


random luck

Implied Vol Market forward vol IVR 0-100 [Link]


forecast in option
prices

Vol Skew OTM puts cost more — Skew slope BS reverse engineering
always in equity mkts

GARCH(1,1) Dynamic vol that alpha+beta < 1 arch library


updates daily with new
shocks

Live Pipeline Auto-download, compute, 4:05 PM daily yfinance + schedule


alert, log daily

Transaction Costs Real PnL after STT, RT ~0.23% Custom cost model
slippage, spread

Walk-Forward OOS validation on data WFE > 0.5 Rolling window loop
strategy never saw

Fea
Machine Learning for Finance — Done Properly

Quant Pro | Advanced Quant Finance Course | Week 5 Page 33

You might also like