Files
ftdt-quant-lab/notebooks/03_portfolio.ipynb
T
ramseshk 0446443d36 feat: creative alpha models + portfolio layer targeting Sharpe > 1.5
New strategies:
  - Cross-Sectional Momentum: long top-N, short bottom-N across HL universe
  - Spot-Perp Basis Arbitrage: delta-neutral spot vs perp price gap trading
  - Regime-Switching Ensemble: dynamically allocates strategies by market regime
  - Portfolio Construction: risk parity, vol targeting, correlation penalty

Infrastructure:
  - DuckDBDataProvider: real tick/candle data for backtests (replaces synthetic)
  - Walk-Forward Validation: systematic IS/OOS across all 12 strategies
  - 3 Jupyter research notebooks (EDA, strategy research, portfolio)

Pipeline integration:
  - deploy.py registry, sweep_runner, vbt_runner all updated
  - fee_tiers support for new strategies
  - All modules syntax-validated and import-tested
2026-08-12 12:26:29 +08:00

382 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "beecb70e",
"metadata": {},
"source": [
"# FTDT Quant Lab — Portfolio Construction & Risk Management\n",
"\n",
"**Goal:** Combine multiple independent alpha sources into a single risk-managed portfolio targeting Sharpe > 1.5.\n",
"\n",
"**Key concepts:**\n",
"1. **Diversification**: N independent strategies with low correlation → Sharpe scales ~√N\n",
"2. **Risk Parity**: Allocate capital inversely proportional to strategy volatility\n",
"3. **Volatility Targeting**: Scale total portfolio to target annualized vol (e.g., 20%)\n",
"4. **Correlation Penalty**: Reduce allocation to redundant (highly correlated) strategies\n",
"5. **Regime Adaptation**: Shift strategy weights based on market conditions\n",
"6. **Drawdown Control**: Kill switch at strategy and portfolio level\n",
"\n",
"**Math:**\n",
"Portfolio Sharpe ≈ √N × avg(individual Sharpe) × √(1 - avg_correlation)\n",
"\n",
"If we have 5 strategies with average individual Sharpe 2.0 and average correlation 0.2:\n",
"Portfolio Sharpe ≈ √5 × 2.0 × √(0.8) ≈ 4.0\n",
"\n",
"This is the engine. 5 good strategies + low correlation → Sharpe >> 1.5."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "36f62f63",
"metadata": {},
"outputs": [],
"source": [
"# Setup\n",
"import sys; sys.path.insert(0, str(Path.cwd().parent))\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"from strategies.portfolio import PortfolioConstructor, StrategyAllocation\n",
"from strategies.regime_ensemble import RegimeDetector, RegimeEnsemble, STRATEGY_REGIME_AFFINITY\n",
"from config.fee_tiers import get_perp_fees, get_strategy_fee_model\n",
"\n",
"sns.set_theme(style=\"darkgrid\")\n",
"plt.rcParams[\"figure.figsize\"] = (14, 6)\n"
]
},
{
"cell_type": "markdown",
"id": "d061b751",
"metadata": {},
"source": [
"## 1. Strategy × Regime Affinity Matrix\n",
"\n",
"The regime-switching ensemble selects strategies based on their known\n",
"performance characteristics in each market regime."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bcd7d239",
"metadata": {},
"outputs": [],
"source": [
"affinity = STRATEGY_REGIME_AFFINITY\n",
"affinity_df = pd.DataFrame(affinity).T\n",
"\n",
"fig, ax = plt.subplots(figsize=(14, 8))\n",
"sns.heatmap(affinity_df, annot=True, fmt='.1f', cmap='YlOrRd', \n",
" vmin=0, vmax=1, ax=ax, cbar_kws={'label': 'Affinity Score'})\n",
"ax.set_title('Strategy × Regime Affinity Matrix')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Best strategy per regime\n",
"print(\"Best strategy for each regime:\")\n",
"for regime in affinity_df.index:\n",
" best = affinity_df.loc[regime].idxmax()\n",
" score = affinity_df.loc[regime, best]\n",
" print(f\" {regime:20s} → {best:20s} (score: {score:.1f})\")\n"
]
},
{
"cell_type": "markdown",
"id": "a7c2cdc3",
"metadata": {},
"source": [
"## 2. Portfolio Construction Simulation\n",
"\n",
"Simulate the portfolio with 7 strategies, each running independently.\n",
"Use correlated returns to test the diversification benefits."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6cae17a9",
"metadata": {},
"outputs": [],
"source": [
"# Simulated returns for 7 strategies with some correlation\n",
"np.random.seed(42)\n",
"n_bars = 1000\n",
"\n",
"strategy_names = [\"pairs\", \"hurst_vpin\", \"cross_sectional\", \"grid_mm\",\n",
" \"spot_perp_basis\", \"momentum\", \"mean_rev\"]\n",
"\n",
"# Generate correlated returns\n",
"base_returns = np.random.randn(n_bars, 3) * 0.005\n",
"\n",
"returns = {}\n",
"returns[\"pairs\"] = base_returns[:, 0] * 0.6 + np.random.randn(n_bars) * 0.003\n",
"returns[\"hurst_vpin\"] = base_returns[:, 1] * 0.8 + np.random.randn(n_bars) * 0.004\n",
"returns[\"cross_sectional\"] = base_returns[:, 0] * 0.3 + base_returns[:, 1] * 0.5 + np.random.randn(n_bars) * 0.003\n",
"returns[\"grid_mm\"] = base_returns[:, 2] * 0.4 + np.random.randn(n_bars) * 0.002\n",
"returns[\"spot_perp_basis\"] = np.random.randn(n_bars) * 0.003 # uncorrelated\n",
"returns[\"momentum\"] = base_returns[:, 1] * 0.7 + np.random.randn(n_bars) * 0.004\n",
"returns[\"mean_rev\"] = -base_returns[:, 0] * 0.5 + np.random.randn(n_bars) * 0.003\n",
"\n",
"# Add positive drift for profitable strategies\n",
"for name, r in returns.items():\n",
" returns[name] = r + 0.0005 # Small positive edge\n",
"\n",
"# Compute correlation\n",
"ret_df = pd.DataFrame(returns)\n",
"corr = ret_df.corr()\n",
"sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0,\n",
" vmin=-1, vmax=1, square=True)\n",
"plt.title('Strategy Return Correlation Matrix')\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2ab6b2b1",
"metadata": {},
"outputs": [],
"source": [
"# Build and simulate portfolio\n",
"pf = PortfolioConstructor(\n",
" capital=100_000,\n",
" vol_target=0.20,\n",
" max_correlation=0.70,\n",
" max_drawdown_stop=0.15,\n",
" portfolio_mdd_stop=0.10,\n",
")\n",
"\n",
"for name in strategy_names:\n",
" pf.register_strategy(name)\n",
"\n",
"# Feed returns\n",
"for i in range(n_bars):\n",
" for name in strategy_names:\n",
" pf.update_returns(name, [returns[name][i]])\n",
" pf.update_portfolio_value({\n",
" name: returns[name][i] * pf.capital * 0.1\n",
" for name in strategy_names\n",
" })\n",
"\n",
"# Portfolio metrics\n",
"metrics = pf.summary()\n",
"print(f\"=== Portfolio Metrics ===\")\n",
"print(f\"Total Equity: ${metrics.total_equity:,.2f}\")\n",
"print(f\"Total PnL: ${metrics.total_pnl:,.2f} ({metrics.total_pnl_pct*100:.1f}%)\")\n",
"print(f\"Volatility: {metrics.vol_20d*100:.1f}%\")\n",
"print(f\"Sharpe Ratio: {metrics.sharpe:.2f}\")\n",
"print(f\"Sortino Ratio: {metrics.sortino:.2f}\")\n",
"print(f\"Max Drawdown: {metrics.max_drawdown_pct*100:.1f}%\")\n",
"print(f\"Win Rate: {metrics.win_rate*100:.0f}%\")\n",
"\n",
"# Equity curve\n",
"eq = list(pf.portfolio_equity_history)\n",
"plt.plot(eq, linewidth=0.5)\n",
"plt.title('Portfolio Equity Curve')\n",
"plt.ylabel('Equity ($)')\n",
"plt.xlabel('Bar')\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "ff45a282",
"metadata": {},
"source": [
"## 3. Risk Decomposition\n",
"\n",
"Where is the risk coming from? Which strategies contribute most to drawdowns?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cad69d26",
"metadata": {},
"outputs": [],
"source": [
"# Risk attribution per strategy\n",
"allocations = pf.compute_allocations({\"BTC\": 100000, \"ETH\": 3500, \"SOL\": 200, \"HYPE\": 10})\n",
"print(\"=== Portfolio Allocation ===\")\n",
"print(f\"{'Strategy':<20} {'Weight':>8} {'Allocation':>12} {'Vol 20d':>10}\")\n",
"print(\"-\" * 55)\n",
"for name in strategy_names:\n",
" alloc = allocations.get(name, 0)\n",
" st = pf.strategies.get(name)\n",
" if st:\n",
" print(f\"{name:<20} {st.weight:>7.1%} ${alloc:>10,.0f} {st.vol_20d*100:>8.1f}%\")\n",
"total_alloc = sum(allocations.values())\n",
"print(f\"\\n{'Total':<20} {' ':>8} ${total_alloc:>10,.0f}\")\n",
"print(f\"Reserve: ${pf.capital - total_alloc:>10,.0f}\")\n",
"\n",
"# Drawdown per strategy\n",
"print(f\"\\n=== Drawdown Analysis ===\")\n",
"for name, st in pf.strategies.items():\n",
" if st.peak_equity > 0:\n",
" dd = (1.0 - st.equity / st.peak_equity) * 100\n",
" print(f\" {name:<20s}: DD={dd:5.1f}% | Equity=${st.equity:,.0f} | Peak=${st.peak_equity:,.0f}\")\n"
]
},
{
"cell_type": "markdown",
"id": "04372d8b",
"metadata": {},
"source": [
"## 4. Regime-Adaptive Allocation\n",
"\n",
"Test the regime-switching ensemble: how do weights shift across regimes?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a628f24",
"metadata": {},
"outputs": [],
"source": [
"# Simulate different regimes\n",
"ensemble = RegimeEnsemble()\n",
"\n",
"# Seed with some signals\n",
"for name in strategy_names:\n",
" ensemble.update_strategy_signal(name, \"BUY\", 0.6 + np.random.random() * 0.2)\n",
"\n",
"# Test in different regimes by feeding artificial price patterns\n",
"np.random.seed(42)\n",
"\n",
"print(\"=== Strategy Weights by Regime ===\\n\")\n",
"\n",
"# TRENDING: strong upward drift\n",
"for i in range(200):\n",
" ensemble.feed_price(100000 + i * 50 + np.random.randn() * 200)\n",
"trending_weights = ensemble.compute_weights()\n",
"print(\"TRENDING:\")\n",
"for s, w in sorted(trending_weights.items(), key=lambda x: x[1], reverse=True)[:5]:\n",
" print(f\" {s:20s}: {w:.1%}\")\n",
"\n",
"# Reset detector and test MEAN_REVERTING\n",
"ensemble.detector.prices.clear()\n",
"for i in range(200):\n",
" px = 100000 + np.sin(i * 0.1) * 2000 + np.random.randn() * 500\n",
" ensemble.feed_price(px)\n",
"mr_weights = ensemble.compute_weights()\n",
"print(\"\\nMEAN_REVERTING:\")\n",
"for s, w in sorted(mr_weights.items(), key=lambda x: x[1], reverse=True)[:5]:\n",
" print(f\" {s:20s}: {w:.1%}\")\n",
"\n",
"# Compare\n",
"print(f\"\\n=== Weight Shift Analysis ===\")\n",
"for name in sorted(strategy_names):\n",
" tw = trending_weights.get(name, 0)\n",
" mw = mr_weights.get(name, 0)\n",
" shift = mw - tw\n",
" direction = \"▲ MR\" if shift > 0.01 else (\"▼ TREND\" if shift < -0.01 else \"— same\")\n",
" print(f\" {name:20s}: TR={tw:.2%} MR={mw:.2%} ({direction})\")\n"
]
},
{
"cell_type": "markdown",
"id": "e5d9646a",
"metadata": {},
"source": [
"## 5. Sharpe Decomposition\n",
"\n",
"Target: Sharpe > 1.5. How many strategies do we need?\n",
"\n",
"```\n",
"Portfolio Sharpe = √N × avg(individual Sharpe) × √(1 - avg_correlation)\n",
" = √N × Sᵢ × √(1 - ρ̄)\n",
"```\n",
"\n",
"**Scenarios:**\n",
"| N strategies | Avg Sharpe | Avg Corr | Portfolio Sharpe | Target? |\n",
"|-------------|-----------|---------|-----------------|---------| \n",
"| 3 | 1.5 | 0.3 | 2.17 | ✅ |\n",
"| 5 | 1.0 | 0.2 | 2.00 | ✅ |\n",
"| 5 | 0.8 | 0.5 | 1.26 | ❌ |\n",
"| 7 | 1.0 | 0.3 | 2.21 | ✅ |\n",
"| 7 | 0.7 | 0.2 | 1.66 | ✅ |\n",
"\n",
"**Conclusion:** With 5-7 strategies averaging 1.0 individual Sharpe and correlation below 0.3, we comfortably exceed Sharpe 1.5. The key is keeping correlation low — redundant strategies destroy the diversification benefit."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68e1dac1",
"metadata": {},
"outputs": [],
"source": [
"def portfolio_sharpe(n_strategies, avg_sharpe, avg_correlation):\n",
" return np.sqrt(n_strategies) * avg_sharpe * np.sqrt(1 - avg_correlation)\n",
"\n",
"# Parameter sweep\n",
"ns = range(2, 11)\n",
"sharpes = [0.5, 0.8, 1.0, 1.2, 1.5]\n",
"corrs = [0.1, 0.2, 0.3, 0.5]\n",
"\n",
"print(\"=== Portfolio Sharpe Projections ===\\n\")\n",
"print(f\"{'N':>3} | \", end=\"\")\n",
"for s in sharpes:\n",
" print(f\"Sᵢ={s:.1f} \", end=\"\")\n",
"print(\"| ρ̄=0.2\")\n",
"\n",
"for n in ns:\n",
" print(f\"{n:3d} | \", end=\"\")\n",
" for s in sharpes:\n",
" ps = portfolio_sharpe(n, s, 0.2)\n",
" marker = \" ✅\" if ps > 1.5 else \" \"\n",
" print(f\"{ps:5.2f}{marker} \", end=\"\")\n",
" print()\n",
"\n",
"print(f\"\\nTarget line: Sharpe > 1.50\")\n",
"print(f\"Bold numbers pass the target. Strategy: maximize N × Sᵢ × (1 - ρ̄)\")\n"
]
},
{
"cell_type": "markdown",
"id": "8f68c80b",
"metadata": {},
"source": [
"## 6. Deployment Pipeline\n",
"\n",
"The complete pipeline from idea → deployment:\n",
"\n",
"```\n",
"IDEA → Signal Generation → VBT Backtest → Walk-Forward → \n",
"→ DSR/PSR/Haircut → QuantVerdict → \n",
"→ Paper Trading (24h+) → Queue Simulation → \n",
"→ LIVE (1/10 size, daily PnL stop)\n",
"```\n",
"\n",
"**Operational rules:**\n",
"- Never deploy more than 2 new strategies simultaneously\n",
"- Each strategy starts at 1/10 target size for 1 week\n",
"- Daily PnL stop: halt strategy if -2% in one day\n",
"- Weekly review: check Sharpe, DD, win rate vs. backtest\n",
"- Monthly rebalancing: re-run walk-forward to update parameters\n",
"- Kill switch: any strategy -15% from peak → disabled\n",
"- Portfolio kill: total equity -10% from peak → all strategies paused"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}