Skip to main content Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill financial-time-seriesThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... name financial-time-series description Use this Skill for financial time series analysis: GARCH volatility modeling, cointegration, pairs trading, factor models, and risk decomposition.
tags ["finance","time-series","garch","volatility","quantitative-finance"] version 1.0.0 authors [{"name":"Rosetta Skills Contributors","github":"@xjtulyc"}] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["numpy>=1.24","pandas>=2.0","scipy>=1.11","statsmodels>=0.14","arch>=6.2","matplotlib>=3.7","yfinance>=0.2"]} last_updated 2026-03-17 status stable
Financial Time Series Analysis
One-line summary : Analyze financial time series with GARCH volatility models, cointegration tests, pairs trading signals, Fama-French factor regressions, and rolling risk decomposition.
When to Use This Skill
When modeling volatility clustering with GARCH/EGARCH models
When testing and exploiting cointegration for pairs trading
When running Fama-French 3/5 factor regressions for alpha/beta
When computing rolling VaR, expected shortfall, and drawdowns
When detecting structural breaks in financial time series
When building momentum and mean-reversion trading signals
Trigger keywords : GARCH, volatility, cointegration, pairs trading, Fama-French, factor model, VaR, expected shortfall, momentum, mean reversion, financial time series, ARCH, unit root, ADF test, Kalman filter
Background & Key Concepts
GARCH(p,q) Volatility
$$
r_t = \mu + \epsilon_t, \quad \epsilon_t = \sigma_t z_t, \quad z_t \sim \mathcal{N}(0,1)
$$
$$
\sigma_t^2 = \omega + \sum_{i=1}^q \alpha_i \epsilon_{t-i}^2 + \sum_{j=1}^p \beta_j \sigma_{t-j}^2
$$
Stationarity: $\sum \alpha_i + \sum \beta_j < 1$.
Cointegration (Engle-Granger)
Two I(1) series $X_t, Y_t$ are cointegrated if $\exists \beta: Y_t - \beta X_t = u_t$ where $u_t$ is I(0). The spread $u_t$ is mean-reverting and exploitable for pairs trading.
Fama-French 3-Factor Model
$$
R_i - R_f = \alpha_i + \beta_{MKT}(R_m - R_f) + \beta_{SMB} \cdot SMB + \beta_{HML} \cdot HML + \epsilon_i
$$
Environment Setup
Install Dependencies
pip install numpy>=1.24 pandas>=2.0 scipy>=1.11 statsmodels>=0.14 \
arch >=6.2 matplotlib>=3.7 yfinance>=0.2
Verify Installation
import numpy as np
import pandas as pd
from arch import arch_model
from statsmodels.tsa.stattools import adfuller
np.random.seed(42 )
returns = np.random.randn(500 ) * 0.01
am = arch_model(returns, vol= , p= , q= )
res = am.fit(disp= )
( )
'Garch'
1
1
'off'
print
f"arch version OK; GARCH omega={res.params['omega' ]:.6 f} "
Core Workflow
Step 1: GARCH Volatility Modeling import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from arch import arch_model
import yfinance as yf
print ("Downloading S&P 500 data..." )
try :
spy = yf.download("SPY" , start="2010-01-01" , end="2024-12-31" , progress=False )
prices = spy["Adj Close" ].dropna()
except Exception:
np.random.seed(0 )
dates = pd.date_range("2010-01-01" , periods=3780 , freq='B' )
omega, alpha, beta = 5e-6 , 0.09 , 0.88
n = len (dates)
sigma2 = np.zeros(n); eps = np.zeros(n)
sigma2[0 ] = omega / (1 - alpha - beta)
for t in range (1 , n):
sigma2[t] = omega + alpha * eps[t-1 ]**2 + beta * sigma2[t-1 ]
eps[t] = np.sqrt(sigma2[t]) * np.random.randn()
prices = pd.Series(100 * np.exp(np.cumsum(eps)), index=dates, name="SPY" )
returns = 100 * prices.pct_change().dropna()
am = arch_model(returns, vol='Garch' , p=1 , q=1 , dist='Normal' )
res = am.fit(disp='off' )
print (res.summary())
omega_hat = res.params['omega' ]
alpha_hat = res.params['alpha[1]' ]
beta_hat = res.params['beta[1]' ]
persistence = alpha_hat + beta_hat
half_life = np.log(0.5 ) / np.log(persistence)
print (f"\nVolatility persistence: {persistence:.4 f} " )
print (f"Shock half-life: {half_life:.1 f} trading days" )
print (f"Long-run annualized vol: {np.sqrt(omega_hat/(1 -persistence)*252 ):.2 f} %" )
cond_vol = res.conditional_volatility
annual_vol = cond_vol * np.sqrt(252 )
fig, axes = plt.subplots(3 , 1 , figsize=(14 , 10 ), sharex=True )
axes[0 ].plot(returns.index, returns, 'k-' , linewidth=0.4 , alpha=0.7 )
axes[0 ].set_title("Daily Returns (%)" )
axes[0 ].set_ylabel("Return (%)" )
axes[0 ].grid(True , alpha=0.3 )
axes[1 ].plot(returns.index, annual_vol, color='#e74c3c' , linewidth=1 )
axes[1 ].fill_between(returns.index, 0 , annual_vol, alpha=0.2 , color='#e74c3c' )
axes[1 ].set_ylabel("Annualized Vol (%)" )
axes[1 ].set_title("GARCH(1,1) Conditional Volatility" )
axes[1 ].grid(True , alpha=0.3 )
std_resid = res.std_resid
axes[2 ].plot(returns.index, std_resid, 'b-' , linewidth=0.4 , alpha=0.7 )
axes[2 ].axhline(0 , color='gray' , linewidth=0.5 )
axes[2 ].axhline(3 , color='red' , linestyle='--' , linewidth=1 )
axes[2 ].axhline(-3 , color='red' , linestyle='--' , linewidth=1 )
axes[2 ].set_ylabel("Std. Residuals" ); axes[2 ].set_xlabel("Date" )
axes[2 ].set_title("Standardized Residuals (should be ~N(0,1))" )
axes[2 ].grid(True , alpha=0.3 )
plt.suptitle("GARCH(1,1) Volatility Model — SPY" , y=1.01 )
plt.tight_layout()
plt.savefig("garch_volatility.png" , dpi=150 )
plt.show()
Step 2: Cointegration and Pairs Trading import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller, coint
from statsmodels.regression.linear_model import OLS
import statsmodels.api as sm
np.random.seed(7 )
n = 1000
dates = pd.date_range("2020-01-01" , periods=n, freq='B' )
trend = np.cumsum(np.random.randn(n) * 0.5 )
spread_true = np.random.randn(n) * 2.0
price_X = 100 + trend + np.random.randn(n) * 0.3
price_Y = 50 + 0.6 * trend + spread_true + np.random.randn(n) * 0.3
X = pd.Series(price_X, index=dates, name="Stock_X" )
Y = pd.Series(price_Y, index=dates, name="Stock_Y" )
for name, series in [("Stock X" , X), ("Stock Y" , Y)]:
adf_result = adfuller(series, autolag='AIC' )
print (f"ADF test — {name} : stat={adf_result[0 ]:.4 f} , p={adf_result[1 ]:.4 f} "
f"({'I(1)' if adf_result[1 ] > 0.05 else 'stationary' } )" )
score, pvalue, _ = coint(X, Y)
print (f"\nCointegration test: stat={score:.4 f} , p={pvalue:.4 f} " )
print ("Cointegrated!" if pvalue < 0.05 else "NOT cointegrated (p>0.05)" )
X_const = sm.add_constant(X)
ols_res = OLS(Y, X_const).fit()
beta_hedge = ols_res.params["Stock_X" ]
alpha_const = ols_res.params["const" ]
print (f"\nHedge ratio β = {beta_hedge:.4 f} (Y ≈ {alpha_const:.2 f} + {beta_hedge:.4 f} ·X)" )
spread = Y - beta_hedge * X - alpha_const
roll_mean = spread.rolling(window=60 ).mean()
roll_std = spread.rolling(window=60 ).std()
z_score = (spread - roll_mean) / roll_std
entry_long = z_score < -2.0
entry_short = z_score > +2.0
exit_signal = (z_score.abs () < 0.5 )
fig, axes = plt.subplots(3 , 1 , figsize=(14 , 10 ), sharex=True )
axes[0 ].plot(X.index, X, label="Stock X" , linewidth=0.8 )
axes[0 ].plot(Y.index, Y, label="Stock Y" , linewidth=0.8 )
axes[0 ].set_title("Cointegrated Price Pair" ); axes[0 ].legend(); axes[0 ].grid(alpha=0.3 )
axes[1 ].plot(spread.index, spread, 'purple' , linewidth=0.8 )
axes[1 ].plot(roll_mean.index, roll_mean, 'gray' , linestyle='--' , linewidth=1 , label='60d mean' )
axes[1 ].set_title("Spread (Y - βX - α)" ); axes[1 ].legend(); axes[1 ].grid(alpha=0.3 )
axes[2 ].plot(z_score.index, z_score, 'b-' , linewidth=0.7 )
axes[2 ].axhline( 2 , color='red' , linestyle='--' , linewidth=1.5 , label='Short (z>+2)' )
axes[2 ].axhline(-2 , color='green' , linestyle='--' , linewidth=1.5 , label='Long (z<-2)' )
axes[2 ].axhline( 0.5 , color='gray' , linestyle=':' , linewidth=1 )
axes[2 ].axhline(-0.5 , color='gray' , linestyle=':' , linewidth=1 )
axes[2 ].fill_between(z_score.index, -2 , z_score, where=z_score<-2 , alpha=0.3 , color='green' )
axes[2 ].fill_between(z_score.index, z_score, 2 , where=z_score>2 , alpha=0.3 , color='red' )
axes[2 ].set_title("Spread Z-Score — Trading Signals" )
axes[2 ].set_xlabel("Date" ); axes[2 ].legend(); axes[2 ].grid(alpha=0.3 )
plt.tight_layout()
plt.savefig("pairs_trading.png" , dpi=150 )
plt.show()
long_trades = entry_long.sum ()
short_trades = entry_short.sum ()
print (f"\nLong entries: {long_trades} , Short entries: {short_trades} " )
Step 3: Factor Model Regression import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
np.random.seed(42 )
n = 252 * 5
dates = pd.date_range("2019-01-01" , periods=n, freq='B' )
mkt_premium = np.random.randn(n) * 0.01 + 0.0003
smb = np.random.randn(n) * 0.005
hml = np.random.randn(n) * 0.004
rf = np.full(n, 0.02 /252 )
true_alpha = 0.02 /252
true_beta_mkt = 1.15
true_beta_smb = 0.3
true_beta_hml = -0.2
asset_excess = (true_alpha
+ true_beta_mkt * mkt_premium
+ true_beta_smb * smb
+ true_beta_hml * hml
+ np.random.randn(n) * 0.008 )
factors = pd.DataFrame({
"Mkt-RF" : mkt_premium,
"SMB" : smb,
"HML" : hml,
"Rf" : rf,
}, index=dates)
y = pd.Series(asset_excess, index=dates, name="Asset excess return" )
X = sm.add_constant(factors[["Mkt-RF" , "SMB" , "HML" ]])
model = sm.OLS(y, X)
result = model.fit(cov_type='HAC' , cov_kwds={'maxlags' : 21 })
print (result.summary())
alpha_daily = result.params["const" ]
alpha_annual = alpha_daily * 252
t_alpha = result.tvalues["const" ]
print (f"\nAnnualized alpha: {alpha_annual*100 :.3 f} % (t={t_alpha:.2 f} )" )
print (f"Market beta: {result.params['Mkt-RF' ]:.4 f} (true: {true_beta_mkt} )" )
print (f"SMB beta: {result.params['SMB' ]:.4 f} (true: {true_beta_smb} )" )
print (f"HML beta: {result.params['HML' ]:.4 f} (true: {true_beta_hml} )" )
print (f"R²: {result.rsquared:.4 f} " )
roll_window = 63
def rolling_ols_beta (y_series, X_df, window ):
betas = {}
for col in X_df.columns:
betas[col] = [np.nan] * (window - 1 )
for end in range (window, len (y_series)+1 ):
y_w = y_series.iloc[end-window:end]
X_w = sm.add_constant(X_df.iloc[end-window:end])
try :
b = sm.OLS(y_w, X_w).fit().params[col]
except Exception:
b = np.nan
betas[col].append(b)
return pd.DataFrame(betas, index=y_series.index)
rolling_betas = rolling_ols_beta(y, factors[["Mkt-RF" , "SMB" , "HML" ]], roll_window)
fig, axes = plt.subplots(3 , 1 , figsize=(13 , 8 ), sharex=True )
colors = ['#e74c3c' , '#3498db' , '#2ecc71' ]
for ax, col, color, truth in zip (axes, ["Mkt-RF" ,"SMB" ,"HML" ], colors, [true_beta_mkt, true_beta_smb, true_beta_hml]):
ax.plot(rolling_betas.index, rolling_betas[col], color=color, linewidth=1.5 )
ax.axhline(truth, color='black' , linestyle='--' , linewidth=1 , alpha=0.7 , label=f'True β={truth} ' )
ax.set_ylabel(f"β ({col} )" ); ax.legend(loc='upper right' ); ax.grid(alpha=0.3 )
axes[-1 ].set_xlabel("Date" )
plt.suptitle("Rolling Fama-French Factor Exposures (63-day window)" )
plt.tight_layout()
plt.savefig("factor_model.png" , dpi=150 )
plt.show()
Advanced Usage
EGARCH and Fat-Tailed Distributions import numpy as np
from arch import arch_model
np.random.seed(0 )
returns = np.random.randn(1000 ) * 0.01
am = arch_model(returns * 100 , vol='EGARCH' , p=1 , q=1 , dist='StudentsT' )
res = am.fit(disp='off' )
print (res.summary())
nu = res.params.get('nu' , 'N/A' )
print (f"\nStudent's t d.o.f.: {nu} " )
print (f"EGARCH gamma (leverage): {res.params.get('gamma[1]' , 'N/A' ):.4 f} " )
Troubleshooting
Error: ConvergenceWarning in GARCH fit Cause : Poor starting values or very short series.
am = arch_model(returns, vol='Garch' , p=1 , q=1 )
res = am.fit(starting_values=np.array([0.01 , 0.05 , 0.90 ]), disp='off' , options={'maxiter' : 500 })
Error: ValueError: NaN values in ADF test Fix : Drop NaN before testing:
series = series.dropna()
result = adfuller(series, autolag='AIC' )
Spurious regression warning Always difference I(1) series before OLS, or use proper cointegration:
from statsmodels.tsa.stattools import adfuller
p_level = adfuller(X)[1 ]
p_diff = adfuller(X.diff().dropna())[1 ]
print (f"I(1) if p_level>0.05 and p_diff<0.05: {p_level:.3 f} , {p_diff:.3 f} " )
Version Compatibility Package Tested versions Notes arch 6.2, 6.3 arch_model API stablestatsmodels 0.14 coint requires statsmodels ≥ 0.11yfinance 0.2.x Adj Close column name may change
External Resources
Official Documentation
Key Papers
Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity . Journal of Econometrics.
Engle, R.F. & Granger, C.W.J. (1987). Co-integration and error correction . Econometrica.
Examples
Example 1: Value-at-Risk via Historical Simulation import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(42 )
from scipy.stats import t as t_dist
returns = t_dist.rvs(df=5 , size=1000 ) * 0.01
confidence = 0.99
var_99 = np.percentile(returns, (1 -confidence)*100 )
es_99 = returns[returns <= var_99].mean()
print (f"1-day VaR (99%): {var_99*100 :.3 f} %" )
print (f"1-day ES (99%): {es_99*100 :.3 f} %" )
fig, ax = plt.subplots(figsize=(9 , 4 ))
ax.hist(returns*100 , bins=50 , color='steelblue' , edgecolor='white' , linewidth=0.5 , alpha=0.7 )
ax.axvline(var_99*100 , color='red' , linewidth=2 , linestyle='--' , label=f'VaR(99%)={var_99*100 :.2 f} %' )
ax.axvline(es_99*100 , color='orange' , linewidth=2 , linestyle='--' , label=f'ES(99%)={es_99*100 :.2 f} %' )
ax.set_xlabel("Daily Return (%)" ); ax.set_title("Portfolio Return Distribution — VaR / ES" )
ax.legend(); ax.grid(alpha=0.3 )
plt.tight_layout(); plt.savefig("var_es.png" , dpi=150 ); plt.show()
Example 2: Hurst Exponent (Long Memory Detection) import numpy as np
def hurst_exponent (ts, max_lag=100 ):
"""Estimate Hurst exponent via R/S analysis."""
lags = range (2 , max_lag)
tau = []
for lag in lags:
n_blocks = len (ts) // lag
if n_blocks < 2 :
break
rs_values = []
for i in range (n_blocks):
block = ts[i*lag:(i+1 )*lag]
mean_b = np.mean(block)
dev = np.cumsum(block - mean_b)
R = dev.max () - dev.min ()
S = np.std(block, ddof=1 )
if S > 0 :
rs_values.append(R/S)
if rs_values:
tau.append((lag, np.mean(rs_values)))
lags_arr = np.array([x[0 ] for x in tau])
rs_arr = np.array([x[1 ] for x in tau])
H, _ = np.polyfit(np.log(lags_arr), np.log(rs_arr), 1 )
return H
np.random.seed(1 )
rw = np.cumsum(np.random.randn(1000 ))
trending = np.cumsum(np.random.randn(1000 ) + 0.02 )
print (f"Hurst(random walk): H = {hurst_exponent(rw):.4 f} (expected ≈0.5)" )
print (f"Hurst(with trend): H = {hurst_exponent(trending):.4 f} (expected >0.5)" )
Last updated: 2026-03-17 | Maintainer: @xjtulyc
Issues: GitHub Issues
More from this repository
Related occupations SOC
Based on SOC occupation classification