#!/usr/bin/env python """ NexQuant Multi-Asset Portfolio Generator — Target: 10%/month. Combines best strategies per asset, optimizes position sizing, adds leverage. """ from __future__ import annotations import json, sys from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk DATA = Path("git_ignore_folder/factor_implementation_source_data/multi_asset_daily.h5") def load_all(): df = pd.read_hdf(DATA, key="data") close_dict = {} for col in df.columns: c = df[col].dropna() if len(c) > 500: close_dict[col] = c return close_dict def rsi_signal(c, period, lo, hi): d = c.diff(); g = d.clip(lower=0); l = -d.clip(upper=0) rsi = 100 - (100 / (1 + g.rolling(period).mean() / (l.rolling(period).mean() + 1e-8))) sig = pd.Series(0.0, index=c.index) sig[rsi < lo] = 1; sig[rsi > hi] = -1 return sig def sma_signal(c, fast, slow): f = c.rolling(fast).mean(); s = c.rolling(slow).mean() sig = pd.Series(0.0, index=c.index) sig[f > s] = 1; sig[f < s] = -1 return sig def mr_signal(c, n): ret = c.pct_change(n) return pd.Series(-np.sign(ret).fillna(0), index=c.index) def mom_signal(c, n): mom = c.pct_change(n) return pd.Series(np.sign(mom).fillna(0), index=c.index) # Best strategy per asset (from our grid search) STRATEGIES = { "OIL": lambda c: mr_signal(c, 50), "DXY": lambda c: sma_signal(c, 5, 25), "SPX": lambda c: mom_signal(c, 100), "EURUSD": lambda c: rsi_signal(c, 21, 25, 75), "USDJPY": lambda c: sma_signal(c, 50, 200), "GOLD": lambda c: rsi_signal(c, 21, 25, 75), "GBPUSD": lambda c: rsi_signal(c, 21, 25, 75), } def main(): print(f"\n{'='*65}") print(" NexQuant Multi-Asset Portfolio — 10%/month Target") print(f"{'='*65}") closes = load_all() assets = sorted(closes.keys()) print(f"Assets: {len(assets)} | Total bars: {max(len(c) for c in closes.values()):,}\n") aligned_signals = {} all_returns = [] # Step 1: Generate signals per asset print("=== Individual Asset Performance ===") for name in assets: c = closes[name] sig_func = STRATEGIES.get(name, lambda c: rsi_signal(c, 21, 25, 75)) sig = sig_func(c).fillna(0) r = backtest_signal_risk(c, sig, txn_cost_bps=2.14, wf_rolling=True) oos = r.get("wf_oos_sharpe_mean") or r.get("oos_sharpe", -999) oos_m = r.get("oos_monthly_return_pct", 0) or 0 status = "✅" if oos > 0 else " " print(f" {name:<10} OOS={oos:+8.2f} Mon={oos_m:+7.3f}% {status}") aligned_signals[name] = sig # Monthly returns for this asset ret = c.pct_change() * sig.shift(1) ret.name = name all_returns.append(ret) # Step 2: Build equal-weight portfolio returns returns_df = pd.concat(all_returns, axis=1).dropna(how="all") common = returns_df.dropna().index returns_df = returns_df.loc[common].fillna(0) port_ret_equal = returns_df.mean(axis=1) print(f"\n=== Equal-Weight Portfolio ({len(returns_df.columns)} assets) ===") # Monthly returns monthly_eq = port_ret_equal.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100 months = len(monthly_eq.dropna()) print(f" Mean monthly: {monthly_eq.mean():+.3f}%") print(f" Median monthly: {monthly_eq.median():+.3f}%") print(f" Positive months: {(monthly_eq > 0).mean()*100:.1f}%") print(f" Months: {months}") # Annualized ann_ret = (1 + port_ret_equal).prod() ** (252 / len(port_ret_equal)) - 1 ann_vol = port_ret_equal.std() * np.sqrt(252) ann_sharpe = ann_ret / ann_vol if ann_vol > 0 else 0 print(f" Annual return: {ann_ret*100:.1f}%") print(f" Annual vol: {ann_vol*100:.1f}%") print(f" Annual Sharpe: {ann_sharpe:.3f}") # Step 3: Risk-parity weighting vols = returns_df.std() inv_vols = 1.0 / (vols + 1e-8) rp_weights = inv_vols / inv_vols.sum() port_ret_rp = (returns_df * rp_weights).sum(axis=1) monthly_rp = port_ret_rp.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100 print(f"\n=== Risk-Parity Portfolio ===") print(f" Weights: {dict(zip(returns_df.columns, rp_weights.round(3)))}") print(f" Mean monthly: {monthly_rp.mean():+.3f}%") print(f" Positive months: {(monthly_rp > 0).mean()*100:.1f}%") ann_rp = (1 + port_ret_rp).prod() ** (252 / len(port_ret_rp)) - 1 print(f" Annual return: {ann_rp*100:.1f}%") # Step 4: With leverage print(f"\n=== With Leverage (2x, 3x, 5x) ===") for lev in [2, 3, 5]: port_lev = port_ret_rp * lev monthly_lev = port_lev.resample("M").apply(lambda x: (1 + x).prod() - 1) * 100 ann_lev = (1 + port_lev).prod() ** (252 / len(port_lev)) - 1 max_dd = (port_lev.cumsum().cummax() - port_lev.cumsum()).max() print(f" {lev}x: Ann={ann_lev*100:+.1f}% Mon={monthly_lev.mean():+.2f}% MaxDD={max_dd*100:.1f}%") # Step 5: Check if 10% is reachable target_monthly = 10.0 needed_lev = target_monthly / monthly_rp.mean() if monthly_rp.mean() > 0 else float("inf") print(f"\n=== Target: {target_monthly}%/month ===") print(f" Current (risk-parity): {monthly_rp.mean():+.2f}%/month") print(f" Leverage needed: {needed_lev:.1f}x") if needed_lev < 10: print(f" ✅ Achievable with {needed_lev:.1f}x leverage") else: print(f" ❌ Not achievable — need {needed_lev:.1f}x leverage") if __name__ == "__main__": main()