Time Series → Risk → Model Validation
Python-focused interview notes
A consolidated guide from the time-series foundations we covered through ARIMA, GARCH, VaR, stress testing,
benchmarking, and validation. The emphasis is on what the Python code does, how to interpret it, and what to say in an
interview.
Core pipeline: Price data → returns → stationarity → ACF/PACF → ARIMA → residual diagnostics → volatility
clustering → GARCH → volatility forecast → VaR → VaR backtesting → stress testing → benchmarking →
model-validation conclusion.
1. Time-Series Foundations
1.1 What is a time series?
A time series is data observed in time order: prices, returns, interest rates, volatility, trading volume, etc. The ordering
matters because today's observation can depend on previous observations.
Financial example: daily stock prices are a time series; daily returns are usually more suitable for statistical modelling
because prices are often non-stationary while returns are closer to stationary.
1.2 Price → return
prices = [Link]([100, 102, 101, 105, 103, 108, 110])
returns = prices.pct_change()
pct_change() calculates (today's price / previous price) - 1. The first observation is NaN because there is no previous
price.
1.3 Mean and volatility
mean_return = [Link]()
volatility = [Link]()
annual_vol = [Link]() * [Link](252)
Standard deviation is a basic measure of return dispersion. For daily returns, multiplying by sqrt(252) annualizes
volatility under the usual independent-return scaling assumption.
1.4 Rolling statistics
rolling_mean = [Link](20).mean()
rolling_vol = [Link](20).std()
A 20-observation rolling window uses the latest 20 observations, calculates the statistic, moves forward one
observation, and repeats. Rolling volatility is useful for seeing how risk changes over time.
1.5 Autocorrelation
lag1_corr = [Link](lag=1)
lag2_corr = [Link](lag=2)
Autocorrelation measures the relationship between a series and its lagged values. Lag 1 compares today with
yesterday; lag 2 compares today with two periods ago.
2. ACF and PACF
from [Link] import plot_acf, plot_pacf
plot_acf([Link](), lags=20)
plot_pacf([Link](), lags=20)
ACF: autocorrelation at different lags. PACF: partial autocorrelation at a lag after accounting for intermediate lags.
Rule of thumb for ARIMA identification: PACF can help suggest the AR order p; ACF can help suggest the MA order q.
In practice, candidate models should be compared and validated rather than selected from a plot alone.
3. Stationarity and ADF
3.1 What stationary means
A stationary series has stable statistical properties over time, such as a stable mean, variance, and autocovariance
structure. It does not mean the values stop changing.
from [Link] import adfuller
result = adfuller([Link]())
print("ADF statistic:", result[0])
print("p-value:", result[1])
For the ADF test, the null hypothesis is a unit root / non-stationarity. A p-value below a chosen significance level such as
0.05 provides evidence against that null. A high p-value means we do not have enough evidence to reject it; it does not
prove stationarity.
3.2 Differencing
diff = [Link]()
First differencing calculates today's value minus yesterday's value. In ARIMA, d is the number of differencing operations
applied to make the series suitable for modelling.
4. ARIMA
ARIMA models the conditional mean dynamics of a time series. ARIMA(p,d,q): p = autoregressive order, d = differencing
order, q = moving-average order.
from [Link] import ARIMA
model = ARIMA(returns, order=(1, 0, 1))
result = [Link]()
forecast = [Link](steps=5)
AR terms use past values; MA terms use past model errors; differencing handles non-stationarity. For interview
purposes, remember: ARIMA is mainly about mean/return dynamics, while GARCH is about changing variance/volatility.
4.1 Train/test evaluation
train = [Link][:-3]
test = [Link][-3:]
model = ARIMA(train, order=(1, 0, 1))
result = [Link]()
forecast = [Link](steps=len(test))
from [Link] import mean_squared_error
rmse = [Link](mean_squared_error(test, forecast))
Do not judge a forecasting model only on the data used to fit it. Holdout or rolling out-of-sample testing gives a more
realistic estimate of predictive performance.
4.2 Residual diagnostics
residuals = [Link]
from [Link] import acorr_ljungbox
lb = acorr_ljungbox(residuals, lags=[10], return_df=True)
print(lb)
Residual = actual - fitted value. A good mean model should leave residuals that are approximately uncorrelated.
Ljung-Box tests whether remaining autocorrelation is statistically significant.
5. Volatility Clustering and GARCH
Financial returns often show volatility clustering: large movements tend to be followed by large movements and calm
periods by calm periods. GARCH models this time-varying conditional variance.
5.1 GARCH(1,1)
from arch import arch_model
returns_pct = [Link]() * 100
model = arch_model(
returns_pct,
mean="Constant",
vol="GARCH",
p=1,
q=1
)
result = [Link](disp="off")
print([Link]())
In a GARCH(1,1), p=1 represents one lag of the squared innovation/shock and q=1 represents one lag of conditional
variance. The parameters are commonly interpreted as: omega = baseline variance component, alpha = reaction to
recent shocks, beta = persistence of volatility.
5.2 Forecast volatility
forecast = [Link](horizon=5)
variance = [Link][-1]
volatility = [Link](variance)
The model forecasts conditional variance. Taking the square root converts variance into volatility.
5.3 GARCH diagnostics
std_resid = result.std_resid
from [Link] import acorr_ljungbox, het_arch
lb = acorr_ljungbox(std_resid.dropna(), lags=[10], return_df=True)
arch_test = het_arch(std_resid.dropna())
Standardized residuals should not retain obvious serial dependence. The ARCH test checks whether conditional
heteroskedasticity remains in the residuals. Significant remaining effects may indicate that the volatility model is not fully
adequate.
6. From GARCH to VaR
This is the key bridge between time series, risk, and model validation. A volatility model can provide a conditional
volatility forecast, which can then be used to construct a conditional VaR estimate.
A simplified normal VaR approach is: predicted loss threshold = predicted mean + z-quantile × predicted volatility, with
sign conventions handled consistently. For return-based VaR, the 5% lower-tail quantile is commonly used for 95%
VaR.
from [Link] import norm
mu_forecast = 0.0
sigma_forecast = float([Link]([Link][0]))
z05 = [Link](0.05)
var_return = mu_forecast + z05 * sigma_forecast
print(var_return)
Because z05 is negative, the return VaR threshold is negative under the return convention. For example, -0.03 means a
-3% return threshold. Always state your convention because some risk reports quote VaR as a positive loss amount.
7. Historical / Monte Carlo VaR
7.1 Historical quantile VaR
var95 = [Link](0.05)
This finds the 5th percentile of historical returns. It is non-parametric in the sense that it does not assume a normal
return distribution. Conceptually, the observations are ordered and the percentile is obtained from their empirical
distribution, with interpolation depending on the implementation.
7.2 Expected Shortfall
es95 = returns[returns <= var95].mean()
Expected Shortfall is the average return in the tail beyond the VaR threshold. If VaR is -2%, ES might be -3% because it
averages the observations at or below -2%. It is therefore a measure of the severity of losses in the tail, not just the
cutoff.
7.3 Monte Carlo VaR
S0 = 100
mu = 0.08
sigma = 0.20
T = 1
N = 252
M = 10000
dt = T / N
Z = [Link](0, 1, (M, N))
S = [Link]((M, N + 1))
S[:, 0] = S0
for t in range(1, N + 1):
S[:, t] = S[:, t-1] * [Link](
(mu - 0.5 * sigma**2) * dt
+ sigma * [Link](dt) * Z[:, t-1]
)
final_prices = S[:, -1]
simulated_returns = final_prices / S0 - 1
var95 = [Link](simulated_returns, 0.05)
es95 = simulated_returns[simulated_returns <= var95].mean()
dt is the time increment; with T=1 year and N=252 steps, dt=1/252. Z is a matrix of standard-normal shocks. Each row is
a simulated path and each column is a time step. S stores the simulated prices.
8. VaR Backtesting
Backtesting asks whether the realized outcomes are consistent with the VaR model's forecasts.
8.1 Exception / breach
exceptions = actual_returns < var_forecast
num_exceptions = [Link]()
exception_rate = [Link]()
Under a negative-return VaR convention, an exception occurs when the actual return is more negative than the VaR
threshold. Example: actual = -5%, VaR = -2% → -5% < -2%, so it is a breach.
For a 95% VaR model, the expected unconditional exception frequency is approximately 5%, assuming the model is
correctly calibrated and the observations meet the relevant assumptions.
8.2 Kupiec test
Kupiec's Proportion of Failures test checks whether the observed number of exceptions is consistent with the expected
exception probability. It focuses on the unconditional coverage / frequency of breaches.
from [Link] import chi2
n = len(actual_returns)
x = [Link]()
p = 0.05
phat = x / n
lr_pof = -2 * (
(n-x) * [Link]((1-p)/(1-phat))
+ x * [Link](p/phat)
)
p_value = 1 - [Link](lr_pof, df=1)
In production code, handle x=0 or x=n carefully because logarithms can become undefined. The practical interpretation
is: a small p-value provides evidence that the observed exception frequency differs from the expected rate.
8.3 Christoffersen test
Kupiec checks frequency, but not whether exceptions are clustered. Christoffersen's independence test checks whether
exceptions occur independently rather than arriving in clusters. This matters because a model can have the right total
number of exceptions but still fail badly during volatility episodes.
For interview purposes: Kupiec = unconditional coverage/frequency; Christoffersen = independence/coverage
dynamics; conditional coverage combines both ideas.
9. Stress Testing
Stress testing deliberately applies extreme but plausible conditions and measures the impact. There is no single
universal formula.
Historical stress: replay actual crisis-like market moves. Hypothetical stress: define a scenario such as stock -20%,
volatility +50%, rates +2%. Sensitivity/shock: move one risk factor at a time.
S0 = 100
stress_price = 80
position = 100
pnl = (stress_price - S0) * position
print(pnl)
Here P&L; = (new price - old price) × position = (80 - 100) × 100 = -2,000. Negative P&L; means loss; positive P&L;
means profit.
scenarios = {
"Mild": -0.10,
"Severe": -0.20,
"Extreme": -0.30
}
for name, shock in [Link]():
stressed_price = S0 * (1 + shock)
pnl = (stressed_price - S0) * position
print(name, pnl)
Liquidity stress is different: it asks what happens when the portfolio becomes difficult or expensive to liquidate. You
might stress bid-ask spreads, trading volume, market depth, price impact, or liquidation time.
10. Benchmarking
Benchmarking compares the model against a simpler, alternative, or established model. The question is: does the
complex model provide meaningful improvement?
actual_vol = [Link]([0.20, 0.25, 0.22, 0.30])
model_vol = [Link]([0.21, 0.24, 0.23, 0.28])
benchmark_vol = [Link]([0.20, 0.20, 0.20, 0.20])
model_mse = [Link]((actual_vol - model_vol)**2)
benchmark_mse = [Link]((actual_vol - benchmark_vol)**2)
Lower out-of-sample MSE indicates lower average squared forecast error for this particular evaluation sample.
Benchmarking should consider more than one metric where appropriate and should respect the model's intended use.
11. Model Validation
Model validation is not simply 'run the model and see the answer'. It is an independent assessment of whether the
model is conceptually sound, implemented correctly, performs adequately, and is appropriate for its intended use.
Typical lifecycle: development → independent validation → benchmarking → backtesting → stress testing →
sensitivity analysis → documentation/approval → ongoing monitoring.
11.1 What to check in Python
1. Data quality: missing values, duplicates, incorrect dates, outliers, stale observations. 2. Implementation correctness:
unit tests and known-value checks. 3. Statistical assumptions: stationarity, residual behavior, distributional assumptions.
4. Performance: out-of-sample forecast accuracy and risk backtesting. 5. Stability: performance across time periods and
stress regimes. 6. Benchmarking: compare with simpler alternatives. 7. Monitoring: track exceptions, drift, parameter
changes, and performance.
11.2 Basic data checks
[Link]().sum()
[Link]().sum()
[Link]()
[Link]
These are simple but useful interview-level checks. A model validator should not start with sophisticated modelling while
ignoring data quality.
11.3 Train/test split
train = [Link][:-100]
test = [Link][-100:]
Keep the future period out of training. For time series, do not randomly shuffle observations like a standard IID
machine-learning split unless there is a specific reason. Time ordering matters.
12. Interview Python Questions You Should Be Ready For
Question Interview answer
Why use returns instead of prices? Prices are often non-stationary and can have trends; returns are generally more
suitable for modelling changes and risk.
What is rolling volatility? Standard deviation calculated over a moving window, allowing volatility to vary through
time.
What does ADF test? It tests the null of a unit root. A small p-value gives evidence against non-stationarity.
ARIMA vs GARCH? ARIMA models mean/serial dynamics; GARCH models conditional variance/volatility.
What is a VaR exception? An actual loss worse than the VaR threshold under the chosen sign convention.
Why backtest VaR? To assess whether the model's realized exception behavior is consistent with its stated
confidence level.
Kupiec vs Christoffersen? Kupiec tests exception frequency; Christoffersen tests independence/clustering and
can be combined for conditional coverage.
Question Interview answer
What is stress testing? Applying extreme but plausible scenarios and measuring the resulting impact.
What is benchmarking? Comparing the model with an alternative or simpler model to assess relative
performance.
Why residual diagnostics? To determine whether the fitted model has left systematic structure that it should have
captured.
13. One End-to-End Python Workflow to Remember
# 1. Load / inspect data
[Link]().sum()
[Link]()
# 2. Calculate returns
returns = prices.pct_change().dropna()
# 3. Check stationarity
adfuller(returns)
# 4. Inspect dependence
plot_acf(returns)
plot_pacf(returns)
# 5. Fit mean model
arima = ARIMA(returns, order=(1, 0, 1)).fit()
# 6. Diagnose residuals
acorr_ljungbox([Link], lags=[10], return_df=True)
# 7. Fit volatility model
garch = arch_model(returns * 100, vol="GARCH", p=1, q=1).fit(disp="off")
# 8. Forecast volatility
garch_forecast = [Link](horizon=1)
# 9. Produce a VaR estimate
# Use the forecast mean/variance with a consistent return/loss convention.
# 10. Backtest
exceptions = actual_returns < var_forecast
exception_rate = [Link]()
# 11. Stress test
# Apply predefined shocks and calculate P&L.
# 12. Benchmark
# Compare forecast/risk metrics against a simpler model.
# 13. Validate
# Combine diagnostics, out-of-sample results, stress behavior,
# benchmark comparison, and limitations.
14. Final Mental Map
Time series: understand how observations behave through time.
ARIMA: model the conditional mean / serial dependence.
GARCH: model changing conditional variance / volatility.
VaR: estimate a loss threshold at a chosen confidence level.
ES: average the losses beyond the VaR threshold.
Backtesting: compare predicted risk with realized outcomes.
Stress testing: ask what happens under extreme but plausible scenarios.
Benchmarking: compare against alternatives.
Model validation: assess assumptions, implementation, performance, stability, and suitability using all of the above.
Interview chain: Returns → stationarity → ARIMA/GARCH → volatility forecast → VaR → exceptions →
Kupiec/Christoffersen → stress testing → benchmarking → validation conclusion.