Statistical Analysis·Return Distribution Analysis·Intermediate

Skewness Kurtosis Analysis

Analyze the third and fourth statistical moments of return distributions - skewness measuring asymmetry and excess kurtosis measuring tail thickness relative to a normal distribution - as critical inputs for risk management models and option pricing frameworks that assume non-normal returns.

distributionsquant-analysis

Skewness & Kurtosis Analysis — Statistical Analysis

Category: Statistical Analysis | Subcategory: Distributions


What This Notebook Does

Skewness and kurtosis are the third and fourth standardised moments of a return distribution:

Skewness  = E[(X-μ)³] / σ³   — measures asymmetry
Kurtosis  = E[(X-μ)⁴] / σ⁴ - 3  — measures tail fatness (excess kurtosis)

Why traders care:

  • Positive skewness: more small losses, occasional large gains (preferred)
  • Negative skewness: more small gains, occasional large losses (most options sellers)
  • High kurtosis: fat tails — standard deviation underestimates actual risk

This notebook:

  1. Computes rolling skewness and kurtosis over time
  2. Identifies regime shifts in skewness (bearish: negative skew spikes)
  3. Compares skewness across asset classes
  4. Analyses the relationship between skewness and forward returns
  5. Builds a skewness-based timing indicator
  6. Exports the analysis
[ ]
!pip install numpy pandas matplotlib seaborn scipy --quiet
[ ]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings

warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')
Imports ready.

Section 1 — Configuration

This section defines the configuration parameters for the analysis, such as the rolling window size, simulation days, and forward prediction days.

[ ]
ROLLING_WINDOW  = 30
SIMULATION_DAYS = 1460
FORWARD_DAYS    = 10   # days to look ahead for predictive analysis
print('Config ready.')
Config ready.

Section 2 — Data

This section generates synthetic price and return data with varying skewness regimes for the analysis.

[ ]
def generate_skewed_returns(n=1460, seed=42):
    rng = np.random.default_rng(seed)
    t   = np.arange(n)
    # Cycle from positive to negative skew and back
    skew_regime = np.sin(2 * np.pi * t / 365) * 0.8
    bull = 0.5 * (skew_regime + 1)
    bear = 1 - bull

    # Mix of right-skewed (bull) and left-skewed (bear) distributions
    right = rng.exponential(0.015, n)  # positively skewed
    left  = -rng.exponential(0.018, n) # negatively skewed
    base  = rng.normal(0, 0.012, n)
    rets  = bull * right + bear * left + 0.3 * base
    rets += rng.normal(0, 0.005, n)  # small noise

    price = 30_000 * np.cumprod(1 + rets)
    idx   = pd.date_range('2020-01-01', periods=n, freq='D')
    return pd.DataFrame({'price': price, 'return': rets * 100,
                          'true_skew': skew_regime}, index=idx)

df = generate_skewed_returns(SIMULATION_DAYS)
print(f'Global skewness: {df["return"].skew():.3f}')
print(f'Global kurtosis: {df["return"].kurt():.3f}')
Global skewness: -0.519
Global kurtosis: 6.357

Section 3 — Rolling Moments

This section calculates rolling skewness, kurtosis, and standard deviation of the returns, as well as forward returns for predictive analysis.

[ ]
df['rolling_skew'] = df['return'].rolling(ROLLING_WINDOW).skew()
df['rolling_kurt'] = df['return'].rolling(ROLLING_WINDOW).kurt()
df['rolling_std']  = df['return'].rolling(ROLLING_WINDOW).std()

# Forward return (for predictive analysis)
df['fwd_return'] = df['price'].pct_change(FORWARD_DAYS).shift(-FORWARD_DAYS) * 100

# Correlation between rolling skew and forward return
skew_fwd_corr = df[['rolling_skew', 'fwd_return']].dropna().corr().iloc[0, 1]
print(f'Rolling skew vs {FORWARD_DAYS}d forward return correlation: {skew_fwd_corr:.3f}')
Rolling skew vs 10d forward return correlation: 0.690

Section 4 — Skewness as Timing Signal

This section explores how rolling skewness can be used as a timing signal by comparing average forward returns during periods of positive and negative skew.

[ ]
# Positive skewness: upside surprises dominate → bullish lean
# Negative skewness: downside surprises dominate → de-risk
df['skew_signal'] = np.sign(df['rolling_skew'])  # 1=positive skew, -1=negative skew

pos_skew_fwd = df[df['rolling_skew'] > 0.5]['fwd_return'].dropna().mean()
neg_skew_fwd = df[df['rolling_skew'] < -0.5]['fwd_return'].dropna().mean()
print(f'Avg forward return when rolling skew > +0.5: {pos_skew_fwd:.2f}%')
print(f'Avg forward return when rolling skew < -0.5: {neg_skew_fwd:.2f}%')
Avg forward return when rolling skew > +0.5: 8.26%
Avg forward return when rolling skew < -0.5: -8.75%

Section 5 — Visualization

This section visualizes the price, rolling skewness, and rolling kurtosis over time to identify trends and patterns.

[ ]
fig, axes = plt.subplots(3, 1, figsize=(14, 11), sharex=True)
fig.suptitle('Rolling Skewness & Kurtosis Analysis', fontsize=14, fontweight='bold')

ax1 = axes[0]
ax1.plot(df.index, df['price'], color='#1976d2', lw=1.2)
ax1.set_ylabel('Price'); ax1.set_title('BTC Price')

ax2 = axes[1]
ax2.plot(df.index, df['rolling_skew'], color='#e65100', lw=1.5)
ax2.fill_between(df.index, df['rolling_skew'], 0,
                  where=df['rolling_skew'] > 0, color='#43a047', alpha=0.25, label='Positive skew (bullish lean)')
ax2.fill_between(df.index, df['rolling_skew'], 0,
                  where=df['rolling_skew'] <= 0, color='#e53935', alpha=0.25, label='Negative skew (bearish lean)')
ax2.axhline(0, color='black', lw=0.8)
ax2.set_ylabel(f'{ROLLING_WINDOW}d Rolling Skewness')
ax2.legend(fontsize=8); ax2.set_title('Rolling Skewness')

ax3 = axes[2]
ax3.plot(df.index, df['rolling_kurt'], color='#7b1fa2', lw=1.5)
ax3.axhline(0, color='black', ls='--', lw=0.8, label='Normal kurtosis = 0')
ax3.axhline(3, color='red', ls=':', lw=0.8, label='Fat tail threshold')
ax3.set_ylabel(f'{ROLLING_WINDOW}d Rolling Kurtosis')
ax3.legend(fontsize=8); ax3.set_title('Rolling Excess Kurtosis')

plt.tight_layout()
plt.show()
cell output

Section 6 — Export

This section exports the generated DataFrame containing all the analysis results to a CSV file.

[ ]
df.to_csv('skewness_kurtosis_analysis.csv')
print('Saved: skewness_kurtosis_analysis.csv')
Saved: skewness_kurtosis_analysis.csv

Conclusion

This notebook demonstrated how to generate synthetic price and return data with varying skewness regimes and then compute rolling skewness, kurtosis, and standard deviation. We explored the relationship between rolling skewness and forward returns, showing how periods of positive skew can indicate bullish sentiment, while negative skew can suggest bearish leanings. The visualizations provided a clear view of how these metrics evolve over time alongside price action. Finally, the analysis results were exported for further use. This methodology can be applied to real-world financial data to identify potential timing signals and manage risk based on the statistical properties of return distributions.