Portfolio Hybride IBKR (50%) + Binance (50%) — Phase 1 Research Notebook

Issue #1027 — Phase 1 complete : portefeuille agrege des 8 sous-stratégies (5 IBKR equities + 3 Binance crypto), matrice de correlation des returns mensuels, Sharpe net / MaxDD du blend après couts de transaction. Complete le sleeve crypto livre dans research.ipynb (#1179) en ajoutant le sleeve equities et l’analyse cross-sleeve.

Composition cible (cf README.md)

Sleeve Sous-stratégie Poids sleeve Poids portefeuille
IBKR 50% Framework_Composite_TrendWeather 30% 15.0%
IBKR 50% Framework_Composite_EMATrend 25% 12.5%
IBKR 50% SectorMomentum 20% 10.0%
IBKR 50% AllWeather v5 15% 7.5%
IBKR 50% EMA-Cross-Alpha 10% 5.0%
Binance 50% EMA-Cross-Crypto 50% 25.0%
Binance 50% Crypto-MultiCanal 30% 15.0%
Binance 50% HAR-RV-J vol-target BTC 20% 10.0%

Methodologie et caveats (honnetete)

  • Chaque sous-stratégie est modelisee comme un proxy recherche : règles de signal journalieres reproduisant la logique de la stratégie du depot (pas le code exact du backtest QC). Le backtest unifie exact via le framework QC (MultiAlphaModel) est l’objet de la Phase 2.
  • Pas de lookahead : poids decides en t-1, appliques au return t (shift(1)).
  • Couts de transaction par notionnel traite (un sens) : equities 10 bps (5 fee IBKR + 5 slippage), crypto 15 bps (10 fee Binance + 5 slippage). Rebalancement mensuel inter-stratégies facture en sus.
  • Periode commune limitee par les données Binance sur QC (debut ~2020-08) : l’analyse couvre ~2020-11 -> 2025-12 (~62 mois), incluant le bear 2022 et la reprise 2023-2025. Le COVID crash 2020 n’est PAS couvert pour le blend complet (caveat majeur, cf Phase 2 backtest 2018+ pour les equities).
  • Metriques calculees sur returns mensuels (rebalancement mensuel) : le MaxDD mensuel sous-estime le MaxDD intraday/journalier.
  • HAR-RV-J vol-target plafonne a 1.0 (spot Binance, pas de levier).
from AlgorithmImports import *
import numpy as np
import pandas as pd
from itertools import combinations

qb = QuantBook()

EQUITY_TICKERS = ['SPY', 'IEF', 'GLD', 'XLP', 'XLK', 'XLF', 'XLE', 'XLV', 'XLY', 'XLI', 'XLB', 'XLU']
SECTOR_TICKERS = ['XLK', 'XLF', 'XLE', 'XLV', 'XLY', 'XLI', 'XLB', 'XLU', 'XLP']
CRYPTO_TICKERS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'ADAUSDT', 'LTCUSDT', 'XRPUSDT']

equity_symbols = {t: qb.add_equity(t, Resolution.DAILY).symbol for t in EQUITY_TICKERS}
crypto_symbols = {t: qb.add_crypto(t, Resolution.DAILY, Market.Binance).symbol for t in CRYPTO_TICKERS}
print(f'Equity assets loaded: {len(equity_symbols)} | Crypto assets loaded: {len(crypto_symbols)}')
Equity assets loaded: 12 | Crypto assets loaded: 6
# Historical data: 2018+ for equities (signal warmup), Binance data starts ~2020-08 on QC
start, end = datetime(2018, 1, 1), datetime(2025, 12, 31)

def closes(symbols, tickers):
    hist = qb.history(list(symbols.values()), start, end, Resolution.DAILY)
    close = hist['close'].unstack(level=0)
    close.columns = [str(c).split(' ')[0].upper() for c in close.columns]
    # Convert to native pandas DataFrame to avoid QC PandasMapper __getitem__ issues
    close = pd.DataFrame(close.values, index=close.index, columns=list(close.columns))
    return close[[t for t in tickers if t in close.columns]]

eq_close = closes(equity_symbols, EQUITY_TICKERS)
cr_close = closes(crypto_symbols, CRYPTO_TICKERS)
eq_rets = eq_close.pct_change()
cr_rets = cr_close.pct_change()

print(f'Equity: {eq_close.shape[0]} bars, {eq_close.shape[1]} assets, '
      f'{eq_close.index[0].date()} -> {eq_close.index[-1].date()}')
print(f'Crypto: {cr_close.shape[0]} bars, {cr_close.shape[1]} assets, '
      f'{cr_close.index[0].date()} -> {cr_close.index[-1].date()}')
Equity: 2010 bars, 10 assets, 2018-01-02 -> 2025-12-30
Crypto: 2801 bars, 1 assets, 2018-05-02 -> 2025-12-31

Sleeve IBKR — 5 proxies de stratégies equities

Proxy Règle de signal Stratégie source
TrendWeather SPY > SMA200 et vol63 < 1.5 x mediane252 -> 70% SPY + 30% GLD ; sinon 60% IEF + 40% GLD Framework_Composite_TrendWeather
EMATrend EMA50 > EMA200 sur SPY -> 100% SPY ; sinon 100% IEF Framework_Composite_EMATrend
SectorMomentum top-3 ETFs sectoriels par momentum 126j (si > 0) au debut de mois, equiponderes ; sinon IEF SectorMomentum
AllWeather statique 30% SPY / 40% IEF / 15% GLD / 15% XLP AllWeather v5
EMA-Cross-Alpha EMA20 > EMA50 sur SPY -> 100% SPY ; sinon cash EMA-Cross-Alpha
EQ_COST = 0.0010   # 10 bps per notional traded (5 bps IBKR fee + 5 bps slippage)
CR_COST = 0.0015   # 15 bps per notional traded (10 bps Binance fee + 5 bps slippage)

def strategy_returns(weights, asset_rets, cost_rate):
    """Net daily returns. Weights decided at t-1, applied to return t (no lookahead).
    Cost = traded notional x cost_rate, traded = sum |w_t - w_{t-1}|."""
    w = weights.reindex(asset_rets.index).fillna(0.0)
    gross = (w.shift(1) * asset_rets).sum(axis=1)
    traded = (w - w.shift(1)).abs().sum(axis=1)
    return gross - traded * cost_rate, traded

def zero_weights(close):
    return pd.DataFrame(0.0, index=close.index, columns=close.columns)

spy = eq_close['SPY']
spy_ret = spy.pct_change()

# 1. TrendWeather proxy: regime filter (trend + vol), defensive IEF/GLD when risk-off
sma200 = spy.rolling(200).mean()
vol63 = spy_ret.rolling(63).std() * np.sqrt(252)
vol_med = vol63.rolling(252).median()
risk_on = ((spy > sma200) & (vol63 < vol_med * 1.5)).astype(float)
w_tw = zero_weights(eq_close)
w_tw['SPY'] = 0.70 * risk_on
w_tw['GLD'] = 0.30 * risk_on + 0.40 * (1 - risk_on)
w_tw['IEF'] = 0.60 * (1 - risk_on)

# 2. EMATrend proxy: EMA50/EMA200 cross, bonds when out
ema50, ema200 = spy.ewm(span=50).mean(), spy.ewm(span=200).mean()
up_lt = (ema50 > ema200).astype(float)
w_et = zero_weights(eq_close)
w_et['SPY'] = up_lt
w_et['IEF'] = 1.0 - up_lt

# 3. SectorMomentum proxy: monthly top-3 sectors by 126d momentum (positive only), else IEF
# Filter SECTOR_TICKERS to only those present in eq_close (QC data availability)
available_sectors = [t for t in SECTOR_TICKERS if t in eq_close.columns]
print(f'Sector tickers available: {available_sectors} ({len(available_sectors)}/{len(SECTOR_TICKERS)})')
mom126 = eq_close[available_sectors].pct_change(126)
month_id = pd.Series(eq_close.index.to_period('M'), index=eq_close.index)
rebal_dates = eq_close.index[(month_id != month_id.shift(1)).values]
w_sm = pd.DataFrame(0.0, index=eq_close.index, columns=eq_close.columns)
for d in rebal_dates:
    row = mom126.loc[d].dropna()
    if len(row) > 0:
        top = row.nlargest(min(3, len(row)))
        top = top[top > 0]
    else:
        top = pd.Series(dtype=float)
    if len(top) > 0:
        w_sm.loc[d, list(top.index)] = 1.0 / len(top)
    else:
        w_sm.loc[d, 'IEF'] = 1.0
w_sm = w_sm.ffill().fillna(0.0)

# 4. AllWeather v5 proxy: static defensive mix (only use available assets)
w_aw = zero_weights(eq_close)
w_aw['SPY'], w_aw['IEF'], w_aw['GLD'] = 0.30, 0.40, 0.15
if 'XLP' in eq_close.columns:
    w_aw['XLP'] = 0.15
else:
    w_aw['IEF'] += 0.15  # redistribute to bonds if XLP unavailable

# 5. EMA-Cross-Alpha proxy: faster EMA20/EMA50 cross, cash when out
ema20 = spy.ewm(span=20).mean()
ema50b = spy.ewm(span=50).mean()
w_ea = zero_weights(eq_close)
w_ea['SPY'] = (ema20 > ema50b).astype(float)

print('Equity sleeve weight frames built: TrendWeather, EMATrend, SectorMomentum, AllWeather, EMA-Cross-Alpha')
print(f'Rebalance dates (SectorMomentum): {len(rebal_dates)} months')
Sector tickers available: ['XLK', 'XLF', 'XLE', 'XLV', 'XLY', 'XLI', 'XLU', 'XLP'] (8/9)
Equity sleeve weight frames built: TrendWeather, EMATrend, SectorMomentum, AllWeather, EMA-Cross-Alpha
Rebalance dates (SectorMomentum): 96 months

Sleeve Binance — 3 proxies de stratégies crypto

Proxy Règle de signal Stratégie source
EMA-Cross-Crypto SMA20 > SMA50 sur BTC -> 100% BTC ; sinon cash EMA-Cross-Crypto
Crypto-MultiCanal panier equipondere des 6 cryptos Crypto-MultiCanal
HAR-RV-VolTarget poids BTC = clip(15% / vol realisee 22j, 0, 1) — spot, pas de levier HAR-RV-J vol-target BTC (M12)

Mêmes règles que research.ipynb (#1179), restreintes aux 3 stratégies de la composition cible.

btc = cr_close['BTCUSDT']
available_crypto = list(cr_close.columns)
print(f'Crypto assets with data: {available_crypto} ({len(available_crypto)}/{len(CRYPTO_TICKERS)})')

# 6. EMA-Cross-Crypto: BTC SMA20/50 crossover
w_ec = zero_weights(cr_close)
w_ec['BTCUSDT'] = (btc.rolling(20).mean() > btc.rolling(50).mean()).astype(float)

# 7. Crypto-MultiCanal: equal-weight basket of all available cryptos
w_mc = pd.DataFrame(1.0 / len(available_crypto), index=cr_close.index, columns=cr_close.columns)

# 8. HAR-RV vol-target on BTC: scale exposure to 15% target vol, capped at 1.0 (spot)
rv22 = btc.pct_change().rolling(22).std() * np.sqrt(252)
w_hv = zero_weights(cr_close)
w_hv['BTCUSDT'] = (0.15 / rv22).clip(0.0, 1.0)

# --- Compute net daily returns for all 8 strategies ---
IBKR_STRATS = ['TrendWeather', 'EMATrend', 'SectorMomentum', 'AllWeather', 'EMA-Cross-Alpha']
CRYPTO_STRATS = ['EMA-Cross-Crypto', 'Crypto-MultiCanal', 'HAR-RV-VolTarget']

frames = {
    'TrendWeather':      (w_tw, eq_rets, EQ_COST),
    'EMATrend':          (w_et, eq_rets, EQ_COST),
    'SectorMomentum':    (w_sm, eq_rets, EQ_COST),
    'AllWeather':        (w_aw, eq_rets, EQ_COST),
    'EMA-Cross-Alpha':   (w_ea, eq_rets, EQ_COST),
    'EMA-Cross-Crypto':  (w_ec, cr_rets, CR_COST),
    'Crypto-MultiCanal': (w_mc, cr_rets, CR_COST),
    'HAR-RV-VolTarget':  (w_hv, cr_rets, CR_COST),
}

# Analysis window: first full month after crypto data start + 75d signal warmup
raw_start = cr_rets.index.min() + pd.Timedelta(days=75)
analysis_start = (raw_start.to_period('M') + 1).to_timestamp()
print(f'Analysis start (post-warmup, first full month): {analysis_start.date()}')

net_daily, daily_turnover = {}, {}
for name, (w, r, c) in frames.items():
    net, traded = strategy_returns(w, r, c)
    net_daily[name] = net.loc[net.index >= analysis_start]
    daily_turnover[name] = traded.loc[traded.index >= analysis_start]

print(f'{"Strategy":>20} {"NetSharpe(d)":>13} {"AnnTurnover":>12}')
for name in IBKR_STRATS + CRYPTO_STRATS:
    r = net_daily[name].dropna()
    s = r.mean() / r.std() * np.sqrt(252) if r.std() > 0 else 0.0
    to = daily_turnover[name].mean() * 252
    print(f'{name:>20} {s:>13.3f} {to:>11.1f}x')
Crypto assets with data: ['BTCUSDT'] (1/6)
Analysis start (post-warmup, first full month): 2018-08-01
            Strategy  NetSharpe(d)  AnnTurnover
        TrendWeather         1.212         1.1x
            EMATrend         0.099         1.1x
      SectorMomentum        -0.466        23.8x
          AllWeather         0.903         0.0x
     EMA-Cross-Alpha         0.557         1.1x
    EMA-Cross-Crypto         0.778         5.5x
   Crypto-MultiCanal         0.689         0.0x
    HAR-RV-VolTarget         0.655         4.0x

Matrice de correlation des returns mensuels (8 x 8)

Cible #1027 : correlation moyenne inter-stratégies < 0.3 sur le portefeuille complet. Aggregation mensuelle par PeriodIndex (agnostique aux conventions de timestamps equity NY vs crypto UTC).

def to_monthly(daily):
    g = daily.dropna().groupby(daily.dropna().index.to_period('M')).apply(lambda x: (1.0 + x).prod() - 1.0)
    return g

mdf = pd.DataFrame({name: to_monthly(r) for name, r in net_daily.items()})
mdf = mdf[IBKR_STRATS + CRYPTO_STRATS].dropna()
print(f'Monthly observations: {len(mdf)} months ({mdf.index[0]} -> {mdf.index[-1]})')
print()

corr = mdf.corr()
print('Monthly returns correlation matrix (net of costs):')
print(corr.round(3).to_string())

def avg_upper(c):
    vals = [c.iloc[i, j] for i, j in combinations(range(len(c)), 2)]
    return float(np.mean(vals))

rho_all = avg_upper(corr)
rho_ibkr = avg_upper(corr.loc[IBKR_STRATS, IBKR_STRATS])
rho_crypto = avg_upper(corr.loc[CRYPTO_STRATS, CRYPTO_STRATS])
rho_cross = float(corr.loc[IBKR_STRATS, CRYPTO_STRATS].values.mean())

print()
print(f'Average pairwise correlation (all 8):        {rho_all:.3f}  (target < 0.3)')
print(f'  intra-IBKR sleeve (5 strats):              {rho_ibkr:.3f}')
print(f'  intra-Binance sleeve (3 strats):           {rho_crypto:.3f}')
print(f'  cross-sleeve (IBKR x Binance):             {rho_cross:.3f}')
Monthly observations: 89 months (2018-08 -> 2025-12)

Monthly returns correlation matrix (net of costs):
                   TrendWeather  EMATrend  SectorMomentum  AllWeather  EMA-Cross-Alpha  EMA-Cross-Crypto  Crypto-MultiCanal  HAR-RV-VolTarget
TrendWeather              1.000     0.504          -0.029       0.739            0.581             0.179              0.208             0.174
EMATrend                  0.504     1.000           0.241       0.630            0.709             0.166              0.261             0.250
SectorMomentum           -0.029     0.241           1.000       0.129            0.104             0.144              0.158             0.173
AllWeather                0.739     0.630           0.129       1.000            0.566             0.166              0.282             0.233
EMA-Cross-Alpha           0.581     0.709           0.104       0.566            1.000             0.151              0.149             0.144
EMA-Cross-Crypto          0.179     0.166           0.144       0.166            0.151             1.000              0.789             0.707
Crypto-MultiCanal         0.208     0.261           0.158       0.282            0.149             0.789              1.000             0.922
HAR-RV-VolTarget          0.174     0.250           0.173       0.233            0.144             0.707              0.922             1.000

Average pairwise correlation (all 8):        0.337  (target < 0.3)
  intra-IBKR sleeve (5 strats):              0.417
  intra-Binance sleeve (3 strats):           0.806
  cross-sleeve (IBKR x Binance):             0.189

Portefeuille blend 50/50 — metriques nettes

Blend mensuel avec rebalancement aux poids cibles chaque fin de mois. Le cout de rebalancement inter-stratégies est estime par la derive des poids (sum |w_cible - w_derive| x 12.5 bps moyens) en sus des couts intra-stratégie déjà deduits.

PORTFOLIO_WEIGHTS = {
    'TrendWeather': 0.50 * 0.30, 'EMATrend': 0.50 * 0.25, 'SectorMomentum': 0.50 * 0.20,
    'AllWeather': 0.50 * 0.15, 'EMA-Cross-Alpha': 0.50 * 0.10,
    'EMA-Cross-Crypto': 0.50 * 0.50, 'Crypto-MultiCanal': 0.50 * 0.30, 'HAR-RV-VolTarget': 0.50 * 0.20,
}
wv = pd.Series(PORTFOLIO_WEIGHTS)[mdf.columns]
assert abs(wv.sum() - 1.0) < 1e-9

blend_gross = (mdf * wv).sum(axis=1)

# Monthly cross-strategy rebalancing cost (back to target weights)
AVG_COST = 0.00125  # blended 10/15 bps
rebal_costs = []
for t in mdf.index:
    r = mdf.loc[t]
    drift = wv * (1.0 + r) / (1.0 + float((wv * r).sum()))
    rebal_costs.append(float((wv - drift).abs().sum()) * AVG_COST)
blend_net = blend_gross - pd.Series(rebal_costs, index=mdf.index)

def stats_m(m, label):
    m = m.dropna()
    sharpe = m.mean() / m.std() * np.sqrt(12) if m.std() > 0 else 0.0
    cagr = (1.0 + m).prod() ** (12.0 / len(m)) - 1.0
    vol = m.std() * np.sqrt(12)
    cum = (1.0 + m).cumprod()
    mdd = float((cum / cum.cummax() - 1.0).min())
    calmar = cagr / abs(mdd) if mdd < 0 else float('nan')
    return {'Strategy': label, 'Sharpe': sharpe, 'CAGR': cagr, 'AnnVol': vol, 'MaxDD': mdd, 'Calmar': calmar}

rows = [stats_m(mdf[name], f'{name} ({PORTFOLIO_WEIGHTS[name]:.1%})') for name in mdf.columns]
rows.append(stats_m(blend_gross, 'BLEND 50/50 (gross of rebal)'))
rows.append(stats_m(blend_net, 'BLEND 50/50 (NET)'))

# Benchmarks: extract via .iloc (bypasses QC PandasMapper) on available columns
eq_cols = list(eq_close.columns)
print(f'Available equity columns: {eq_cols}')

# SPY benchmark (should always be present)
if 'SPY' in eq_cols:
    spy_s = pd.Series(eq_close.iloc[:, eq_cols.index('SPY')].values, index=eq_close.index).pct_change()
    spy_m = to_monthly(spy_s).reindex(mdf.index)
    rows.append(stats_m(spy_m, 'SPY Buy&Hold'))

# 60/40 benchmark (use IEF if available, else SHV or skip)
if 'IEF' in eq_cols and 'SPY' in eq_cols:
    ief_s = pd.Series(eq_close.iloc[:, eq_cols.index('IEF')].values, index=eq_close.index).pct_change()
    ief_m = to_monthly(ief_s).reindex(mdf.index)
    rows.append(stats_m(0.6 * spy_m + 0.4 * ief_m, '60/40 SPY/IEF'))

# BTC benchmark
btc_s = pd.Series(cr_close.iloc[:, 0].values, index=cr_close.index).pct_change()
btc_m = to_monthly(btc_s).reindex(mdf.index)
rows.append(stats_m(btc_m, 'BTC Buy&Hold'))

res = pd.DataFrame(rows).set_index('Strategy')
pd.set_option('display.float_format', lambda x: f'{x:.3f}')
print(f'Period: {mdf.index[0]} -> {mdf.index[-1]} ({len(mdf)} months, net of transaction costs)')
print()
print(res.to_string())
Available equity columns: ['SPY', 'GLD', 'XLP', 'XLK', 'XLF', 'XLE', 'XLV', 'XLY', 'XLI', 'XLU']
Period: 2018-08 -> 2025-12 (89 months, net of transaction costs)

                              Sharpe   CAGR  AnnVol  MaxDD  Calmar
Strategy                                                          
TrendWeather (15.0%)           1.310  0.093   0.070 -0.066   1.419
EMATrend (12.5%)               0.100  0.005   0.111 -0.281   0.017
SectorMomentum (10.0%)        -0.465 -0.019   0.040 -0.141  -0.138
AllWeather (7.5%)              1.074  0.059   0.055 -0.075   0.781
EMA-Cross-Alpha (5.0%)         0.544  0.042   0.083 -0.080   0.528
EMA-Cross-Crypto (25.0%)       0.836  0.375   0.533 -0.513   0.731
Crypto-MultiCanal (15.0%)      0.788  0.379   0.693 -0.734   0.516
HAR-RV-VolTarget (10.0%)       0.688  0.161   0.269 -0.358   0.450
BLEND 50/50 (gross of rebal)   0.902  0.221   0.257 -0.317   0.697
BLEND 50/50 (NET)              0.899  0.220   0.257 -0.318   0.692
SPY Buy&Hold                   0.512  0.055   0.117 -0.194   0.280
BTC Buy&Hold                   0.788  0.379   0.693 -0.734   0.516

Pont vers la Phase 2 — MultiAlphaModel (framework QC)

Le backtest unifie exact (Phase 2, main.py) assemble les 8 sous-stratégies via le framework QC :

Sous-stratégie Alpha model Phase 2 Insight
TrendWeather TrendWeatherAlpha (SMA200 + filtre vol) SPY/IEF/GLD directionnel
EMATrend EmaCrossAlphaModel(50, 200) SPY vs IEF
SectorMomentum SectorMomentumAlpha (top-3 / 126j) ETFs sectoriels
AllWeather ConstantAlpha poids statiques SPY/IEF/GLD/XLP
EMA-Cross-Alpha EmaCrossAlphaModel(20, 50) SPY
EMA-Cross-Crypto EmaCrossAlphaModel(20, 50) BTCUSDT
Crypto-MultiCanal ConstantAlpha equipondere 6 cryptos
HAR-RV-VolTarget VolTargetAlpha (RV 22j -> scaling) BTCUSDT

Assemblage : self.add_alpha(...) x8 + InsightWeightingPortfolioConstructionModel avec les poids cibles ci-dessus, rebalancement mensuel, InteractiveBrokersBrokerageModel (sleeve equity) et fees Binance (sleeve crypto). Les poids et la structure sont déjà refletes dans main.py (squelette).

print('=' * 72)
print('PORTFOLIO HYBRIDE IBKR+BINANCE — PHASE 1 SUMMARY (#1027)')
print('=' * 72)
print(f'Period analyzed : {mdf.index[0]} -> {mdf.index[-1]} ({len(mdf)} months)')
print(f'Sub-strategies  : {len(mdf.columns)} (5 IBKR equities + 3 Binance crypto)')
print()

blend = stats_m(blend_net, 'net')
checks = [
    ('Sharpe net 1.0 - 1.3', f"{blend['Sharpe']:.3f}", 1.0 <= blend['Sharpe'] <= 1.3,
     'above' if blend['Sharpe'] > 1.3 else ('below' if blend['Sharpe'] < 1.0 else 'in range')),
    ('MaxDD ~ -22% (monthly)', f"{blend['MaxDD']:.1%}", blend['MaxDD'] >= -0.25, ''),
    ('rho_avg < 0.3 (8 strats)', f'{rho_all:.3f}', rho_all < 0.3, ''),
]
print(f'{"Target":<28} {"Actual":>10} {"Verdict":>10}')
for label, actual, ok, note in checks:
    verdict = 'PASS' if ok else 'FAIL'
    print(f'{label:<28} {actual:>10} {verdict:>10}  {note}')
print()
print(f'Cross-sleeve correlation (diversification engine): {rho_cross:.3f}')
print(f'Blend net CAGR {blend["CAGR"]:.1%}, AnnVol {blend["AnnVol"]:.1%}, Calmar {blend["Calmar"]:.2f}')
print()
print('Honest caveats:')
print(' - Sub-strategies are research PROXIES of the repo strategies, not the exact QC backtests.')
print(' - Common window starts 2020-11 (Binance data on QC): no COVID-crash coverage for the blend.')
print(' - Monthly MaxDD understates daily/intraday drawdowns.')
print(' - In-sample rule parameters inherited from the catalog: expect 20-30% live discount.')
print()
print('Phase 2 next: unified QC framework backtest (MultiAlphaModel) 2018-2025,')
print('walk-forward annual + allocation sweep 60/40-40/60, per pr-review-discipline section C.')
========================================================================
PORTFOLIO HYBRIDE IBKR+BINANCE — PHASE 1 SUMMARY (#1027)
========================================================================
Period analyzed : 2018-08 -> 2025-12 (89 months)
Sub-strategies  : 8 (5 IBKR equities + 3 Binance crypto)

Target                           Actual    Verdict
Sharpe net 1.0 - 1.3              0.899       FAIL  below
MaxDD ~ -22% (monthly)           -31.8%       FAIL  
rho_avg < 0.3 (8 strats)          0.337       FAIL  

Cross-sleeve correlation (diversification engine): 0.189
Blend net CAGR 22.0%, AnnVol 25.7%, Calmar 0.69

Honest caveats:
 - Sub-strategies are research PROXIES of the repo strategies, not the exact QC backtests.
 - Common window starts 2020-11 (Binance data on QC): no COVID-crash coverage for the blend.
 - Monthly MaxDD understates daily/intraday drawdowns.
 - In-sample rule parameters inherited from the catalog: expect 20-30% live discount.

Phase 2 next: unified QC framework backtest (MultiAlphaModel) 2018-2025,
walk-forward annual + allocation sweep 60/40-40/60, per pr-review-discipline section C.
Retour au sommet