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
This commit is contained in:
ramseshk
2026-08-12 12:26:29 +08:00
parent d967301834
commit 0446443d36
14 changed files with 3942 additions and 10 deletions
+381
View File
@@ -0,0 +1,381 @@
{
"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
}