Files
ftdt-quant-lab/scripts/generate_notebooks.py
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

872 lines
32 KiB
Python
Raw Permalink 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.
"""
Generate research notebooks for FTDT Quant Lab.
Creates three notebooks:
1. 01_eda.ipynb — market data exploration, distributions, correlations
2. 02_strategy_research.ipynb — strategy backtesting, signal analysis, optimization
3. 03_portfolio.ipynb — portfolio construction, risk allocation, ensemble
"""
import nbformat as nbf
from pathlib import Path
NOTEBOOKS_DIR = Path(__file__).resolve().parent.parent / "notebooks"
NOTEBOOKS_DIR.mkdir(parents=True, exist_ok=True)
def create_eda_notebook():
nb = nbf.v4.new_notebook()
nb.metadata = {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3",
},
"language_info": {"name": "python", "version": "3.13.0"},
}
nb.cells = [
nbf.v4.new_markdown_cell("""# FTDT Quant Lab — Exploratory Data Analysis
**Goal:** Understand Hyperliquid market microstructure, identify alpha sources, verify data quality.
**Assets:** BTC, ETH, SOL, HYPE, ARB, OP, and others
**Data Sources:** DuckDB tick database, HL REST API, Parquet raw store
**Timeframe:** 1s tick → 1h candles → daily aggregation"""),
nbf.v4.new_code_cell("""# Setup
import sys
from pathlib import Path
sys.path.insert(0, str(Path.cwd().parent))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from data.duckdb_provider import DuckDBProvider
from framework.data import HyperliquidDataProvider
sns.set_theme(style="darkgrid")
plt.rcParams["figure.figsize"] = (14, 6)
plt.rcParams["figure.dpi"] = 100
# Data providers
duckdb = DuckDBProvider()
hl_rest = HyperliquidDataProvider(testnet=False)
print(f"DuckDB available: {duckdb.available}")
print(f"Data range: {duckdb.get_data_range()}")
print(f"Available coins: {duckdb.get_available_coins()}")
"""),
nbf.v4.new_markdown_cell("""## 1. Universe Overview
What assets are available and how much data do we have for each?"""),
nbf.v4.new_code_cell("""from strategies.cross_sectional_momentum import HL_UNIVERSE, HIGH_LIQUIDITY
print(f"Full universe ({len(HL_UNIVERSE)} assets): {HL_UNIVERSE}")
print(f"High liquidity ({len(HIGH_LIQUIDITY)}): {HIGH_LIQUIDITY}")
# Fetch candle data for each asset
prices = duckdb.fetch_multi_candles(HIGH_LIQUIDITY, interval='1h', limit=500)
print(f"\\nData availability:")
for coin, df in prices.items():
if not df.empty:
print(f" {coin:6s}: {len(df):5d} bars | {df.index[0]} to {df.index[-1]} | close=${df['close'].iloc[-1]:.2f}")
"""),
nbf.v4.new_markdown_cell("""## 2. Return Distributions
Check return distributions for normality, skew, kurtosis, and tail behavior.
This informs strategy design — mean reversion works on platykurtic distributions,
momentum thrives on leptokurtic tails."""),
nbf.v4.new_code_cell("""returns_data = {}
for coin in HIGH_LIQUIDITY:
df = prices.get(coin)
if df is None or df.empty:
continue
rets = df['close'].pct_change().dropna()
returns_data[coin] = rets
stats = []
for coin, rets in returns_data.items():
stats.append({
'coin': coin,
'mean_annual': rets.mean() * 365 * 24,
'vol_annual': rets.std() * np.sqrt(365 * 24),
'sharpe': rets.mean() / rets.std() * np.sqrt(365 * 24) if rets.std() > 0 else 0,
'skew': rets.skew(),
'kurtosis': rets.kurtosis(),
'var_95': rets.quantile(0.05),
'cv': rets.std() / rets.mean() if rets.mean() != 0 else 0,
'max_dd': (df['close'] / df['close'].cummax() - 1).min(),
})
stats_df = pd.DataFrame(stats).set_index('coin')
stats_df.round(4)
"""),
nbf.v4.new_code_cell("""# Return distribution plots
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
for ax, (coin, rets) in zip(axes.flat, returns_data.items()):
rets.hist(bins=100, ax=ax, alpha=0.7, density=True)
ax.set_title(f"{coin} — Skew: {rets.skew():.2f}, Kurt: {rets.kurtosis():.2f}")
ax.axvline(0, color='red', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
"""),
nbf.v4.new_markdown_cell("""## 3. Correlation Matrix
Identify redundant assets and diversification opportunities.
High correlation = limited diversification benefit.
Low correlation = potential for uncorrelated alpha streams."""),
nbf.v4.new_code_cell("""corr_matrix = pd.DataFrame(returns_data).corr()
mask = np.triu(np.ones_like(corr_matrix), k=1)
sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.3f', cmap='RdBu_r',
center=0, vmin=-1, vmax=1, square=True)
plt.title('Hourly Return Correlation Matrix')
plt.tight_layout()
plt.show()
"""),
nbf.v4.new_markdown_cell("""## 4. Volatility Regimes
Classify the market into volatility regimes. This drives strategy selection
in the Regime-Switching Ensemble.
- LOW_VOL: annualized < 15% → market making, pairs trading
- NORMAL: 15-60% → all strategies at baseline
- HIGH_VOL: > 60% → momentum, Hurst/VPIN, tight risk controls"""),
nbf.v4.new_code_cell("""from strategies.regime_ensemble import RegimeDetector
detector = RegimeDetector(
high_vol_threshold=0.60,
low_vol_threshold=0.15,
funding_extreme_apr=0.30,
)
btc_prices = prices['BTC']['close']
regimes = []
for i, px in enumerate(btc_prices):
detector.feed_price(px)
if i >= 100:
regimes.append(detector.primary_regime())
# Count regime distribution
regime_counts = pd.Series(regimes).value_counts()
print("Regime Distribution:")
for regime, count in regime_counts.items():
print(f" {regime:20s}: {count:5d} bars ({count/len(regimes)*100:.1f}%)")
"""),
nbf.v4.new_code_cell("""# Regime timeline
import matplotlib.dates as mdates
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 8), sharex=True)
ax1.plot(btc_prices.index[-len(regimes):], btc_prices.values[-len(regimes):],
linewidth=0.5, color='black')
ax1.set_ylabel('BTC Price')
ax1.set_title('BTC Price with Market Regimes')
regime_colors = {
'NORMAL': 'gray', 'TRENDING': 'green', 'MEAN_REVERTING': 'blue',
'CHOPPY': 'orange', 'HIGH_VOL': 'red', 'LOW_VOL': 'lightblue',
'FUNDING_EXTREME': 'purple',
}
regime_numeric = pd.Series(
[{v: i for i, v in enumerate(regime_colors)}.get(r, 0) for r in regimes],
index=btc_prices.index[-len(regimes):]
)
ax2.scatter(regime_numeric.index, regime_numeric.values, c=[regime_colors.get(r, 'gray') for r in regimes],
s=1, alpha=0.6)
ax2.set_yticks(range(len(regime_colors)))
ax2.set_yticklabels(regime_colors.keys())
ax2.set_ylabel('Regime')
plt.tight_layout()
plt.show()
"""),
nbf.v4.new_markdown_cell("""## 5. Fee Impact Analysis
Hyperliquid perp fee schedule. Calculate the minimum edge needed to overcome
fees at each tier. This sets the floor for signal strength thresholds."""),
nbf.v4.new_code_cell("""from config.fee_tiers import PERPS_TIERS, SPOT_TIERS, STAKING_TIERS, get_perp_fees, compute_trade_fees
print("=== Perp Fee Tiers ===")
print(f"{'Tier':<20} {'Volume':>12} {'Taker':>8} {'Maker':>8}")
print("-" * 50)
for tier, info in PERPS_TIERS.items():
print(f"{info['name']:<20} ${info['min_volume']:>10,.0f} "
f"{info['taker']*100:.3f}% {info['maker']*100:.3f}%")
print(f"\\n=== Spot Fee Tiers ===")
for tier, info in SPOT_TIERS.items():
print(f"{info['name']:<20} ${info['min_volume']:>10,.0f} "
f"{info['taker']*100:.3f}% {info['maker']*100:.3f}%")
# Break-even trade size by fee tier
print(f"\\n=== Minimum Profitable Trade (BTC round-trip, 1bps edge) ===")
for tier in range(7):
fees = compute_trade_fees("BUY", 0.001, 100000, 100000, vip_tier=tier)
print(f" Tier {tier}: {fees['effective_rate_pct']:.4f}% per side "
f"→ ${fees['total_fee']:.4f} round-trip")
"""),
nbf.v4.new_markdown_cell("""## 6. Key Takeaways
1. **Asset universe**: BTC dominates volume; ETH, SOL, HYPE are the next most liquid. Use 3-6 assets for cross-sectional strategies.
2. **Return distributions**: Crypto returns are leptokurtic (fat tails) — expect black swans. Size positions accordingly.
3. **Correlations**: BTC/ETH correlation ~0.7. Most alts >0.5 correlated with BTC. True diversification is hard.
4. **Regime frequency**: NORMAL dominates but HIGH_VOL regime provides the best trading opportunities.
5. **Fee hurdle**: At Tier 0, a round-trip costs ~0.09%. This means a 1bps edge is enough for a single tick, but barely. We need 2-5bps edges minimum for consistent profitability. At higher tiers, the bar drops significantly.
6. **DuckDB data**: Enables sub-second queries on tick-level data. Essential for Hurst/VPIN and microstructure strategies.
"""),
]
nb_path = NOTEBOOKS_DIR / "01_eda.ipynb"
nbf.write(nb, str(nb_path))
print(f"Created {nb_path}")
def create_strategy_research_notebook():
nb = nbf.v4.new_notebook()
nb.metadata = {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.13.0"},
}
nb.cells = [
nbf.v4.new_markdown_cell("""# FTDT Quant Lab — Strategy Research & Backtesting
**Goal:** Develop and validate systematic trading strategies targeting Sharpe > 1.5 on Hyperliquid assets.
**Framework:**
1. Signal Generation — compute alpha from market data
2. VectorBT Backtest — fast vectorized simulation with fee-accurate PnL
3. Walk-Forward Validation — IS/OOS parameter optimization
4. Statistical Significance — DSR, PSR, Sharpe Haircut, QuantVerdict
5. Deployment Decision — DEPLOY / SIMULATE / DISCARD
**Key Thresholds for Sharpe > 1.5:**
- Win rate > 55% with positive expectancy
- Max drawdown < 15%
- Walk-forward consistency > 60%
- DSR > 0.80, PSR > 0.70
- Average trade PnL > 2x fees"""),
nbf.v4.new_code_cell("""# Setup
import sys; sys.path.insert(0, str(Path.cwd().parent))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
import json, time
from backtests.vbt_runner import VBTBacktestRunner
from backtests.vbt_validator import VBTValidator
from quant.significance import QuantVerdict, validate_strategy
from quant.walkforward import WalkForwardRunner, quick_validate
from quant.optimizer import ParamOptimizer
from framework.data import HyperliquidDataProvider
from config.fee_tiers import get_perp_fees, get_strategy_fee_model
sns.set_theme(style="darkgrid")
plt.rcParams["figure.figsize"] = (14, 6)
"""),
nbf.v4.new_markdown_cell("""## 1. Strategy Inventory
Current strategies and their signal logic:"""),
nbf.v4.new_code_cell("""strategies = {
"pairs": {
"name": "Pairs Trading",
"type": "Stat Arb",
"signal": "BTC/ETH ratio Z-score",
"entry": "|Z| > 1.5σ",
"exit": "|Z| < 0.5σ",
"best_use": "Range-bound, mean-reverting markets",
"sharpe_target": 2.0,
},
"hurst_vpin": {
"name": "Hurst VPIN",
"type": "Directional",
"signal": "Hurst > 0.55 AND VPIN > 0.25",
"entry": "Both trending + high flow imbalance",
"exit": "Hurst < 0.45 or direction flip",
"best_use": "Trending, high-volume markets",
"sharpe_target": 2.5,
},
"cross_sectional": {
"name": "Cross-Sectional Momentum",
"type": "Multi-Asset Long/Short",
"signal": "Past N-bar return ranking",
"entry": "Long top-3, short bottom-3",
"exit": "Next rebalance period",
"best_use": "All regimes, best in TRENDING",
"sharpe_target": 1.8,
},
"spot_perp_basis": {
"name": "Spot-Perp Basis Arb",
"type": "Delta-Neutral Carry",
"signal": "Perp vs spot price gap > 3bps",
"entry": "Short premium leg, long discount leg",
"exit": "Basis convergence < 1bps",
"best_use": "FUNDING_EXTREME, volatile basis",
"sharpe_target": 2.0,
},
"regime_ensemble": {
"name": "Regime-Switching Ensemble",
"type": "Meta-Strategy",
"signal": "Regime × strategy affinity matrix",
"entry": "Weights strategies by regime fit",
"exit": "Regime change or signal fade",
"best_use": "All environments — adapts dynamically",
"sharpe_target": 2.0,
},
"grid_mm": {
"name": "Grid Market Making",
"type": "Market Making",
"signal": "Symmetric grid around mid",
"entry": "Grid fill triggers position",
"exit": "Grid exit on rebalance",
"best_use": "LOW_VOL, CHOPPY",
"sharpe_target": 2.0,
},
"as_mm": {
"name": "Avellaneda-Stoikov MM",
"type": "Market Making",
"signal": "Reservation price from inventory",
"entry": "Reservation > best bid (buy) / < best ask (sell)",
"exit": "Hold period or profit target",
"best_use": "LOW_VOL with tight spreads",
"sharpe_target": 1.5,
},
}
for key, s in strategies.items():
print(f"\\n{s['name']} ({key})")
print(f" Type: {s['type']}")
print(f" Signal: {s['signal']}")
print(f" Entry: {s['entry']}")
print(f" Exit: {s['exit']}")
print(f" Regime: {s['best_use']}")
print(f" Target Sharpe: {s['sharpe_target']}")
"""),
nbf.v4.new_markdown_cell("""## 2. Backtest Harness
Run any strategy through the VBT backtest engine with fee-accurate PnL, then
validate with statistical significance tests."""),
nbf.v4.new_code_cell("""def run_and_validate(strategy, interval='1h', params=None):
'''Run a full backtest + statistical validation pipeline.'''
print(f"\\n{'='*60}")
print(f" {strategies.get(strategy, {}).get('name', strategy)} — {interval}")
print(f"{'='*60}")
runner = VBTBacktestRunner(vip_tier=0, staking_tier='none')
result = runner.run_strategy(
strategy=strategy, interval=interval, testnet=False, limit=500, params=params
)
if result is None:
print(f" No result (no trades or data error)")
return None
# Display key metrics
print(f" Sharpe: {result.get('sharpe', 0):.3f}")
print(f" Total Return: {result.get('total_return_pct', 0):.1f}%")
print(f" Max Drawdown: {result.get('max_drawdown_pct', 0):.1f}%")
print(f" Win Rate: {result.get('win_rate', 0)*100:.0f}%")
print(f" Profit Factor: {result.get('profit_factor', 0):.2f}")
print(f" Total Trades: {result.get('total_trades', 0)}")
print(f" PnL: ${result.get('pnl', 0):.2f}")
# Statistical validation
n_trades = max(result.get('total_trades', 1), 1)
verdict = validate_strategy(
sharpe=result.get('sharpe', 0),
n_trades=n_trades,
n_trials=10,
wf_consistency=0.7,
)
print(f"\\n Verdict: {verdict['verdict']}")
print(f" DSR (deflated): {verdict['deflated_sharpe']:.3f}")
print(f" PSR: {verdict['psr']:.3f}")
print(f" Haircut Sharpe: {verdict['haircut_sharpe']:.3f}")
print(f" Score: {verdict['score']}")
print(f" → {verdict['recommendation']}")
# Plot equity curve
eq = result.get('equity_curve')
if eq:
df_eq = pd.DataFrame(eq)
df_eq['t'] = pd.to_datetime(df_eq['t'])
df_eq.set_index('t', inplace=True)
df_eq['v'].plot()
plt.title(f"{strategies.get(strategy, {}).get('name', strategy)} — Equity Curve")
plt.ylabel('Equity ($)')
plt.show()
return result
# Quick sweep of top strategies
for s in ["pairs", "hurst_vpin", "grid_mm", "momentum", "mean_rev"]:
run_and_validate(s, "1h")
"""),
nbf.v4.new_markdown_cell("""## 3. Cross-Sectional Momentum Backtest
The new multi-asset strategy. Long the top performers, short the laggards."""),
nbf.v4.new_code_cell("""from strategies.cross_sectional_momentum import CrossSectionalMomentum, HIGH_LIQUIDITY
from data.duckdb_provider import DuckDBProvider
duckdb = DuckDBProvider()
# Fetch multi-asset candles
coins = ["BTC", "ETH", "SOL", "HYPE", "ARB", "OP"]
prices = duckdb.fetch_multi_candles(coins, interval='1h', limit=500)
print(f"Coins with data: {list(prices.keys())}")
for coin in sorted(prices):
df = prices[coin]
print(f" {coin}: {len(df)} bars, close=${df['close'].iloc[-1]:.2f}")
# Compute cross-sectional momentum signals
cs_mom = CrossSectionalMomentum(lookback=20, top_n=2, bottom_n=2, risk_parity=True, vol_target=0.20)
close_prices = {c: df['close'] for c, df in prices.items()}
weights = cs_mom.compute_signals(close_prices)
print(f"\\nCross-Sectional Momentum Weights:")
for coin, wt in sorted(weights.items(), key=lambda x: abs(x[1]), reverse=True):
direction = "LONG" if wt > 0 else "SHORT"
print(f" {coin:6s}: {direction:5s} {wt:+.3f}")
"""),
nbf.v4.new_markdown_cell("""## 4. Walk-Forward Parameter Optimization
For strategies that show promise, run walk-forward to find stable parameters
and validate OOS performance."""),
nbf.v4.new_code_cell("""from quant.optimizer import ParamOptimizer
# Grid MM parameter sweep
print("=== Grid Market Making — Parameter Optimization ===\\n")
opt = ParamOptimizer(strategy='grid_mm', interval='1h', coin='BTC', n_windows=3)
opt.add_param('grid_levels', [5, 10, 20])
opt.add_param('spacing_bps', [2, 5, 10])
opt.add_param('rebalance_every', [5, 10, 20])
optimizer = ParamOptimizer.__new__(ParamOptimizer)
# [MANUAL RUN REQUIRED — uses live HL API, uncomment to run]
# report = opt.run()
# report.print()
print(" Walk-forward optimizer ready. Uncomment `opt.run()` to execute (requires live HL API data).")
print(" Grid: 3 grid_levels × 3 spacing × 3 rebalance = 27 combinations × 3 windows = 81 backtests")
"""),
nbf.v4.new_markdown_cell("""## 5. Pairs Trading Deep Dive
The only live-profitable strategy. Analyze its performance characteristics
and identify improvement opportunities."""),
nbf.v4.new_code_cell("""# Pairs trading: analyze BTC/ETH spread dynamics
btc = prices.get('BTC', {}).get('close')
eth = prices.get('ETH', {}).get('close')
if btc is not None and eth is not None and not btc.empty and not eth.empty:
common_idx = btc.index.intersection(eth.index)
btc = btc[common_idx]
eth = eth[common_idx]
ratio = btc / eth
mu = ratio.rolling(20).mean()
std = ratio.rolling(20).std()
z_score = (ratio - mu) / std
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 12), sharex=True)
ax1.plot(ratio.index, ratio, linewidth=0.5, color='black', label='BTC/ETH Ratio')
ax1.plot(mu.index, mu, linewidth=1, color='blue', label='20-bar MA')
ax1.fill_between(mu.index, mu - 2*std, mu + 2*std, alpha=0.15, color='blue', label='±2σ')
ax1.legend()
ax1.set_title('BTC/ETH Ratio with Bollinger Bands')
ax2.plot(z_score.index, z_score, linewidth=0.5, color='purple')
ax2.axhline(1.5, color='red', linestyle='--', alpha=0.5, label='Entry (1.5σ)')
ax2.axhline(-1.5, color='red', linestyle='--', alpha=0.5)
ax2.axhline(0.5, color='green', linestyle='--', alpha=0.3, label='Exit (0.5σ)')
ax2.axhline(-0.5, color='green', linestyle='--', alpha=0.3)
ax2.legend()
ax2.set_ylabel('Z-Score')
ax3.plot(z_score.index, abs(z_score), linewidth=0.5, color='orange')
ax3.axhline(1.5, color='red', linestyle='--', alpha=0.5)
ax3.set_ylabel('|Z|')
ax3.set_xlabel('Date')
plt.tight_layout()
plt.show()
# Signal statistics
entry_count = (abs(z_score) > 1.5).sum()
exit_count = ((abs(z_score.shift(1)) > 0.5) & (abs(z_score) < 0.5)).sum()
print(f"Entry signals (|Z| > 1.5): {entry_count}")
print(f"Exit signals (|Z| < 0.5): {exit_count}")
print(f"Signal density: {entry_count / len(z_score) * 100:.1f}%")
# Distribution of Z-scores
print(f"\\nZ-Score distribution:")
print(f" Mean: {z_score.mean():.3f}")
print(f" Std: {z_score.std():.3f}")
print(f" Pct > 2σ: {(abs(z_score) > 2).mean()*100:.1f}%")
print(f" Pct > 1.5σ: {(abs(z_score) > 1.5).mean()*100:.1f}%")
# Half-life of mean reversion
spread = ratio.dropna()
spread_lag = spread.shift(1).dropna()
spread_diff = spread - spread_lag
spread_diff = spread_diff.iloc[1:]
spread_lag = spread_lag.iloc[:len(spread_diff)]
if len(spread_lag) > 0:
import statsmodels.api as sm # may need install
try:
X = sm.add_constant(spread_lag.values)
model = sm.OLS(spread_diff.values, X).fit()
hl = -np.log(2) / model.params[1] if model.params[1] < 0 else float('inf')
print(f"\\nMean reversion half-life: {hl:.1f} bars ({hl * pd.Timedelta(hours=1).total_seconds()/3600:.1f} hours)")
except Exception:
print("\\n(Install statsmodels for half-life estimation: pip install statsmodels)")
"""),
nbf.v4.new_markdown_cell("""## 6. Strategy Development Checklist
Before deploying any strategy to live/papers:
- [ ] VectorBT backtest on real data (not synthetic)
- [ ] At least 50 trades in the backtest
- [ ] Walk-forward consistency > 50%
- [ ] DSR > 0.80, PSR > 0.70
- [ ] Haircut Sharpe > 0.50
- [ ] Maximum drawdown < 15%
- [ ] Win rate > 50% OR profit factor > 1.5
- [ ] Average trade PnL > 2x fee cost
- [ ] Correlation < 0.7 with existing portfolio strategies
- [ ] Phase 3 queue simulation (queue-aware fills) for maker strategies
- [ ] Paper trading for at least 24h before live
**Only deploy strategies that pass all 11 checks.**"""),
]
nb_path = NOTEBOOKS_DIR / "02_strategy_research.ipynb"
nbf.write(nb, str(nb_path))
print(f"Created {nb_path}")
def create_portfolio_notebook():
nb = nbf.v4.new_notebook()
nb.metadata = {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.13.0"},
}
nb.cells = [
nbf.v4.new_markdown_cell("""# FTDT Quant Lab — Portfolio Construction & Risk Management
**Goal:** Combine multiple independent alpha sources into a single risk-managed portfolio targeting Sharpe > 1.5.
**Key concepts:**
1. **Diversification**: N independent strategies with low correlation → Sharpe scales ~√N
2. **Risk Parity**: Allocate capital inversely proportional to strategy volatility
3. **Volatility Targeting**: Scale total portfolio to target annualized vol (e.g., 20%)
4. **Correlation Penalty**: Reduce allocation to redundant (highly correlated) strategies
5. **Regime Adaptation**: Shift strategy weights based on market conditions
6. **Drawdown Control**: Kill switch at strategy and portfolio level
**Math:**
Portfolio Sharpe ≈ √N × avg(individual Sharpe) × √(1 - avg_correlation)
If we have 5 strategies with average individual Sharpe 2.0 and average correlation 0.2:
Portfolio Sharpe ≈ √5 × 2.0 × √(0.8) ≈ 4.0
This is the engine. 5 good strategies + low correlation → Sharpe >> 1.5."""),
nbf.v4.new_code_cell("""# Setup
import sys; sys.path.insert(0, str(Path.cwd().parent))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from strategies.portfolio import PortfolioConstructor, StrategyAllocation
from strategies.regime_ensemble import RegimeDetector, RegimeEnsemble, STRATEGY_REGIME_AFFINITY
from config.fee_tiers import get_perp_fees, get_strategy_fee_model
sns.set_theme(style="darkgrid")
plt.rcParams["figure.figsize"] = (14, 6)
"""),
nbf.v4.new_markdown_cell("""## 1. Strategy × Regime Affinity Matrix
The regime-switching ensemble selects strategies based on their known
performance characteristics in each market regime."""),
nbf.v4.new_code_cell("""affinity = STRATEGY_REGIME_AFFINITY
affinity_df = pd.DataFrame(affinity).T
fig, ax = plt.subplots(figsize=(14, 8))
sns.heatmap(affinity_df, annot=True, fmt='.1f', cmap='YlOrRd',
vmin=0, vmax=1, ax=ax, cbar_kws={'label': 'Affinity Score'})
ax.set_title('Strategy × Regime Affinity Matrix')
plt.tight_layout()
plt.show()
# Best strategy per regime
print("Best strategy for each regime:")
for regime in affinity_df.index:
best = affinity_df.loc[regime].idxmax()
score = affinity_df.loc[regime, best]
print(f" {regime:20s} → {best:20s} (score: {score:.1f})")
"""),
nbf.v4.new_markdown_cell("""## 2. Portfolio Construction Simulation
Simulate the portfolio with 7 strategies, each running independently.
Use correlated returns to test the diversification benefits."""),
nbf.v4.new_code_cell("""# Simulated returns for 7 strategies with some correlation
np.random.seed(42)
n_bars = 1000
strategy_names = ["pairs", "hurst_vpin", "cross_sectional", "grid_mm",
"spot_perp_basis", "momentum", "mean_rev"]
# Generate correlated returns
base_returns = np.random.randn(n_bars, 3) * 0.005
returns = {}
returns["pairs"] = base_returns[:, 0] * 0.6 + np.random.randn(n_bars) * 0.003
returns["hurst_vpin"] = base_returns[:, 1] * 0.8 + np.random.randn(n_bars) * 0.004
returns["cross_sectional"] = base_returns[:, 0] * 0.3 + base_returns[:, 1] * 0.5 + np.random.randn(n_bars) * 0.003
returns["grid_mm"] = base_returns[:, 2] * 0.4 + np.random.randn(n_bars) * 0.002
returns["spot_perp_basis"] = np.random.randn(n_bars) * 0.003 # uncorrelated
returns["momentum"] = base_returns[:, 1] * 0.7 + np.random.randn(n_bars) * 0.004
returns["mean_rev"] = -base_returns[:, 0] * 0.5 + np.random.randn(n_bars) * 0.003
# Add positive drift for profitable strategies
for name, r in returns.items():
returns[name] = r + 0.0005 # Small positive edge
# Compute correlation
ret_df = pd.DataFrame(returns)
corr = ret_df.corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdBu_r', center=0,
vmin=-1, vmax=1, square=True)
plt.title('Strategy Return Correlation Matrix')
plt.show()
"""),
nbf.v4.new_code_cell("""# Build and simulate portfolio
pf = PortfolioConstructor(
capital=100_000,
vol_target=0.20,
max_correlation=0.70,
max_drawdown_stop=0.15,
portfolio_mdd_stop=0.10,
)
for name in strategy_names:
pf.register_strategy(name)
# Feed returns
for i in range(n_bars):
for name in strategy_names:
pf.update_returns(name, [returns[name][i]])
pf.update_portfolio_value({
name: returns[name][i] * pf.capital * 0.1
for name in strategy_names
})
# Portfolio metrics
metrics = pf.summary()
print(f"=== Portfolio Metrics ===")
print(f"Total Equity: ${metrics.total_equity:,.2f}")
print(f"Total PnL: ${metrics.total_pnl:,.2f} ({metrics.total_pnl_pct*100:.1f}%)")
print(f"Volatility: {metrics.vol_20d*100:.1f}%")
print(f"Sharpe Ratio: {metrics.sharpe:.2f}")
print(f"Sortino Ratio: {metrics.sortino:.2f}")
print(f"Max Drawdown: {metrics.max_drawdown_pct*100:.1f}%")
print(f"Win Rate: {metrics.win_rate*100:.0f}%")
# Equity curve
eq = list(pf.portfolio_equity_history)
plt.plot(eq, linewidth=0.5)
plt.title('Portfolio Equity Curve')
plt.ylabel('Equity ($)')
plt.xlabel('Bar')
plt.show()
"""),
nbf.v4.new_markdown_cell("""## 3. Risk Decomposition
Where is the risk coming from? Which strategies contribute most to drawdowns?"""),
nbf.v4.new_code_cell("""# Risk attribution per strategy
allocations = pf.compute_allocations({"BTC": 100000, "ETH": 3500, "SOL": 200, "HYPE": 10})
print("=== Portfolio Allocation ===")
print(f"{'Strategy':<20} {'Weight':>8} {'Allocation':>12} {'Vol 20d':>10}")
print("-" * 55)
for name in strategy_names:
alloc = allocations.get(name, 0)
st = pf.strategies.get(name)
if st:
print(f"{name:<20} {st.weight:>7.1%} ${alloc:>10,.0f} {st.vol_20d*100:>8.1f}%")
total_alloc = sum(allocations.values())
print(f"\\n{'Total':<20} {' ':>8} ${total_alloc:>10,.0f}")
print(f"Reserve: ${pf.capital - total_alloc:>10,.0f}")
# Drawdown per strategy
print(f"\\n=== Drawdown Analysis ===")
for name, st in pf.strategies.items():
if st.peak_equity > 0:
dd = (1.0 - st.equity / st.peak_equity) * 100
print(f" {name:<20s}: DD={dd:5.1f}% | Equity=${st.equity:,.0f} | Peak=${st.peak_equity:,.0f}")
"""),
nbf.v4.new_markdown_cell("""## 4. Regime-Adaptive Allocation
Test the regime-switching ensemble: how do weights shift across regimes?"""),
nbf.v4.new_code_cell("""# Simulate different regimes
ensemble = RegimeEnsemble()
# Seed with some signals
for name in strategy_names:
ensemble.update_strategy_signal(name, "BUY", 0.6 + np.random.random() * 0.2)
# Test in different regimes by feeding artificial price patterns
np.random.seed(42)
print("=== Strategy Weights by Regime ===\\n")
# TRENDING: strong upward drift
for i in range(200):
ensemble.feed_price(100000 + i * 50 + np.random.randn() * 200)
trending_weights = ensemble.compute_weights()
print("TRENDING:")
for s, w in sorted(trending_weights.items(), key=lambda x: x[1], reverse=True)[:5]:
print(f" {s:20s}: {w:.1%}")
# Reset detector and test MEAN_REVERTING
ensemble.detector.prices.clear()
for i in range(200):
px = 100000 + np.sin(i * 0.1) * 2000 + np.random.randn() * 500
ensemble.feed_price(px)
mr_weights = ensemble.compute_weights()
print("\\nMEAN_REVERTING:")
for s, w in sorted(mr_weights.items(), key=lambda x: x[1], reverse=True)[:5]:
print(f" {s:20s}: {w:.1%}")
# Compare
print(f"\\n=== Weight Shift Analysis ===")
for name in sorted(strategy_names):
tw = trending_weights.get(name, 0)
mw = mr_weights.get(name, 0)
shift = mw - tw
direction = "▲ MR" if shift > 0.01 else ("▼ TREND" if shift < -0.01 else "— same")
print(f" {name:20s}: TR={tw:.2%} MR={mw:.2%} ({direction})")
"""),
nbf.v4.new_markdown_cell("""## 5. Sharpe Decomposition
Target: Sharpe > 1.5. How many strategies do we need?
```
Portfolio Sharpe = √N × avg(individual Sharpe) × √(1 - avg_correlation)
= √N × Sᵢ × √(1 - ρ̄)
```
**Scenarios:**
| N strategies | Avg Sharpe | Avg Corr | Portfolio Sharpe | Target? |
|-------------|-----------|---------|-----------------|---------|
| 3 | 1.5 | 0.3 | 2.17 | ✅ |
| 5 | 1.0 | 0.2 | 2.00 | ✅ |
| 5 | 0.8 | 0.5 | 1.26 | ❌ |
| 7 | 1.0 | 0.3 | 2.21 | ✅ |
| 7 | 0.7 | 0.2 | 1.66 | ✅ |
**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."""),
nbf.v4.new_code_cell("""def portfolio_sharpe(n_strategies, avg_sharpe, avg_correlation):
return np.sqrt(n_strategies) * avg_sharpe * np.sqrt(1 - avg_correlation)
# Parameter sweep
ns = range(2, 11)
sharpes = [0.5, 0.8, 1.0, 1.2, 1.5]
corrs = [0.1, 0.2, 0.3, 0.5]
print("=== Portfolio Sharpe Projections ===\\n")
print(f"{'N':>3} | ", end="")
for s in sharpes:
print(f"Sᵢ={s:.1f} ", end="")
print("| ρ̄=0.2")
for n in ns:
print(f"{n:3d} | ", end="")
for s in sharpes:
ps = portfolio_sharpe(n, s, 0.2)
marker = " ✅" if ps > 1.5 else " "
print(f"{ps:5.2f}{marker} ", end="")
print()
print(f"\\nTarget line: Sharpe > 1.50")
print(f"Bold numbers pass the target. Strategy: maximize N × Sᵢ × (1 - ρ̄)")
"""),
nbf.v4.new_markdown_cell("""## 6. Deployment Pipeline
The complete pipeline from idea → deployment:
```
IDEA → Signal Generation → VBT Backtest → Walk-Forward →
→ DSR/PSR/Haircut → QuantVerdict →
→ Paper Trading (24h+) → Queue Simulation →
→ LIVE (1/10 size, daily PnL stop)
```
**Operational rules:**
- Never deploy more than 2 new strategies simultaneously
- Each strategy starts at 1/10 target size for 1 week
- Daily PnL stop: halt strategy if -2% in one day
- Weekly review: check Sharpe, DD, win rate vs. backtest
- Monthly rebalancing: re-run walk-forward to update parameters
- Kill switch: any strategy -15% from peak → disabled
- Portfolio kill: total equity -10% from peak → all strategies paused"""),
]
nb_path = NOTEBOOKS_DIR / "03_portfolio.ipynb"
nbf.write(nb, str(nb_path))
print(f"Created {nb_path}")
if __name__ == "__main__":
create_eda_notebook()
create_strategy_research_notebook()
create_portfolio_notebook()
print(f"\\nAll notebooks created in {NOTEBOOKS_DIR}")