statistics-timeseries — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited statistics-timeseries (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Measure | Formula | Trading Use |
|---|---|---|
| Mean (μ) | Σx / n | Average return |
| Median | Middle value | Robust to outliers; preferred for skewed returns |
| Mode | Most frequent | Price clustering levels |
| Measure | Formula | Trading Use |
|---|---|---|
| Variance (σ²) | Σ(x - μ)² / (n-1) | Risk measure |
| Std Dev (σ) | √Variance | Annualized: σ_daily × √252 |
| Range | Max - Min | Volatility proxy |
| IQR | Q3 - Q1 | Robust dispersion |
Skewness — asymmetry of distribution:
Σ((x - μ)³/σ³) / nKurtosis — tail heaviness vs. normal distribution:
Σ((x - μ)⁴/σ⁴) / n (excess = this - 3)Financial returns are NOT normally distributed. Key violations:
| Property | Normal Assumption | Reality |
|---|---|---|
| Tails | Thin, 3σ events rare | Fat tails — 5σ+ events happen regularly |
| Skewness | Symmetric (0) | Negative skew — crashes are larger than rallies |
| Clustering | Constant variance | Volatility clustering — calm periods follow calm |
| Autocorrelation | None | Returns: near zero; Volatility: high persistence |
# Simple returns
R_t = (P_t - P_{t-1}) / P_{t-1}
# Log returns (preferred for compounding, stationarity)
r_t = ln(P_t / P_{t-1}) = ln(P_t) - ln(P_{t-1})
# Annualization
Annual_Return = (1 + Daily_Return)^252 - 1 # simple
Annual_Return = Daily_Return × 252 # log returnsA stationary time series has constant statistical properties over time:
E[X_t] = μ for all tVar(X_t) = σ² for all tCov(X_t, X_{t+k}) = f(k)Why it matters: Most statistical models (ARIMA, regression) assume stationarity. Non-stationary series produce spurious correlations.
| Type | Example | Fix |
|---|---|---|
| Trend | Stock price level | First-difference or detrend |
| Unit Root | Random walk | First-difference |
| Seasonality | Monthly patterns | Seasonal differencing |
| Structural Break | Regime change | Dummy variables or subsample |
#### Augmented Dickey-Fuller (ADF)
from statsmodels.tsa.stattools import adfuller
result = adfuller(series, autolag='AIC')
# result[1] is the p-value#### KPSS Test (Kwiatkowski-Phillips-Schmidt-Shin)
#### Phillips-Perron (PP)
#### Interpretation Matrix
| ADF result | KPSS result | Conclusion |
|---|---|---|
| Reject H₀ | Don't reject H₀ | Stationary ✓ |
| Don't reject H₀ | Reject H₀ | Non-stationary (unit root) |
| Reject H₀ | Reject H₀ | Trend-stationary |
| Don't reject H₀ | Don't reject H₀ | Ambiguous |
# First differencing (removes random walk / linear trend)
d1_series = series.diff().dropna()
# Log transformation (stabilizes variance)
log_series = np.log(series)
# Log-differencing (returns series — most common for prices)
returns = np.log(series).diff().dropna()
# Seasonal differencing
seasonal_diff = series.diff(12) # monthly with annual seasonalityMeasures correlation between series and its own lagged values:
ACF(k) = Cov(X_t, X_{t-k}) / Var(X_t)Correlation at lag k after removing effects of all shorter lags.
| Pattern | ACF | PACF | Suggests |
|---|---|---|---|
| AR(p) | Decays gradually | Cuts off at lag p | AR model of order p |
| MA(q) | Cuts off at lag q | Decays gradually | MA model of order q |
| ARMA(p,q) | Decays gradually | Decays gradually | ARMA model |
| No pattern | Within bounds | Within bounds | White noise (no signal) |
Tests whether group of autocorrelations ≠ 0:
X_t = c + φ₁X_{t-1} + φ₂X_{t-2} + ... + φₚX_{t-p} + ε_tX_t = μ + ε_t + θ₁ε_{t-1} + θ₂ε_{t-2} + ... + θ_qε_{t-q}Full ARIMA(p,d,q):
ARIMA(1,1,1): ΔX_t = c + φ₁ΔX_{t-1} + θ₁ε_{t-1} + ε_tStep 1 — Check stationarity:
from statsmodels.tsa.stattools import adfuller
p_value = adfuller(series)[1]
# If p_value > 0.05, series is non-stationary → differenceStep 2 — Difference if needed (determine d):
d = 0
while adfuller(series.diff(d if d > 0 else 1).dropna())[1] > 0.05:
d += 1
stationary_series = series.diff(d).dropna() if d > 0 else seriesStep 3 — Plot ACF and PACF to identify p, q:
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
plot_acf(stationary_series, lags=40)
plot_pacf(stationary_series, lags=40)Step 4 — Fit ARIMA (or use auto_arima):
from statsmodels.tsa.arima.model import ARIMA
from pmdarima import auto_arima
# Manual fit
model = ARIMA(series, order=(p, d, q)).fit()
# Auto-select order
auto_model = auto_arima(series, seasonal=False, stepwise=True, information_criterion='aic')Step 5 — Validate residuals (should be white noise):
from statsmodels.stats.diagnostic import acorr_ljungbox
lb_test = acorr_ljungbox(model.resid, lags=[10], return_df=True)
# p_value > 0.05 → residuals are white noise ✓Step 6 — Forecast:
forecast = model.forecast(steps=5)
conf_int = model.get_forecast(steps=5).conf_int()2k - 2ln(L) — penalizes complexity; prefer lowerk·ln(n) - 2ln(L) — stronger penalty; prefer lowerStandard ARIMA assumes constant variance (homoskedasticity). Financial returns show volatility clustering — calm periods followed by turbulent ones. GARCH models this explicitly.
Return equation: r_t = μ + ε_t, where ε_t = σ_t · z_t, z_t ~ N(0,1)
Variance equation: σ²_t = ω + α·ε²_{t-1} + β·σ²_{t-1}
Constraints:
ω > 0 (long-run variance weight)
α ≥ 0 (ARCH effect — yesterday's shock)
β ≥ 0 (GARCH effect — yesterday's variance)
α + β < 1 (stationarity; sum close to 1 = high persistence)Unconditional (long-run) variance: σ²_LR = ω / (1 - α - β)
| Model | Key Feature | Use Case |
|---|---|---|
| GARCH(1,1) | Symmetric, baseline | General volatility modeling |
| EGARCH | Asymmetric, log-variance | Leverage effect (bad news > good news) |
| GJR-GARCH | Asymmetric with indicator | Separate up/down volatility |
| TGARCH (TARCH) | Threshold-based asymmetry | Regime-specific vol estimation |
| GARCH-M | Vol in mean equation | Risk-return premium |
| DCC-GARCH | Dynamic correlation | Portfolio correlation modeling |
GJR-GARCH(1,1):
σ²_t = ω + (α + γ·I_{t-1})·ε²_{t-1} + β·σ²_{t-1}
where I_{t-1} = 1 if ε_{t-1} < 0 (leverage effect indicator)from arch import arch_model
# GARCH(1,1)
am = arch_model(returns * 100, vol='Garch', p=1, q=1)
res = am.fit(disp='off')
# Get conditional volatility forecast
vol_forecast = res.forecast(horizon=5)
next_day_vol = vol_forecast.variance.values[-1][0] ** 0.5 / 100
# EGARCH
am_eg = arch_model(returns * 100, vol='EGARCH', p=1, q=1)Y = α + β·X + εR² = 1 - SS_res/SS_totimport statsmodels.api as sm
X = sm.add_constant(X_data)
model = sm.OLS(y_data, X).fit()
print(model.summary()) # alpha, beta, t-stats, R², F-statR_p - R_f = α + β·(R_m - R_f)R_i - R_f = α + β₁·(R_m - R_f) + β₂·SMB + β₃·HML + ε| Factor | Definition | Economic Rationale |
|---|---|---|
| Rm - Rf | Market excess return | Market risk premium |
| SMB (Small Minus Big) | Small-cap minus large-cap returns | Size premium (~2-3%/yr) |
| HML (High Minus Low) | High B/M minus low B/M returns | Value premium (~3-5%/yr) |
R_i - R_f = α + β₁·MKT + β₂·SMB + β₃·HML + β₄·RMW + β₅·CMA + ε| Factor | Definition |
|---|---|
| RMW (Robust Minus Weak) | High profitability minus low profitability |
| CMA (Conservative Minus Aggressive) | Low investment minus high investment |
R_i - R_f = α + β₁·MKT + β₂·SMB + β₃·HML + β₄·MOM + εpandas_datareader → famafrench data source# 252-day rolling beta estimation
rolling_betas = {}
for i in range(252, len(returns)):
window = slice(i-252, i)
X = sm.add_constant(factors[window])
betas = sm.OLS(returns[window], X).fit().params
rolling_betas[returns.index[i]] = betasWhen two non-stationary series share a common stochastic trend:
from statsmodels.tsa.stattools import coint
score, p_value, critical_values = coint(series_A, series_B)
# p < 0.05 → cointegrated (tradeable spread)
# Spread Z-score for entry/exit
spread = series_A - hedge_ratio * series_B
z_score = (spread - spread.rolling(window).mean()) / spread.rolling(window).std()
# Entry: |z| > 2.0 | Exit: |z| < 0.5 | Stop: |z| > 3.5~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.