This notebook analyzes the six univariate stylized facts of financial time series as described by Alexander McNeil and Marius Hofert in their QRM book. We examine these patterns across three different asset classes:
(U1) Return series are not iid although they show little serial correlation
(U2) Series of absolute (or squared) returns show profound serial correlation
(U3) Conditional expected returns are close to zero
(U4) Volatility (conditional standard deviation) appears to vary over time
(U5) Extreme returns appear in clusters
(U6) Return series are leptokurtic or heavy-tailed (power-like tail)
# Load required librarieslibrary(xts)
Loading required package: zoo
Attaching package: 'zoo'
The following objects are masked from 'package:base':
as.Date, as.Date.numeric
library(quantmod)
Loading required package: TTR
Registered S3 method overwritten by 'quantmod':
method from
as.zoo.data.frame zoo
library(qrmtools)library(PerformanceAnalytics)
Attaching package: 'PerformanceAnalytics'
The following object is masked from 'package:graphics':
legend
library(moments)
Attaching package: 'moments'
The following objects are masked from 'package:PerformanceAnalytics':
kurtosis, skewness
# Set options for cleaner outputoptions(warn =-1)options("getSymbols.warning4.0"=FALSE)options("getSymbols.yahoo.warning"=FALSE)
Data Collection and Preparation
# Function to safely fetch data with error handlingfetch_data <-function(symbol, name, from_date ="2015-01-01") {tryCatch({message("Fetching ", name, " data...")getSymbols(symbol, from = from_date, auto.assign =FALSE) }, error =function(e) {message("Error fetching ", name, ": ", e$message)return(NULL) })}# Fetch data for three different asset classesSP500_data <-fetch_data("^GSPC", "S&P 500") # Equity index
# Display data infoif (!is.null(SP500_data)) message("S&P 500 data: from ", start(SP500_data), " to ", end(SP500_data))
S&P 500 data: from 2015-01-02 to 2025-08-08
if (!is.null(BTC_data)) message("Bitcoin data: from ", start(BTC_data), " to ", end(BTC_data))
Bitcoin data: from 2015-01-01 to 2025-08-10
if (!is.null(EURUSD_data)) message("EUR/USD data: from ", start(EURUSD_data), " to ", end(EURUSD_data))
EUR/USD data: from 2015-01-01 to 2025-08-08
Return Calculation
# Function to process returnsprocess_returns <-function(price_data, name) {if (is.null(price_data)) {message("No data available for ", name)return(NULL) }# Calculate log-returns using closing prices returns <-diff(log(Cl(price_data)))# Remove NAs and zero returns (market closures) returns_clean <- returns[!is.na(returns) & returns !=0]message("Processed ", name, ": ", length(returns_clean), " observations")return(returns_clean)}# Process returns for each assetSP500.X <-process_returns(SP500_data, "S&P 500")
Real financial data shows clear clustering of extreme events Spacings deviate significantly from exponential distribution This contrasts with simulated iid data, confirming stylized fact U5
4.Stylized Fact (U6): Heavy Tails and Leptokurtosis
Hypothesis: Return distributions have heavier tails than normal distributions.
# Q-Q plots against normal distribution for weekly returnsn_assets <-ncol(X.w)par(mfrow =c(1, min(3, n_assets))) # Arrange plotsfor (i in1:min(3, n_assets)) {qq_plot(X.w[, i], FUN = qnorm, method ="empirical",main =paste("Q-Q Plot vs Normal:", colnames(X.w)[i]),xlab ="Normal Quantiles", ylab ="Sample Quantiles")}
# Histogram with normal and t-distribution overlayspar(mfrow =c(2, min(2, ncol(X))))for (i in1:min(ncol(X), 4)) { asset_returns <-as.numeric(X[, i]) asset_name <-colnames(X)[i]# Remove NAs and infinite values asset_returns <- asset_returns[!is.na(asset_returns) &is.finite(asset_returns)]if(length(asset_returns) <50) {message("Skipping ", asset_name, ": insufficient data")next }# Histogramhist(asset_returns, breaks =50, probability =TRUE, main =paste("Distribution:", asset_name),xlab ="Daily Returns", col ="lightblue", border ="white")# Overlay normal distribution x_seq <-seq(min(asset_returns), max(asset_returns), length =100) mu <-mean(asset_returns) sigma <-sd(asset_returns) normal_density <-dnorm(x_seq, mean = mu, sd = sigma)lines(x_seq, normal_density, col ="red", lwd =2, lty =2)# Overlay t-distribution with error handlingtryCatch({# Method 1: Try MASS fitdistr fit_t <- MASS::fitdistr(asset_returns, "t", start =list(m = mu, s = sigma, df =4)) t_density <-dt((x_seq - fit_t$estimate["m"])/fit_t$estimate["s"], df = fit_t$estimate["df"]) / fit_t$estimate["s"]lines(x_seq, t_density, col ="blue", lwd =2) legend_text <-c("Normal", "t-dist (fitted)") }, error =function(e1) {# Method 2: Use method of moments for t-distributiontryCatch({ kurt <-kurtosis(asset_returns)if(kurt >3) { df_est <-max(4, 6/(kurt -3) +4) # Simple moment estimator df_est <-min(df_est, 30) # Cap at 30 t_density <-dt(x_seq/sigma, df = df_est) / sigmalines(x_seq, t_density, col ="blue", lwd =2) legend_text <-c("Normal", paste0("t-dist (df≈", round(df_est, 1), ")")) } else { legend_text <-c("Normal", "t-dist (failed)") } }, error =function(e2) {message("Could not fit t-distribution for ", asset_name) legend_text <-c("Normal") }) })# Add legendif(exists("legend_text")) { colors <-c("red", "blue")[1:length(legend_text)] lty_vals <-c(2, 1)[1:length(legend_text)]legend("topright", legend = legend_text, col = colors, lty = lty_vals, lwd =2, cex =0.8) }# Clean upif(exists("legend_text")) rm(legend_text)}par(mfrow =c(1, 1))
Findings (U6):
All assets show excess kurtosis (> 0), indicating heavy tails Q-Q plots reveal significant departures from normality in the tails t-distribution provides better fit than normal distribution This confirms leptokurtosis across all asset classes 6. Summary and Conclusions
# Print summary of findingsprint("STYLIZED FACTS SUMMARY")
[1] "STYLIZED FACTS SUMMARY"
print("(U1) Serial correlation in returns: CONFIRMED")
[1] "(U1) Serial correlation in returns: CONFIRMED"
print(" Returns show minimal autocorrelation")
[1] " Returns show minimal autocorrelation"
print("(U2) Serial correlation in absolute returns: CONFIRMED")
[1] "(U2) Serial correlation in absolute returns: CONFIRMED"
print(" Strong autocorrelation in absolute returns (volatility clustering)")
[1] " Strong autocorrelation in absolute returns (volatility clustering)"
print("(U3) Zero mean returns: CONFIRMED")
[1] "(U3) Zero mean returns: CONFIRMED"
print(" All mean returns are close to zero")
[1] " All mean returns are close to zero"
print("(U4) Time-varying volatility: CONFIRMED")
[1] "(U4) Time-varying volatility: CONFIRMED"
print(" Clear volatility clustering visible in all series")
[1] " Clear volatility clustering visible in all series"
print("(U5) Clustering of extreme returns: CONFIRMED")
[1] "(U5) Clustering of extreme returns: CONFIRMED"
print(" Extreme events show temporal clustering")
[1] " Extreme events show temporal clustering"
print("(U6) Heavy tails/leptokurtosis: CONFIRMED")
[1] "(U6) Heavy tails/leptokurtosis: CONFIRMED"
print(" All series show excess kurtosis and heavy tails")
[1] " All series show excess kurtosis and heavy tails"
Key Takeaways:
All six stylized facts are confirmed across equity, cryptocurrency, and FX markets.
Financial returns are fundamentally non-normal and exhibit complex temporal dependencies
Risk models must account for volatility clustering, heavy tails, and extreme event clustering
These properties are remarkably consistent across different asset classes and time periods
Implications for Risk Management:
Need for GARCH-type models to capture volatility clustering (U2, U4)
Importance of extreme value theory for tail risk management (U5, U6)
Inadequacy of normal distribution assumptions in risk models (U6)
Correlation structures may vary over time requiring dynamic modeling Standard deviation systematically underestimates tail risk