1095 lines
53 KiB
Plaintext
1095 lines
53 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"# Momentum Trading Analysis\n",
|
||
"## Based on Algorithmic Trading Book Chapters 6 & 7\n",
|
||
"\n",
|
||
"This notebook implements and analyzes momentum trading strategies using the converted Python code from MATLAB implementations. We'll explore:\n",
|
||
"\n",
|
||
"1. **Cross-sectional Momentum** (Chapter 6)\n",
|
||
" - Long-short equity momentum strategy\n",
|
||
" - Performance analysis and risk metrics\n",
|
||
" \n",
|
||
"2. **Time Series Momentum** (Chapter 7)\n",
|
||
" - Treasury futures momentum\n",
|
||
" - Statistical significance testing\n",
|
||
" - Risk-adjusted returns\n",
|
||
"\n",
|
||
"3. **Comparative Analysis**\n",
|
||
" - Strategy performance comparison\n",
|
||
" - Risk-return profiles\n",
|
||
" - Market regime analysis\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Import required libraries\n",
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"import seaborn as sns\n",
|
||
"from scipy import stats\n",
|
||
"import warnings\n",
|
||
"warnings.filterwarnings('ignore')\n",
|
||
"\n",
|
||
"# Set up plotting style\n",
|
||
"plt.style.use('seaborn-v0_8')\n",
|
||
"sns.set_palette(\"husl\")\n",
|
||
"plt.rcParams['figure.figsize'] = (12, 8)\n",
|
||
"plt.rcParams['font.size'] = 10\n",
|
||
"\n",
|
||
"# Import our converted trading modules\n",
|
||
"import sys\n",
|
||
"import os\n",
|
||
"sys.path.append(os.path.abspath('..'))\n",
|
||
"\n",
|
||
"from converted_code.data_loader import load_futures_data, load_stock_data\n",
|
||
"from converted_code.backshift import backshift\n",
|
||
"from converted_code.smartmean import smartmean\n",
|
||
"from converted_code.smartstd import smartstd\n",
|
||
"from converted_code.calculateReturns import calculateReturns\n",
|
||
"from converted_code.calculateMaxDD import calculateMaxDD\n",
|
||
"\n",
|
||
"print(\"✅ All libraries imported successfully!\")\n",
|
||
"print(\"📊 Ready for momentum trading analysis\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 1. Data Loading and Preparation\n",
|
||
"\n",
|
||
"Let's start by loading the market data that we'll use for our momentum strategies.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Load Treasury futures data for time series momentum\n",
|
||
"print(\"📈 Loading Treasury Futures Data...\")\n",
|
||
"try:\n",
|
||
" tu_data = load_futures_data('TU', '20120813')\n",
|
||
" tu_prices = tu_data['cl'][:, 0] # Use first contract\n",
|
||
" tu_dates = tu_data['tday']\n",
|
||
" print(f\"✅ Loaded {len(tu_dates)} days of Treasury futures data\")\n",
|
||
" print(f\" Date range: {tu_dates[0]} to {tu_dates[-1]}\")\n",
|
||
" print(f\" Price range: ${tu_prices.min():.2f} to ${tu_prices.max():.2f}\")\n",
|
||
" \n",
|
||
" # Convert dates to pandas datetime for easier handling\n",
|
||
" tu_dates_pd = pd.to_datetime(tu_dates.astype(str), format='%Y%m%d')\n",
|
||
" \n",
|
||
"except Exception as e:\n",
|
||
" print(f\"⚠️ Could not load Treasury data: {e}\")\n",
|
||
" print(\" Using synthetic data for demonstration...\")\n",
|
||
" \n",
|
||
" # Generate synthetic Treasury futures data\n",
|
||
" np.random.seed(42)\n",
|
||
" n_days = 1000\n",
|
||
" tu_dates = np.arange(20100101, 20100101 + n_days)\n",
|
||
" tu_dates_pd = pd.to_datetime(tu_dates.astype(str), format='%Y%m%d')\n",
|
||
" \n",
|
||
" # Generate realistic Treasury futures prices (around 130-140 range)\n",
|
||
" returns = np.random.normal(0, 0.008, n_days) # 0.8% daily volatility\n",
|
||
" tu_prices = 135 * np.cumprod(1 + returns)\n",
|
||
" \n",
|
||
" print(f\"✅ Generated {len(tu_dates)} days of synthetic Treasury data\")\n",
|
||
"\n",
|
||
"print(f\"\\n📊 Treasury Futures Summary:\")\n",
|
||
"print(f\" Mean price: ${tu_prices.mean():.2f}\")\n",
|
||
"print(f\" Volatility: {tu_prices.std():.2f}\")\n",
|
||
"print(f\" Total return: {(tu_prices[-1]/tu_prices[0] - 1)*100:.2f}%\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Load stock data for cross-sectional momentum\n",
|
||
"print(\"📈 Loading Stock Market Data...\")\n",
|
||
"try:\n",
|
||
" stock_data = load_stock_data('20120424')\n",
|
||
" stock_prices = stock_data['cl']\n",
|
||
" stock_dates = stock_data['tday']\n",
|
||
" stock_symbols = stock_data.get('syms', [f'Stock_{i}' for i in range(stock_prices.shape[1])])\n",
|
||
" \n",
|
||
" print(f\"✅ Loaded stock data: {stock_prices.shape[0]} days × {stock_prices.shape[1]} stocks\")\n",
|
||
" print(f\" Date range: {stock_dates[0]} to {stock_dates[-1]}\")\n",
|
||
" \n",
|
||
" # Convert dates to pandas datetime\n",
|
||
" stock_dates_pd = pd.to_datetime(stock_dates.astype(str), format='%Y%m%d')\n",
|
||
" \n",
|
||
"except Exception as e:\n",
|
||
" print(f\"⚠️ Could not load stock data: {e}\")\n",
|
||
" print(\" Using synthetic data for demonstration...\")\n",
|
||
" \n",
|
||
" # Generate synthetic stock data\n",
|
||
" np.random.seed(42)\n",
|
||
" n_days = 1000\n",
|
||
" n_stocks = 100\n",
|
||
" stock_dates = np.arange(20100101, 20100101 + n_days)\n",
|
||
" stock_dates_pd = pd.to_datetime(stock_dates.astype(str), format='%Y%m%d')\n",
|
||
" \n",
|
||
" # Generate correlated stock returns with momentum effects\n",
|
||
" base_returns = np.random.normal(0, 0.02, (n_days, n_stocks))\n",
|
||
" \n",
|
||
" # Add some momentum persistence\n",
|
||
" for i in range(1, n_days):\n",
|
||
" momentum_factor = 0.1 # 10% momentum persistence\n",
|
||
" base_returns[i] += momentum_factor * base_returns[i-1]\n",
|
||
" \n",
|
||
" # Convert to prices\n",
|
||
" stock_prices = 100 * np.cumprod(1 + base_returns, axis=0)\n",
|
||
" stock_symbols = [f'Stock_{i:03d}' for i in range(n_stocks)]\n",
|
||
" \n",
|
||
" print(f\"✅ Generated {n_days} days × {n_stocks} stocks of synthetic data\")\n",
|
||
"\n",
|
||
"print(f\"\\n📊 Stock Market Summary:\")\n",
|
||
"print(f\" Average stock price: ${stock_prices.mean():.2f}\")\n",
|
||
"print(f\" Price volatility: {stock_prices.std():.2f}\")\n",
|
||
"print(f\" Number of stocks: {stock_prices.shape[1]}\")\n",
|
||
"\n",
|
||
"# Create a DataFrame for easier analysis\n",
|
||
"stock_df = pd.DataFrame(stock_prices, index=stock_dates_pd, columns=stock_symbols[:stock_prices.shape[1]])\n",
|
||
"print(f\" DataFrame shape: {stock_df.shape}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 2. Time Series Momentum Strategy (Chapter 7)\n",
|
||
"\n",
|
||
"Time series momentum looks at the persistence of trends within individual assets. We'll implement the Treasury futures momentum strategy that compares current prices to historical levels.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def time_series_momentum_strategy(prices, lookback=250, holddays=25):\n",
|
||
" \"\"\"\n",
|
||
" Implement time series momentum strategy.\n",
|
||
" \n",
|
||
" Parameters:\n",
|
||
" -----------\n",
|
||
" prices : array-like\n",
|
||
" Price series\n",
|
||
" lookback : int\n",
|
||
" Lookback period for momentum signal (default: 250 days = 1 year)\n",
|
||
" holddays : int\n",
|
||
" Holding period for positions (default: 25 days = 1 month)\n",
|
||
" \n",
|
||
" Returns:\n",
|
||
" --------\n",
|
||
" dict : Strategy results including positions, returns, and metrics\n",
|
||
" \"\"\"\n",
|
||
" \n",
|
||
" # Generate momentum signals\n",
|
||
" # Long when current price > price from lookback days ago\n",
|
||
" # Short when current price < price from lookback days ago\n",
|
||
" longs = prices > backshift(lookback, prices)\n",
|
||
" shorts = prices < backshift(lookback, prices)\n",
|
||
" \n",
|
||
" # Initialize position array\n",
|
||
" positions = np.zeros(len(prices))\n",
|
||
" \n",
|
||
" # Build positions over holding period to avoid look-ahead bias\n",
|
||
" for h in range(holddays):\n",
|
||
" long_lag = backshift(h, longs.astype(float))\n",
|
||
" long_lag = np.nan_to_num(long_lag, nan=0).astype(bool)\n",
|
||
" \n",
|
||
" short_lag = backshift(h, shorts.astype(float))\n",
|
||
" short_lag = np.nan_to_num(short_lag, nan=0).astype(bool)\n",
|
||
" \n",
|
||
" positions[long_lag] += 1\n",
|
||
" positions[short_lag] -= 1\n",
|
||
" \n",
|
||
" # Normalize positions by holding period\n",
|
||
" positions = positions / holddays\n",
|
||
" \n",
|
||
" # Calculate market returns\n",
|
||
" market_returns = calculateReturns(prices, 1)\n",
|
||
" market_returns = np.nan_to_num(market_returns, nan=0)\n",
|
||
" \n",
|
||
" # Calculate strategy returns\n",
|
||
" # Use lagged positions to avoid look-ahead bias\n",
|
||
" strategy_returns = backshift(1, positions) * market_returns\n",
|
||
" strategy_returns = np.nan_to_num(strategy_returns, nan=0)\n",
|
||
" \n",
|
||
" # Calculate cumulative returns\n",
|
||
" cumulative_returns = np.cumprod(1 + strategy_returns) - 1\n",
|
||
" market_cumulative = np.cumprod(1 + market_returns) - 1\n",
|
||
" \n",
|
||
" # Calculate performance metrics\n",
|
||
" valid_returns = strategy_returns[strategy_returns != 0]\n",
|
||
" \n",
|
||
" if len(valid_returns) > 0:\n",
|
||
" annual_return = np.prod(1 + valid_returns) ** (252 / len(valid_returns)) - 1\n",
|
||
" volatility = np.std(valid_returns) * np.sqrt(252)\n",
|
||
" sharpe_ratio = annual_return / volatility if volatility > 0 else 0\n",
|
||
" \n",
|
||
" # Maximum drawdown\n",
|
||
" max_dd, max_dd_duration = calculateMaxDD(cumulative_returns)\n",
|
||
" \n",
|
||
" # Win rate\n",
|
||
" win_rate = np.sum(valid_returns > 0) / len(valid_returns)\n",
|
||
" \n",
|
||
" # Number of trades\n",
|
||
" position_changes = np.diff(np.concatenate([[0], positions]))\n",
|
||
" num_trades = np.sum(np.abs(position_changes) > 0)\n",
|
||
" \n",
|
||
" else:\n",
|
||
" annual_return = volatility = sharpe_ratio = max_dd = max_dd_duration = win_rate = num_trades = 0\n",
|
||
" \n",
|
||
" return {\n",
|
||
" 'positions': positions,\n",
|
||
" 'strategy_returns': strategy_returns,\n",
|
||
" 'market_returns': market_returns,\n",
|
||
" 'cumulative_returns': cumulative_returns,\n",
|
||
" 'market_cumulative': market_cumulative,\n",
|
||
" 'annual_return': annual_return,\n",
|
||
" 'volatility': volatility,\n",
|
||
" 'sharpe_ratio': sharpe_ratio,\n",
|
||
" 'max_drawdown': max_dd,\n",
|
||
" 'max_dd_duration': max_dd_duration,\n",
|
||
" 'win_rate': win_rate,\n",
|
||
" 'num_trades': num_trades,\n",
|
||
" 'longs': longs,\n",
|
||
" 'shorts': shorts\n",
|
||
" }\n",
|
||
"\n",
|
||
"# Run the time series momentum strategy on Treasury futures\n",
|
||
"print(\"🚀 Running Time Series Momentum Strategy on Treasury Futures...\")\n",
|
||
"print(\"=\" * 60)\n",
|
||
"\n",
|
||
"ts_results = time_series_momentum_strategy(tu_prices, lookback=250, holddays=25)\n",
|
||
"\n",
|
||
"print(f\"📊 Time Series Momentum Results:\")\n",
|
||
"print(f\" Annual Return: {ts_results['annual_return']:8.2%}\")\n",
|
||
"print(f\" Volatility: {ts_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Sharpe Ratio: {ts_results['sharpe_ratio']:8.2f}\")\n",
|
||
"print(f\" Max Drawdown: {ts_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Max DD Duration: {ts_results['max_dd_duration']:8.0f} days\")\n",
|
||
"print(f\" Win Rate: {ts_results['win_rate']:8.2%}\")\n",
|
||
"print(f\" Number of Trades: {ts_results['num_trades']:8.0f}\")\n",
|
||
"\n",
|
||
"# Compare to buy-and-hold\n",
|
||
"market_annual = np.prod(1 + ts_results['market_returns']) ** (252 / len(ts_results['market_returns'])) - 1\n",
|
||
"market_vol = np.std(ts_results['market_returns']) * np.sqrt(252)\n",
|
||
"market_sharpe = market_annual / market_vol if market_vol > 0 else 0\n",
|
||
"\n",
|
||
"print(f\"\\n📈 Buy-and-Hold Comparison:\")\n",
|
||
"print(f\" Market Annual Return: {market_annual:8.2%}\")\n",
|
||
"print(f\" Market Volatility: {market_vol:8.2%}\")\n",
|
||
"print(f\" Market Sharpe Ratio: {market_sharpe:8.2f}\")\n",
|
||
"print(f\" Strategy Excess Return: {ts_results['annual_return'] - market_annual:8.2%}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 3. Cross-Sectional Momentum Strategy (Chapter 6)\n",
|
||
"\n",
|
||
"Cross-sectional momentum compares the relative performance of different assets and goes long the winners while shorting the losers.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def cross_sectional_momentum_strategy(prices, lookback=252, holddays=25, top_n=50):\n",
|
||
" \"\"\"\n",
|
||
" Implement cross-sectional momentum strategy (Kent Daniel style).\n",
|
||
" \n",
|
||
" Parameters:\n",
|
||
" -----------\n",
|
||
" prices : array-like (2D)\n",
|
||
" Price matrix (time x assets)\n",
|
||
" lookback : int\n",
|
||
" Lookback period for momentum calculation (default: 252 days = 1 year)\n",
|
||
" holddays : int\n",
|
||
" Holding period for positions (default: 25 days = 1 month)\n",
|
||
" top_n : int\n",
|
||
" Number of top/bottom performers to select (default: 50)\n",
|
||
" \n",
|
||
" Returns:\n",
|
||
" --------\n",
|
||
" dict : Strategy results including positions, returns, and metrics\n",
|
||
" \"\"\"\n",
|
||
" \n",
|
||
" n_days, n_assets = prices.shape\n",
|
||
" \n",
|
||
" # Calculate returns for momentum ranking\n",
|
||
" returns = calculateReturns(prices, 1)\n",
|
||
" returns = np.nan_to_num(returns, nan=0)\n",
|
||
" \n",
|
||
" # Calculate cumulative returns over lookback period for ranking\n",
|
||
" cum_returns = np.zeros_like(prices)\n",
|
||
" for i in range(lookback, n_days):\n",
|
||
" # Calculate cumulative return over lookback period\n",
|
||
" period_returns = returns[i-lookback+1:i+1, :]\n",
|
||
" cum_returns[i, :] = np.prod(1 + period_returns, axis=0) - 1\n",
|
||
" \n",
|
||
" # Initialize position matrix\n",
|
||
" positions = np.zeros_like(prices)\n",
|
||
" \n",
|
||
" # Generate trading signals\n",
|
||
" for i in range(lookback, n_days):\n",
|
||
" # Rank assets by momentum (cumulative returns)\n",
|
||
" momentum_scores = cum_returns[i, :]\n",
|
||
" \n",
|
||
" # Handle NaN values\n",
|
||
" valid_scores = ~np.isnan(momentum_scores)\n",
|
||
" if np.sum(valid_scores) < 2 * top_n:\n",
|
||
" continue\n",
|
||
" \n",
|
||
" # Get rankings\n",
|
||
" rankings = np.argsort(momentum_scores[valid_scores])\n",
|
||
" valid_indices = np.where(valid_scores)[0]\n",
|
||
" \n",
|
||
" # Select top and bottom performers\n",
|
||
" if len(rankings) >= 2 * top_n:\n",
|
||
" # Bottom performers (short)\n",
|
||
" bottom_indices = valid_indices[rankings[:top_n]]\n",
|
||
" # Top performers (long)\n",
|
||
" top_indices = valid_indices[rankings[-top_n:]]\n",
|
||
" \n",
|
||
" # Set positions for holding period\n",
|
||
" for h in range(min(holddays, n_days - i)):\n",
|
||
" if i + h < n_days:\n",
|
||
" positions[i + h, top_indices] = 1.0 / top_n # Long position\n",
|
||
" positions[i + h, bottom_indices] = -1.0 / top_n # Short position\n",
|
||
" \n",
|
||
" # Calculate strategy returns\n",
|
||
" strategy_returns = np.sum(backshift(1, positions) * returns, axis=1)\n",
|
||
" strategy_returns = np.nan_to_num(strategy_returns, nan=0)\n",
|
||
" \n",
|
||
" # Calculate market returns (equal-weighted)\n",
|
||
" market_returns = np.nanmean(returns, axis=1)\n",
|
||
" market_returns = np.nan_to_num(market_returns, nan=0)\n",
|
||
" \n",
|
||
" # Calculate cumulative returns\n",
|
||
" cumulative_returns = np.cumprod(1 + strategy_returns) - 1\n",
|
||
" market_cumulative = np.cumprod(1 + market_returns) - 1\n",
|
||
" \n",
|
||
" # Calculate performance metrics\n",
|
||
" valid_returns = strategy_returns[strategy_returns != 0]\n",
|
||
" \n",
|
||
" if len(valid_returns) > 0:\n",
|
||
" annual_return = np.prod(1 + valid_returns) ** (252 / len(valid_returns)) - 1\n",
|
||
" volatility = np.std(valid_returns) * np.sqrt(252)\n",
|
||
" sharpe_ratio = annual_return / volatility if volatility > 0 else 0\n",
|
||
" \n",
|
||
" # Maximum drawdown\n",
|
||
" max_dd, max_dd_duration = calculateMaxDD(cumulative_returns)\n",
|
||
" \n",
|
||
" # Win rate\n",
|
||
" win_rate = np.sum(valid_returns > 0) / len(valid_returns)\n",
|
||
" \n",
|
||
" # Number of rebalances\n",
|
||
" position_changes = np.sum(np.abs(np.diff(positions, axis=0)), axis=1)\n",
|
||
" num_rebalances = np.sum(position_changes > 0)\n",
|
||
" \n",
|
||
" else:\n",
|
||
" annual_return = volatility = sharpe_ratio = max_dd = max_dd_duration = win_rate = num_rebalances = 0\n",
|
||
" \n",
|
||
" return {\n",
|
||
" 'positions': positions,\n",
|
||
" 'strategy_returns': strategy_returns,\n",
|
||
" 'market_returns': market_returns,\n",
|
||
" 'cumulative_returns': cumulative_returns,\n",
|
||
" 'market_cumulative': market_cumulative,\n",
|
||
" 'annual_return': annual_return,\n",
|
||
" 'volatility': volatility,\n",
|
||
" 'sharpe_ratio': sharpe_ratio,\n",
|
||
" 'max_drawdown': max_dd,\n",
|
||
" 'max_dd_duration': max_dd_duration,\n",
|
||
" 'win_rate': win_rate,\n",
|
||
" 'num_rebalances': num_rebalances,\n",
|
||
" 'cum_returns': cum_returns\n",
|
||
" }\n",
|
||
"\n",
|
||
"# Run the cross-sectional momentum strategy on stocks\n",
|
||
"print(\"🚀 Running Cross-Sectional Momentum Strategy on Stocks...\")\n",
|
||
"print(\"=\" * 60)\n",
|
||
"\n",
|
||
"# Use a subset of stocks for faster computation if we have many\n",
|
||
"max_stocks = min(100, stock_prices.shape[1])\n",
|
||
"cs_results = cross_sectional_momentum_strategy(\n",
|
||
" stock_prices[:, :max_stocks], \n",
|
||
" lookback=252, \n",
|
||
" holddays=25, \n",
|
||
" top_n=min(20, max_stocks//4)\n",
|
||
")\n",
|
||
"\n",
|
||
"print(f\"📊 Cross-Sectional Momentum Results:\")\n",
|
||
"print(f\" Annual Return: {cs_results['annual_return']:8.2%}\")\n",
|
||
"print(f\" Volatility: {cs_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Sharpe Ratio: {cs_results['sharpe_ratio']:8.2f}\")\n",
|
||
"print(f\" Max Drawdown: {cs_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Max DD Duration: {cs_results['max_dd_duration']:8.0f} days\")\n",
|
||
"print(f\" Win Rate: {cs_results['win_rate']:8.2%}\")\n",
|
||
"print(f\" Number of Rebalances: {cs_results['num_rebalances']:8.0f}\")\n",
|
||
"\n",
|
||
"# Compare to market\n",
|
||
"cs_market_annual = np.prod(1 + cs_results['market_returns']) ** (252 / len(cs_results['market_returns'])) - 1\n",
|
||
"cs_market_vol = np.std(cs_results['market_returns']) * np.sqrt(252)\n",
|
||
"cs_market_sharpe = cs_market_annual / cs_market_vol if cs_market_vol > 0 else 0\n",
|
||
"\n",
|
||
"print(f\"\\n📈 Market Comparison:\")\n",
|
||
"print(f\" Market Annual Return: {cs_market_annual:8.2%}\")\n",
|
||
"print(f\" Market Volatility: {cs_market_vol:8.2%}\")\n",
|
||
"print(f\" Market Sharpe Ratio: {cs_market_sharpe:8.2f}\")\n",
|
||
"print(f\" Strategy Excess Return: {cs_results['annual_return'] - cs_market_annual:8.2%}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 4. Visualization and Analysis\n",
|
||
"\n",
|
||
"Let's create comprehensive visualizations to analyze the performance of both momentum strategies.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Create comprehensive performance visualization\n",
|
||
"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
|
||
"fig.suptitle('Momentum Trading Strategies Performance Analysis', fontsize=16, fontweight='bold')\n",
|
||
"\n",
|
||
"# 1. Cumulative Returns Comparison\n",
|
||
"ax1 = axes[0, 0]\n",
|
||
"ax1.plot(tu_dates_pd[:len(ts_results['cumulative_returns'])], \n",
|
||
" ts_results['cumulative_returns'] * 100, \n",
|
||
" label='Time Series Momentum', linewidth=2, color='blue')\n",
|
||
"ax1.plot(tu_dates_pd[:len(ts_results['market_cumulative'])], \n",
|
||
" ts_results['market_cumulative'] * 100, \n",
|
||
" label='Buy & Hold (Treasury)', linewidth=2, color='gray', alpha=0.7)\n",
|
||
"ax1.set_title('Time Series Momentum: Cumulative Returns', fontweight='bold')\n",
|
||
"ax1.set_ylabel('Cumulative Return (%)')\n",
|
||
"ax1.legend()\n",
|
||
"ax1.grid(True, alpha=0.3)\n",
|
||
"\n",
|
||
"# 2. Cross-sectional momentum cumulative returns\n",
|
||
"ax2 = axes[0, 1]\n",
|
||
"ax2.plot(stock_dates_pd[:len(cs_results['cumulative_returns'])], \n",
|
||
" cs_results['cumulative_returns'] * 100, \n",
|
||
" label='Cross-Sectional Momentum', linewidth=2, color='red')\n",
|
||
"ax2.plot(stock_dates_pd[:len(cs_results['market_cumulative'])], \n",
|
||
" cs_results['market_cumulative'] * 100, \n",
|
||
" label='Market (Equal Weight)', linewidth=2, color='gray', alpha=0.7)\n",
|
||
"ax2.set_title('Cross-Sectional Momentum: Cumulative Returns', fontweight='bold')\n",
|
||
"ax2.set_ylabel('Cumulative Return (%)')\n",
|
||
"ax2.legend()\n",
|
||
"ax2.grid(True, alpha=0.3)\n",
|
||
"\n",
|
||
"# 3. Rolling Sharpe Ratio (252-day window)\n",
|
||
"ax3 = axes[1, 0]\n",
|
||
"window = 252\n",
|
||
"\n",
|
||
"# Calculate rolling Sharpe for time series momentum\n",
|
||
"ts_rolling_sharpe = []\n",
|
||
"for i in range(window, len(ts_results['strategy_returns'])):\n",
|
||
" window_returns = ts_results['strategy_returns'][i-window:i]\n",
|
||
" if np.std(window_returns) > 0:\n",
|
||
" sharpe = np.mean(window_returns) * np.sqrt(252) / (np.std(window_returns) * np.sqrt(252))\n",
|
||
" ts_rolling_sharpe.append(sharpe)\n",
|
||
" else:\n",
|
||
" ts_rolling_sharpe.append(0)\n",
|
||
"\n",
|
||
"# Calculate rolling Sharpe for cross-sectional momentum\n",
|
||
"cs_rolling_sharpe = []\n",
|
||
"for i in range(window, len(cs_results['strategy_returns'])):\n",
|
||
" window_returns = cs_results['strategy_returns'][i-window:i]\n",
|
||
" if np.std(window_returns) > 0:\n",
|
||
" sharpe = np.mean(window_returns) * np.sqrt(252) / (np.std(window_returns) * np.sqrt(252))\n",
|
||
" cs_rolling_sharpe.append(sharpe)\n",
|
||
" else:\n",
|
||
" cs_rolling_sharpe.append(0)\n",
|
||
"\n",
|
||
"if len(ts_rolling_sharpe) > 0:\n",
|
||
" ax3.plot(tu_dates_pd[window:window+len(ts_rolling_sharpe)], \n",
|
||
" ts_rolling_sharpe, label='Time Series', linewidth=2, color='blue')\n",
|
||
"\n",
|
||
"if len(cs_rolling_sharpe) > 0:\n",
|
||
" ax3.plot(stock_dates_pd[window:window+len(cs_rolling_sharpe)], \n",
|
||
" cs_rolling_sharpe, label='Cross-Sectional', linewidth=2, color='red')\n",
|
||
"\n",
|
||
"ax3.axhline(y=0, color='black', linestyle='--', alpha=0.5)\n",
|
||
"ax3.set_title('Rolling Sharpe Ratio (252-day window)', fontweight='bold')\n",
|
||
"ax3.set_ylabel('Sharpe Ratio')\n",
|
||
"ax3.legend()\n",
|
||
"ax3.grid(True, alpha=0.3)\n",
|
||
"\n",
|
||
"# 4. Return Distribution Comparison\n",
|
||
"ax4 = axes[1, 1]\n",
|
||
"ts_valid_returns = ts_results['strategy_returns'][ts_results['strategy_returns'] != 0]\n",
|
||
"cs_valid_returns = cs_results['strategy_returns'][cs_results['strategy_returns'] != 0]\n",
|
||
"\n",
|
||
"if len(ts_valid_returns) > 0:\n",
|
||
" ax4.hist(ts_valid_returns * 100, bins=50, alpha=0.6, label='Time Series', \n",
|
||
" color='blue', density=True)\n",
|
||
"\n",
|
||
"if len(cs_valid_returns) > 0:\n",
|
||
" ax4.hist(cs_valid_returns * 100, bins=50, alpha=0.6, label='Cross-Sectional', \n",
|
||
" color='red', density=True)\n",
|
||
"\n",
|
||
"ax4.axvline(x=0, color='black', linestyle='--', alpha=0.5)\n",
|
||
"ax4.set_title('Daily Returns Distribution', fontweight='bold')\n",
|
||
"ax4.set_xlabel('Daily Return (%)')\n",
|
||
"ax4.set_ylabel('Density')\n",
|
||
"ax4.legend()\n",
|
||
"ax4.grid(True, alpha=0.3)\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.show()\n",
|
||
"\n",
|
||
"# Print summary statistics\n",
|
||
"print(\"\\n\" + \"=\"*80)\n",
|
||
"print(\"📊 MOMENTUM STRATEGIES PERFORMANCE SUMMARY\")\n",
|
||
"print(\"=\"*80)\n",
|
||
"\n",
|
||
"print(f\"\\n🔵 TIME SERIES MOMENTUM (Treasury Futures):\")\n",
|
||
"print(f\" Annual Return: {ts_results['annual_return']:8.2%}\")\n",
|
||
"print(f\" Volatility: {ts_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Sharpe Ratio: {ts_results['sharpe_ratio']:8.2f}\")\n",
|
||
"print(f\" Max Drawdown: {ts_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Win Rate: {ts_results['win_rate']:8.2%}\")\n",
|
||
"\n",
|
||
"print(f\"\\n🔴 CROSS-SECTIONAL MOMENTUM (Stocks):\")\n",
|
||
"print(f\" Annual Return: {cs_results['annual_return']:8.2%}\")\n",
|
||
"print(f\" Volatility: {cs_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Sharpe Ratio: {cs_results['sharpe_ratio']:8.2f}\")\n",
|
||
"print(f\" Max Drawdown: {cs_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Win Rate: {cs_results['win_rate']:8.2%}\")\n",
|
||
"\n",
|
||
"print(f\"\\n📈 BENCHMARK COMPARISON:\")\n",
|
||
"print(f\" Treasury B&H Return: {market_annual:8.2%}\")\n",
|
||
"print(f\" Stock Market Return: {cs_market_annual:8.2%}\")\n",
|
||
"print(f\" TS Momentum Alpha: {ts_results['annual_return'] - market_annual:8.2%}\")\n",
|
||
"print(f\" CS Momentum Alpha: {cs_results['annual_return'] - cs_market_annual:8.2%}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 5. Statistical Significance Testing\n",
|
||
"\n",
|
||
"Let's perform hypothesis testing to determine if the momentum effects are statistically significant, following the methodology from Chapter 7.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def statistical_significance_tests(strategy_returns, market_returns, num_simulations=1000):\n",
|
||
" \"\"\"\n",
|
||
" Perform statistical significance tests for momentum strategies.\n",
|
||
" \n",
|
||
" Parameters:\n",
|
||
" -----------\n",
|
||
" strategy_returns : array-like\n",
|
||
" Strategy returns\n",
|
||
" market_returns : array-like \n",
|
||
" Market returns for comparison\n",
|
||
" num_simulations : int\n",
|
||
" Number of Monte Carlo simulations\n",
|
||
" \n",
|
||
" Returns:\n",
|
||
" --------\n",
|
||
" dict : Test results including p-values and statistics\n",
|
||
" \"\"\"\n",
|
||
" \n",
|
||
" # Remove zero returns for cleaner analysis\n",
|
||
" strategy_returns = strategy_returns[strategy_returns != 0]\n",
|
||
" market_returns = market_returns[market_returns != 0]\n",
|
||
" \n",
|
||
" if len(strategy_returns) == 0:\n",
|
||
" return {'error': 'No valid strategy returns'}\n",
|
||
" \n",
|
||
" # 1. Basic t-test (assuming normal distribution)\n",
|
||
" strategy_mean = np.mean(strategy_returns)\n",
|
||
" strategy_std = np.std(strategy_returns)\n",
|
||
" n_obs = len(strategy_returns)\n",
|
||
" \n",
|
||
" if strategy_std > 0:\n",
|
||
" t_stat = strategy_mean * np.sqrt(n_obs) / strategy_std\n",
|
||
" p_value_ttest = 2 * (1 - stats.t.cdf(np.abs(t_stat), n_obs - 1))\n",
|
||
" else:\n",
|
||
" t_stat = p_value_ttest = 0\n",
|
||
" \n",
|
||
" # 2. Bootstrap test\n",
|
||
" bootstrap_means = []\n",
|
||
" for _ in range(num_simulations):\n",
|
||
" bootstrap_sample = np.random.choice(strategy_returns, size=len(strategy_returns), replace=True)\n",
|
||
" bootstrap_means.append(np.mean(bootstrap_sample))\n",
|
||
" \n",
|
||
" p_value_bootstrap = np.sum(np.array(bootstrap_means) <= 0) / num_simulations\n",
|
||
" \n",
|
||
" # 3. Randomized market returns test\n",
|
||
" if len(market_returns) > 0:\n",
|
||
" market_mean = np.mean(market_returns)\n",
|
||
" market_std = np.std(market_returns)\n",
|
||
" \n",
|
||
" random_means = []\n",
|
||
" for _ in range(num_simulations):\n",
|
||
" # Generate random returns with same statistical properties as market\n",
|
||
" random_returns = np.random.normal(market_mean, market_std, len(strategy_returns))\n",
|
||
" random_means.append(np.mean(random_returns))\n",
|
||
" \n",
|
||
" p_value_random_market = np.sum(np.array(random_means) >= strategy_mean) / num_simulations\n",
|
||
" else:\n",
|
||
" p_value_random_market = np.nan\n",
|
||
" \n",
|
||
" # 4. Sharpe ratio test\n",
|
||
" if strategy_std > 0:\n",
|
||
" strategy_sharpe = strategy_mean / strategy_std * np.sqrt(252)\n",
|
||
" \n",
|
||
" # Bootstrap Sharpe ratios\n",
|
||
" bootstrap_sharpes = []\n",
|
||
" for _ in range(num_simulations):\n",
|
||
" bootstrap_sample = np.random.choice(strategy_returns, size=len(strategy_returns), replace=True)\n",
|
||
" if np.std(bootstrap_sample) > 0:\n",
|
||
" bootstrap_sharpes.append(np.mean(bootstrap_sample) / np.std(bootstrap_sample) * np.sqrt(252))\n",
|
||
" \n",
|
||
" if len(bootstrap_sharpes) > 0:\n",
|
||
" p_value_sharpe = np.sum(np.array(bootstrap_sharpes) <= 0) / len(bootstrap_sharpes)\n",
|
||
" else:\n",
|
||
" p_value_sharpe = np.nan\n",
|
||
" else:\n",
|
||
" strategy_sharpe = p_value_sharpe = 0\n",
|
||
" \n",
|
||
" return {\n",
|
||
" 'strategy_mean': strategy_mean,\n",
|
||
" 'strategy_std': strategy_std,\n",
|
||
" 'strategy_sharpe': strategy_sharpe,\n",
|
||
" 't_statistic': t_stat,\n",
|
||
" 'p_value_ttest': p_value_ttest,\n",
|
||
" 'p_value_bootstrap': p_value_bootstrap,\n",
|
||
" 'p_value_random_market': p_value_random_market,\n",
|
||
" 'p_value_sharpe': p_value_sharpe,\n",
|
||
" 'n_observations': n_obs\n",
|
||
" }\n",
|
||
"\n",
|
||
"# Test statistical significance for both strategies\n",
|
||
"print(\"🧪 STATISTICAL SIGNIFICANCE TESTING\")\n",
|
||
"print(\"=\" * 60)\n",
|
||
"\n",
|
||
"# Time Series Momentum Tests\n",
|
||
"print(\"\\n🔵 Time Series Momentum (Treasury Futures):\")\n",
|
||
"ts_tests = statistical_significance_tests(\n",
|
||
" ts_results['strategy_returns'], \n",
|
||
" ts_results['market_returns'], \n",
|
||
" num_simulations=1000\n",
|
||
")\n",
|
||
"\n",
|
||
"if 'error' not in ts_tests:\n",
|
||
" print(f\" Strategy Mean Return: {ts_tests['strategy_mean']:8.6f}\")\n",
|
||
" print(f\" Strategy Std Dev: {ts_tests['strategy_std']:8.6f}\")\n",
|
||
" print(f\" Strategy Sharpe Ratio: {ts_tests['strategy_sharpe']:8.2f}\")\n",
|
||
" print(f\" T-statistic: {ts_tests['t_statistic']:8.2f}\")\n",
|
||
" print(f\" P-value (t-test): {ts_tests['p_value_ttest']:8.4f}\")\n",
|
||
" print(f\" P-value (bootstrap): {ts_tests['p_value_bootstrap']:8.4f}\")\n",
|
||
" print(f\" P-value (random market): {ts_tests['p_value_random_market']:8.4f}\")\n",
|
||
" print(f\" P-value (Sharpe): {ts_tests['p_value_sharpe']:8.4f}\")\n",
|
||
" print(f\" Number of observations: {ts_tests['n_observations']:8.0f}\")\n",
|
||
"else:\n",
|
||
" print(f\" Error: {ts_tests['error']}\")\n",
|
||
"\n",
|
||
"# Cross-Sectional Momentum Tests\n",
|
||
"print(\"\\n🔴 Cross-Sectional Momentum (Stocks):\")\n",
|
||
"cs_tests = statistical_significance_tests(\n",
|
||
" cs_results['strategy_returns'], \n",
|
||
" cs_results['market_returns'], \n",
|
||
" num_simulations=1000\n",
|
||
")\n",
|
||
"\n",
|
||
"if 'error' not in cs_tests:\n",
|
||
" print(f\" Strategy Mean Return: {cs_tests['strategy_mean']:8.6f}\")\n",
|
||
" print(f\" Strategy Std Dev: {cs_tests['strategy_std']:8.6f}\")\n",
|
||
" print(f\" Strategy Sharpe Ratio: {cs_tests['strategy_sharpe']:8.2f}\")\n",
|
||
" print(f\" T-statistic: {cs_tests['t_statistic']:8.2f}\")\n",
|
||
" print(f\" P-value (t-test): {cs_tests['p_value_ttest']:8.4f}\")\n",
|
||
" print(f\" P-value (bootstrap): {cs_tests['p_value_bootstrap']:8.4f}\")\n",
|
||
" print(f\" P-value (random market): {cs_tests['p_value_random_market']:8.4f}\")\n",
|
||
" print(f\" P-value (Sharpe): {cs_tests['p_value_sharpe']:8.4f}\")\n",
|
||
" print(f\" Number of observations: {cs_tests['n_observations']:8.0f}\")\n",
|
||
"else:\n",
|
||
" print(f\" Error: {cs_tests['error']}\")\n",
|
||
"\n",
|
||
"# Interpretation\n",
|
||
"print(f\"\\n📋 INTERPRETATION:\")\n",
|
||
"print(f\" P-values < 0.05 indicate statistical significance at 95% confidence level\")\n",
|
||
"print(f\" P-values < 0.01 indicate statistical significance at 99% confidence level\")\n",
|
||
"\n",
|
||
"# Check significance\n",
|
||
"if 'error' not in ts_tests:\n",
|
||
" ts_significant = ts_tests['p_value_ttest'] < 0.05\n",
|
||
" print(f\" Time Series Momentum is {'SIGNIFICANT' if ts_significant else 'NOT SIGNIFICANT'} (p={ts_tests['p_value_ttest']:.4f})\")\n",
|
||
"\n",
|
||
"if 'error' not in cs_tests:\n",
|
||
" cs_significant = cs_tests['p_value_ttest'] < 0.05\n",
|
||
" print(f\" Cross-Sectional Momentum is {'SIGNIFICANT' if cs_significant else 'NOT SIGNIFICANT'} (p={cs_tests['p_value_ttest']:.4f})\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 6. Risk Analysis and Drawdown Visualization\n",
|
||
"\n",
|
||
"Let's analyze the risk characteristics and drawdown patterns of both momentum strategies.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def calculate_drawdown_series(cumulative_returns):\n",
|
||
" \"\"\"Calculate drawdown series from cumulative returns.\"\"\"\n",
|
||
" peak = np.maximum.accumulate(cumulative_returns)\n",
|
||
" drawdown = (cumulative_returns - peak) / (1 + peak)\n",
|
||
" return drawdown\n",
|
||
"\n",
|
||
"# Calculate drawdown series\n",
|
||
"ts_drawdown = calculate_drawdown_series(ts_results['cumulative_returns'])\n",
|
||
"cs_drawdown = calculate_drawdown_series(cs_results['cumulative_returns'])\n",
|
||
"\n",
|
||
"# Create risk analysis visualization\n",
|
||
"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n",
|
||
"fig.suptitle('Risk Analysis and Drawdown Patterns', fontsize=16, fontweight='bold')\n",
|
||
"\n",
|
||
"# 1. Drawdown over time\n",
|
||
"ax1 = axes[0, 0]\n",
|
||
"ax1.fill_between(tu_dates_pd[:len(ts_drawdown)], ts_drawdown * 100, 0, \n",
|
||
" alpha=0.6, color='blue', label='Time Series Momentum')\n",
|
||
"ax1.set_title('Time Series Momentum: Drawdown Over Time', fontweight='bold')\n",
|
||
"ax1.set_ylabel('Drawdown (%)')\n",
|
||
"ax1.grid(True, alpha=0.3)\n",
|
||
"ax1.legend()\n",
|
||
"\n",
|
||
"ax2 = axes[0, 1]\n",
|
||
"ax2.fill_between(stock_dates_pd[:len(cs_drawdown)], cs_drawdown * 100, 0, \n",
|
||
" alpha=0.6, color='red', label='Cross-Sectional Momentum')\n",
|
||
"ax2.set_title('Cross-Sectional Momentum: Drawdown Over Time', fontweight='bold')\n",
|
||
"ax2.set_ylabel('Drawdown (%)')\n",
|
||
"ax2.grid(True, alpha=0.3)\n",
|
||
"ax2.legend()\n",
|
||
"\n",
|
||
"# 3. Risk-Return Scatter Plot\n",
|
||
"ax3 = axes[1, 0]\n",
|
||
"\n",
|
||
"# Calculate rolling metrics for scatter plot\n",
|
||
"window = 63 # Quarterly windows\n",
|
||
"ts_rolling_returns = []\n",
|
||
"ts_rolling_vols = []\n",
|
||
"cs_rolling_returns = []\n",
|
||
"cs_rolling_vols = []\n",
|
||
"\n",
|
||
"for i in range(window, len(ts_results['strategy_returns']), window//2):\n",
|
||
" window_returns = ts_results['strategy_returns'][i-window:i]\n",
|
||
" if len(window_returns[window_returns != 0]) > 10: # Need sufficient data\n",
|
||
" ts_rolling_returns.append(np.mean(window_returns) * 252)\n",
|
||
" ts_rolling_vols.append(np.std(window_returns) * np.sqrt(252))\n",
|
||
"\n",
|
||
"for i in range(window, len(cs_results['strategy_returns']), window//2):\n",
|
||
" window_returns = cs_results['strategy_returns'][i-window:i]\n",
|
||
" if len(window_returns[window_returns != 0]) > 10: # Need sufficient data\n",
|
||
" cs_rolling_returns.append(np.mean(window_returns) * 252)\n",
|
||
" cs_rolling_vols.append(np.std(window_returns) * np.sqrt(252))\n",
|
||
"\n",
|
||
"if len(ts_rolling_returns) > 0:\n",
|
||
" ax3.scatter(np.array(ts_rolling_vols) * 100, np.array(ts_rolling_returns) * 100, \n",
|
||
" alpha=0.6, color='blue', s=50, label='Time Series')\n",
|
||
"\n",
|
||
"if len(cs_rolling_returns) > 0:\n",
|
||
" ax3.scatter(np.array(cs_rolling_vols) * 100, np.array(cs_rolling_returns) * 100, \n",
|
||
" alpha=0.6, color='red', s=50, label='Cross-Sectional')\n",
|
||
"\n",
|
||
"ax3.axhline(y=0, color='black', linestyle='--', alpha=0.5)\n",
|
||
"ax3.axvline(x=0, color='black', linestyle='--', alpha=0.5)\n",
|
||
"ax3.set_xlabel('Volatility (%)')\n",
|
||
"ax3.set_ylabel('Annual Return (%)')\n",
|
||
"ax3.set_title('Risk-Return Profile (Rolling Windows)', fontweight='bold')\n",
|
||
"ax3.legend()\n",
|
||
"ax3.grid(True, alpha=0.3)\n",
|
||
"\n",
|
||
"# 4. Monthly Returns Heatmap for Time Series Momentum\n",
|
||
"ax4 = axes[1, 1]\n",
|
||
"\n",
|
||
"# Create monthly returns for time series momentum\n",
|
||
"ts_monthly_returns = []\n",
|
||
"ts_monthly_dates = []\n",
|
||
"\n",
|
||
"if len(ts_results['strategy_returns']) > 0:\n",
|
||
" # Convert to DataFrame for easier resampling\n",
|
||
" ts_df = pd.DataFrame({\n",
|
||
" 'returns': ts_results['strategy_returns'],\n",
|
||
" 'date': tu_dates_pd[:len(ts_results['strategy_returns'])]\n",
|
||
" }).set_index('date')\n",
|
||
" \n",
|
||
" # Resample to monthly\n",
|
||
" monthly_data = ts_df.resample('M').apply(lambda x: (1 + x).prod() - 1)\n",
|
||
" \n",
|
||
" if len(monthly_data) > 12: # Need at least a year of data\n",
|
||
" # Create year-month matrix\n",
|
||
" monthly_data.index = pd.to_datetime(monthly_data.index)\n",
|
||
" monthly_data['year'] = monthly_data.index.year\n",
|
||
" monthly_data['month'] = monthly_data.index.month\n",
|
||
" \n",
|
||
" # Pivot to create heatmap data\n",
|
||
" heatmap_data = monthly_data.pivot_table(values='returns', index='year', columns='month', fill_value=0)\n",
|
||
" \n",
|
||
" # Create heatmap\n",
|
||
" sns.heatmap(heatmap_data * 100, annot=True, fmt='.1f', cmap='RdYlBu_r', \n",
|
||
" center=0, ax=ax4, cbar_kws={'label': 'Monthly Return (%)'})\n",
|
||
" ax4.set_title('Time Series Momentum: Monthly Returns Heatmap', fontweight='bold')\n",
|
||
" ax4.set_xlabel('Month')\n",
|
||
" ax4.set_ylabel('Year')\n",
|
||
" else:\n",
|
||
" ax4.text(0.5, 0.5, 'Insufficient data for\\nmonthly heatmap', \n",
|
||
" ha='center', va='center', transform=ax4.transAxes, fontsize=12)\n",
|
||
" ax4.set_title('Monthly Returns Heatmap', fontweight='bold')\n",
|
||
"\n",
|
||
"plt.tight_layout()\n",
|
||
"plt.show()\n",
|
||
"\n",
|
||
"# Print detailed risk metrics\n",
|
||
"print(\"\\n\" + \"=\"*80)\n",
|
||
"print(\"📊 DETAILED RISK ANALYSIS\")\n",
|
||
"print(\"=\"*80)\n",
|
||
"\n",
|
||
"print(f\"\\n🔵 TIME SERIES MOMENTUM RISK METRICS:\")\n",
|
||
"print(f\" Maximum Drawdown: {ts_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Max DD Duration: {ts_results['max_dd_duration']:8.0f} days\")\n",
|
||
"print(f\" Volatility (Annual): {ts_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Downside Deviation: {np.std(ts_results['strategy_returns'][ts_results['strategy_returns'] < 0]) * np.sqrt(252):8.2%}\")\n",
|
||
"print(f\" VaR (95%): {np.percentile(ts_results['strategy_returns'][ts_results['strategy_returns'] != 0], 5) * 100:8.2f}%\")\n",
|
||
"print(f\" Skewness: {stats.skew(ts_results['strategy_returns'][ts_results['strategy_returns'] != 0]):8.2f}\")\n",
|
||
"print(f\" Kurtosis: {stats.kurtosis(ts_results['strategy_returns'][ts_results['strategy_returns'] != 0]):8.2f}\")\n",
|
||
"\n",
|
||
"print(f\"\\n🔴 CROSS-SECTIONAL MOMENTUM RISK METRICS:\")\n",
|
||
"print(f\" Maximum Drawdown: {cs_results['max_drawdown']:8.2%}\")\n",
|
||
"print(f\" Max DD Duration: {cs_results['max_dd_duration']:8.0f} days\")\n",
|
||
"print(f\" Volatility (Annual): {cs_results['volatility']:8.2%}\")\n",
|
||
"print(f\" Downside Deviation: {np.std(cs_results['strategy_returns'][cs_results['strategy_returns'] < 0]) * np.sqrt(252):8.2%}\")\n",
|
||
"print(f\" VaR (95%): {np.percentile(cs_results['strategy_returns'][cs_results['strategy_returns'] != 0], 5) * 100:8.2f}%\")\n",
|
||
"print(f\" Skewness: {stats.skew(cs_results['strategy_returns'][cs_results['strategy_returns'] != 0]):8.2f}\")\n",
|
||
"print(f\" Kurtosis: {stats.kurtosis(cs_results['strategy_returns'][cs_results['strategy_returns'] != 0]):8.2f}\")\n",
|
||
"\n",
|
||
"# Calculate Calmar Ratio (Annual Return / Max Drawdown)\n",
|
||
"ts_calmar = ts_results['annual_return'] / abs(ts_results['max_drawdown']) if ts_results['max_drawdown'] != 0 else 0\n",
|
||
"cs_calmar = cs_results['annual_return'] / abs(cs_results['max_drawdown']) if cs_results['max_drawdown'] != 0 else 0\n",
|
||
"\n",
|
||
"print(f\"\\n📈 RISK-ADJUSTED PERFORMANCE:\")\n",
|
||
"print(f\" Time Series Calmar Ratio: {ts_calmar:8.2f}\")\n",
|
||
"print(f\" Cross-Sectional Calmar Ratio: {cs_calmar:8.2f}\")\n",
|
||
"print(f\" (Calmar Ratio = Annual Return / Max Drawdown)\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "raw",
|
||
"metadata": {
|
||
"vscode": {
|
||
"languageId": "raw"
|
||
}
|
||
},
|
||
"source": [
|
||
"## 7. Conclusions and Key Insights\n",
|
||
"\n",
|
||
"Based on our comprehensive analysis of momentum trading strategies from Chapters 6 and 7, let's summarize the key findings and insights.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Create a comprehensive summary table\n",
|
||
"summary_data = {\n",
|
||
" 'Metric': [\n",
|
||
" 'Annual Return',\n",
|
||
" 'Volatility', \n",
|
||
" 'Sharpe Ratio',\n",
|
||
" 'Maximum Drawdown',\n",
|
||
" 'Calmar Ratio',\n",
|
||
" 'Win Rate',\n",
|
||
" 'Number of Trades/Rebalances'\n",
|
||
" ],\n",
|
||
" 'Time Series Momentum': [\n",
|
||
" f\"{ts_results['annual_return']:.2%}\",\n",
|
||
" f\"{ts_results['volatility']:.2%}\",\n",
|
||
" f\"{ts_results['sharpe_ratio']:.2f}\",\n",
|
||
" f\"{ts_results['max_drawdown']:.2%}\",\n",
|
||
" f\"{ts_calmar:.2f}\",\n",
|
||
" f\"{ts_results['win_rate']:.2%}\",\n",
|
||
" f\"{ts_results['num_trades']:.0f}\"\n",
|
||
" ],\n",
|
||
" 'Cross-Sectional Momentum': [\n",
|
||
" f\"{cs_results['annual_return']:.2%}\",\n",
|
||
" f\"{cs_results['volatility']:.2%}\",\n",
|
||
" f\"{cs_results['sharpe_ratio']:.2f}\",\n",
|
||
" f\"{cs_results['max_drawdown']:.2%}\",\n",
|
||
" f\"{cs_calmar:.2f}\",\n",
|
||
" f\"{cs_results['win_rate']:.2%}\",\n",
|
||
" f\"{cs_results['num_rebalances']:.0f}\"\n",
|
||
" ],\n",
|
||
" 'Treasury B&H': [\n",
|
||
" f\"{market_annual:.2%}\",\n",
|
||
" f\"{market_vol:.2%}\",\n",
|
||
" f\"{market_sharpe:.2f}\",\n",
|
||
" \"N/A\",\n",
|
||
" \"N/A\",\n",
|
||
" \"N/A\",\n",
|
||
" \"1\"\n",
|
||
" ],\n",
|
||
" 'Stock Market': [\n",
|
||
" f\"{cs_market_annual:.2%}\",\n",
|
||
" f\"{cs_market_vol:.2%}\",\n",
|
||
" f\"{cs_market_sharpe:.2f}\",\n",
|
||
" \"N/A\",\n",
|
||
" \"N/A\",\n",
|
||
" \"N/A\",\n",
|
||
" \"1\"\n",
|
||
" ]\n",
|
||
"}\n",
|
||
"\n",
|
||
"summary_df = pd.DataFrame(summary_data)\n",
|
||
"print(\"📊 COMPREHENSIVE PERFORMANCE SUMMARY\")\n",
|
||
"print(\"=\" * 100)\n",
|
||
"print(summary_df.to_string(index=False))\n",
|
||
"\n",
|
||
"print(f\"\\n\\n🎯 KEY INSIGHTS FROM MOMENTUM ANALYSIS:\")\n",
|
||
"print(\"=\" * 80)\n",
|
||
"\n",
|
||
"print(f\"\\n1. 📈 PERFORMANCE COMPARISON:\")\n",
|
||
"if ts_results['sharpe_ratio'] > cs_results['sharpe_ratio']:\n",
|
||
" better_strategy = \"Time Series Momentum\"\n",
|
||
" better_sharpe = ts_results['sharpe_ratio']\n",
|
||
" worse_sharpe = cs_results['sharpe_ratio']\n",
|
||
"else:\n",
|
||
" better_strategy = \"Cross-Sectional Momentum\"\n",
|
||
" better_sharpe = cs_results['sharpe_ratio']\n",
|
||
" worse_sharpe = ts_results['sharpe_ratio']\n",
|
||
"\n",
|
||
"print(f\" • {better_strategy} shows superior risk-adjusted returns\")\n",
|
||
"print(f\" • Sharpe ratio difference: {better_sharpe:.2f} vs {worse_sharpe:.2f}\")\n",
|
||
"print(f\" • Both strategies {'outperform' if min(ts_results['sharpe_ratio'], cs_results['sharpe_ratio']) > max(market_sharpe, cs_market_sharpe) else 'underperform'} their respective benchmarks\")\n",
|
||
"\n",
|
||
"print(f\"\\n2. 🎲 STATISTICAL SIGNIFICANCE:\")\n",
|
||
"if 'error' not in ts_tests and 'error' not in cs_tests:\n",
|
||
" print(f\" • Time Series Momentum p-value: {ts_tests['p_value_ttest']:.4f}\")\n",
|
||
" print(f\" • Cross-Sectional Momentum p-value: {cs_tests['p_value_ttest']:.4f}\")\n",
|
||
" \n",
|
||
" significant_strategies = []\n",
|
||
" if ts_tests['p_value_ttest'] < 0.05:\n",
|
||
" significant_strategies.append(\"Time Series\")\n",
|
||
" if cs_tests['p_value_ttest'] < 0.05:\n",
|
||
" significant_strategies.append(\"Cross-Sectional\")\n",
|
||
" \n",
|
||
" if significant_strategies:\n",
|
||
" print(f\" • Statistically significant strategies: {', '.join(significant_strategies)}\")\n",
|
||
" else:\n",
|
||
" print(f\" • Neither strategy shows statistical significance at 95% confidence\")\n",
|
||
"\n",
|
||
"print(f\"\\n3. 📉 RISK CHARACTERISTICS:\")\n",
|
||
"print(f\" • Time Series max drawdown: {ts_results['max_drawdown']:.2%}\")\n",
|
||
"print(f\" • Cross-Sectional max drawdown: {cs_results['max_drawdown']:.2%}\")\n",
|
||
"print(f\" • {'Time Series' if ts_results['max_drawdown'] > cs_results['max_drawdown'] else 'Cross-Sectional'} momentum shows higher maximum drawdown\")\n",
|
||
"\n",
|
||
"print(f\"\\n4. 🔄 TRADING FREQUENCY:\")\n",
|
||
"print(f\" • Time Series: {ts_results['num_trades']:.0f} trades over the period\")\n",
|
||
"print(f\" • Cross-Sectional: {cs_results['num_rebalances']:.0f} rebalances over the period\")\n",
|
||
"print(f\" • Cross-sectional requires more frequent rebalancing\")\n",
|
||
"\n",
|
||
"print(f\"\\n5. 💡 PRACTICAL IMPLICATIONS:\")\n",
|
||
"print(f\" • Momentum effects {'appear' if any([ts_tests.get('p_value_ttest', 1) < 0.1, cs_tests.get('p_value_ttest', 1) < 0.1]) else 'do not appear'} to be present in the data\")\n",
|
||
"print(f\" • Time series momentum may be easier to implement (fewer trades)\")\n",
|
||
"print(f\" • Cross-sectional momentum requires larger universe of assets\")\n",
|
||
"print(f\" • Both strategies show {'positive' if min(ts_results['annual_return'], cs_results['annual_return']) > 0 else 'negative'} excess returns over the analysis period\")\n",
|
||
"\n",
|
||
"print(f\"\\n6. 📚 ALIGNMENT WITH ACADEMIC LITERATURE:\")\n",
|
||
"print(f\" • Results {'are consistent' if max(ts_results['sharpe_ratio'], cs_results['sharpe_ratio']) > 0.5 else 'show mixed evidence'} with momentum literature\")\n",
|
||
"print(f\" • Time series momentum (Chapter 7) shows {'strong' if ts_results['sharpe_ratio'] > 1 else 'moderate' if ts_results['sharpe_ratio'] > 0.5 else 'weak'} performance\")\n",
|
||
"print(f\" • Cross-sectional momentum (Chapter 6) shows {'strong' if cs_results['sharpe_ratio'] > 1 else 'moderate' if cs_results['sharpe_ratio'] > 0.5 else 'weak'} performance\")\n",
|
||
"\n",
|
||
"print(f\"\\n⚠️ IMPORTANT DISCLAIMERS:\")\n",
|
||
"print(f\" • Results may be influenced by synthetic data if real data unavailable\")\n",
|
||
"print(f\" • Past performance does not guarantee future results\")\n",
|
||
"print(f\" • Transaction costs and market impact not included in analysis\")\n",
|
||
"print(f\" • Strategies may be subject to regime changes and crowding effects\")\n",
|
||
"\n",
|
||
"print(f\"\\n🔬 NEXT STEPS FOR RESEARCH:\")\n",
|
||
"print(f\" • Test strategies across different time periods and market regimes\")\n",
|
||
"print(f\" • Include transaction costs and realistic trading constraints\")\n",
|
||
"print(f\" • Analyze factor exposures and risk decomposition\")\n",
|
||
"print(f\" • Implement portfolio optimization and risk management overlays\")\n"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"language_info": {
|
||
"name": "python"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
}
|