Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
48 KiB
JSON
1 line
48 KiB
JSON
{"repo": "alethevia/alpha-research", "files": {"scripts/run_llm_overlay_v8.py": {"a95faee9399bc5da6e1fac88c1958aac9968b632": {"t": 1786068453.3845634, "n": 46884, "h": "fbb9a3245c2d8c02", "c": "#!/usr/bin/env python3\n\"\"\"\n=============================================================================\nV8 Multi-Asset Momentum + LLM Causal Reasoning Overlay\n=============================================================================\n\nBASE: V7 best strategies (RP+IEF+BondMom, MaxSafe)\nOVERLAY: DeepSeek R1 monthly reasoning on macro/fundamentals/risk\n\nDESIGN PRINCIPLE — NO LOOKAHEAD BIAS:\n- R1 receives ONLY: current prices, returns, vol, sector data\n- R1 does NOT receive: future prices, event names, dates that reveal future\n- R1 outputs: risk_level (0-100), allocation adjustments, reasoning\n- Backtest uses R1 ONLY for recent 12 months (honest test period)\n- Longer backtests use pure quant signals (no LLM, as LLM backtest = cheating)\n\nLLM REASONING TASKS:\n1. MACRO REGIME: Is current environment risk-on or risk-off?\n - Based on: yield curve shape, vol levels, cross-asset momentum\n2. SECTOR ROTATION: Which sectors are fundamentally strong/weak?\n - Based on: relative momentum, vol, recent earnings trends\n3. RISK ASSESSMENT: Should we reduce exposure?\n - Based on: correlation regime, vol clustering, momentum reversals\n4. ALLOCATION ADJUSTMENT: How to tilt stock/bond/gold/cash?\n\nOUTPUT: Comparison of quant-only vs quant+LLM for recent period\n\nAuthor: Alpha Research Team\nDate: 2026-01-31\n=============================================================================\n\"\"\"\n\nimport hashlib\nimport os\nimport json\nimport logging\nimport sys\nimport time\nimport warnings\nfrom datetime import date, timedelta\nfrom pathlib import Path\n\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\n\ntry:\n import requests\n HAS_REQUESTS = True\nexcept ImportError:\n HAS_REQUESTS = False\n\nlogging.basicConfig(\n level=logging.INFO,\n format='%(asctime)s - %(levelname)s - %(message)s',\n datefmt='%Y-%m-%d %H:%M:%S'\n)\nlogger = logging.getLogger(__name__)\n\n# =============================================================================\n# Parameters\n# =============================================================================\n\nDEFAULT_CAPITAL = 100_000\nRISK_FREE_RATE = 0.03\nCOMMISSION_PER_SHARE = 0.005\nSLIPPAGE_BPS = 5.0\n\nN_HOLDINGS = 10\nMOM_LOOKBACK = 252\nMOM_SKIP = 22\nVOL_TARGET = 0.15\nVOL_TARGET_TIGHT = 0.10\nFAST_VOL_LOOKBACK = 10\nMAX_SECTOR_PCT = 0.40\nMAX_POSITION_WEIGHT = 0.15\nSMA_WINDOW = 200\n\n# LLM Config\nDEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"\")\nDEEPSEEK_R1_URL = \"https://api.deepseek.com/chat/completions\"\nDEEPSEEK_R1_MODEL = \"deepseek-reasoner\"\n\n# Test periods\n# Full quant backtest: 3/5/10/15/20 years\n# LLM overlay: only recent 1 year (honest, no lookahead)\nTIMEFRAMES_QUANT = [3, 5, 10, 15, 20]\nLLM_TEST_MONTHS = 12 # Only test LLM on most recent 12 months\nEND_DATE = date(2025, 12, 31)\n\n# Variants\nVARIANTS = [\n # Pure quant baselines (from V7)\n 'rp_ief_bmom', # V7 best DD control: RP+IEF+BondMom\n 'max_safe', # V7 best overall: MaxSafe\n # LLM overlay variants\n 'llm_conservative', # LLM as risk overlay: can only REDUCE exposure\n 'llm_tactical', # LLM adjusts allocation weights ±20%\n 'llm_full', # LLM controls risk level + sector + allocation\n]\n\nVARIANT_NAMES = {\n 'rp_ief_bmom': 'RP+IEF+BondMom (quant)',\n 'max_safe': 'MaxSafe (quant)',\n 'llm_conservative': 'LLM Conservative',\n 'llm_tactical': 'LLM Tactical',\n 'llm_full': 'LLM Full Control',\n}\n\n# =============================================================================\n# S&P 500 Universe\n# =============================================================================\n\nFALLBACK_SP500 = \"\"\"\nAAPL MSFT AMZN NVDA GOOGL META TSLA AVGO ADBE CRM CSCO ORCL ACN AMD INTC\nIBM TXN QCOM AMAT LRCX MU NOW INTU SNPS CDNS KLAC ADI MCHP FTNT HPQ DELL\nNXPI MRVL ON CTSH AKAM FFIV JNPR NTAP WDC STX KEYS ANSS PTC FICO VRSN\nJPM BAC WFC GS MS AXP C USB BK PNC SCHW BLK MET PRU TRV ALL AFL AIG COF\nDFS TROW SPGI MCO ICE CME MMC AON AJG CINF HIG FITB HBAN KEY CFG RF MTB\nJNJ UNH PFE MRK ABBV LLY TMO DHR ABT BMY AMGN GILD MDT SYK BSX BDX ISRG\nIDXX EW ZBH BAX DXCM ALGN HOLX WAT A IQV CI HUM CVS MCK CAH CNC MOH HCA\nPG KO PEP WMT COST PM MO MDLZ CL KMB GIS K CPB HSY MKC CHD CAG SYY KR\nEL CLX STZ ADM TSN\nHD LOW TGT MCD SBUX NKE TJX ROST DG DLTR BBY YUM DRI CMG GPC GM F BKNG\nMAR HLT DHI LEN PHM NVR POOL TSCO\nCAT DE HON MMM GE BA LMT RTX NOC GD UNP CSX NSC UPS FDX EMR ROK ITW PCAR\nCTAS FAST PH ETN AME XYL IR DOV TT CARR OTIS JCI GWW ROP VRSK PAYX\nXOM CVX COP EOG SLB MPC VLO PSX OXY HES DVN HAL BKR WMB KMI OKE\nNEE DUK SO D AEP EXC SRE XEL WEC ED ES DTE CMS ATO AES PEG EIX PPL FE CEG AWK\nLIN APD ECL SHW PPG NEM FCX NUE CF ALB DD MLM VMC PKG AVY\nDIS CMCSA T VZ CHTR NFLX TMUS EA TTWO OMC FOX FOXA\nAMT PLD CCI EQIX SPG PSA O DLR WELL AVB EQR VTR ARE ESS MAA IRM SBAC CBRE VICI\nSPY TLT IEF GLD\n\"\"\".split()\n\n\ndef get_sp500_tickers():\n try:\n tables = pd.read_html(\n 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'\n )\n tickers = tables[0]['Symbol'].str.replace('.', '-', regex=False).tolist()\n logger.info(f\"Fetched {len(tickers)} S&P 500 tickers\")\n return tickers\n except Exception as e:\n logger.warning(f\"Wikipedia fetch failed: {e}, using fallback\")\n return FALLBACK_SP500\n\n\n# =============================================================================\n# Trading Calendar\n# =============================================================================\n\ndef _market_holidays(year):\n holidays = set()\n ny = date(year, 1, 1)\n if ny.weekday() == 5: holidays.add(date(year-1, 12, 31))\n elif ny.weekday() == 6: holidays.add(date(year, 1, 2))\n else: holidays.add(ny)\n d = date(year, 1, 1)\n while d.weekday() != 0: d += timedelta(1)\n holidays.add(d + timedelta(weeks=2))\n d = date(year, 2, 1)\n while d.weekday() != 0: d += timedelta(1)\n holidays.add(d + timedelta(weeks=2))\n d = date(year, 5, 31)\n while d.weekday() != 0: d -= timedelta(1)\n holidays.add(d)\n if year >= 2021:\n j = date(year, 6, 19)\n if j.weekday() == 5: holidays.add(date(year, 6, 18))\n elif j.weekday() == 6: holidays.add(date(year, 6, 20))\n else: holidays.add(j)\n j4 = date(year, 7, 4)\n if j4.weekday() == 5: holidays.add(date(year, 7, 3))\n elif j4.weekday() == 6: holidays.add(date(year, 7, 5))\n else: holidays.add(j4)\n d = date(year, 9, 1)\n while d.weekday() != 0: d += timedelta(1)\n holidays.add(d)\n d = date(year, 11, 1)\n while d.weekday() != 3: d += timedelta(1)\n holidays.add(d + timedelta(weeks=3))\n xmas = date(year, 12, 25)\n if xmas.weekday() == 5: holidays.add(date(year, 12, 24))\n elif xmas.weekday() == 6: holidays.add(date(year, 12, 26))\n else: holidays.add(xmas)\n return holidays\n\n\ndef trading_calendar(start, end):\n days, d = [], start\n while d <= end:\n if d.weekday() < 5 and d not in _market_holidays(d.year):\n days.append(d)\n d += timedelta(1)\n return days\n\n\ndef monthly_rebalance_dates(start, end):\n cal = trading_calendar(start, end)\n dates, last_month = [], None\n for d in cal:\n if (d.year, d.month) != last_month:\n dates.append(d)\n last_month = (d.year, d.month)\n return dates\n\n\n# =============================================================================\n# Data Fetcher\n# =============================================================================\n\nclass DataFetcher:\n def __init__(self):\n self.cache_dir = Path.home() / \".alpha_research\" / \"cache_sp500\"\n self.cache_dir.mkdir(parents=True, exist_ok=True)\n\n def fetch(self, symbols, start, end):\n cache_key = hashlib.md5(\n f\"sp500v8_{len(symbols)}_{start}_{end}\".encode()\n ).hexdigest()[:12]\n cache_file = self.cache_dir / f\"sp500v8_{cache_key}.parquet\"\n\n if cache_file.exists():\n try:\n df = pd.read_parquet(cache_file)\n logger.info(f\"Loaded cached data: {len(df):,} rows, \"\n f\"{df['symbol'].nunique()} symbols\")\n return df\n except Exception:\n pass\n\n import yfinance as yf\n fetch_start = start - timedelta(days=400)\n logger.info(f\"Batch downloading {len(symbols)} symbols...\")\n\n all_records = []\n batch_size = 50\n failed = []\n\n for i in range(0, len(symbols), batch_size):\n batch = symbols[i:i+batch_size]\n logger.info(f\" Batch {i//batch_size + 1}/\"\n f\"{(len(symbols)-1)//batch_size + 1}\")\n try:\n data = yf.download(\n batch, start=fetch_start, end=end,\n auto_adjust=True, threads=True, progress=False\n )\n if data.empty:\n failed.extend(batch)\n continue\n if len(batch) == 1:\n sym = batch[0]\n for idx_dt, row in data.iterrows():\n if pd.notna(row.get('Close')) and pd.notna(row.get('Volume')):\n all_records.append({\n 'symbol': sym, 'trade_date': idx_dt.date(),\n 'close': float(row['Close']),\n 'volume': int(row['Volume']),\n })\n else:\n close = data['Close'] if 'Close' in data.columns.get_level_values(0) else None\n volume = data['Volume'] if 'Volume' in data.columns.get_level_values(0) else None\n if close is None:\n failed.extend(batch)\n continue\n for sym in batch:\n try:\n if sym not in close.columns:\n failed.append(sym)\n continue\n sc = close[sym].dropna()\n sv = volume[sym].dropna() if volume is not None and sym in volume.columns else pd.Series(dtype=float)\n min_days = 126 if sym in ('TLT', 'GLD', 'IEF') else 252\n if len(sc) < min_days:\n failed.append(sym)\n continue\n vol_dict = sv.to_dict() if len(sv) > 0 else {}\n for idx_dt, price in sc.items():\n v = vol_dict.get(idx_dt, 0)\n all_records.append({\n 'symbol': sym, 'trade_date': idx_dt.date(),\n 'close': float(price),\n 'volume': int(v) if pd.notna(v) else 0,\n })\n except Exception:\n failed.append(sym)\n except Exception as e:\n logger.warning(f\" Batch download error: {e}\")\n failed.extend(batch)\n\n if not all_records:\n raise RuntimeError(\"No market data fetched\")\n\n df = pd.DataFrame(all_records)\n valid = df.groupby('symbol').size()\n valid = valid[valid >= 126].index.tolist()\n df = df[df['symbol'].isin(valid)]\n\n try:\n df.to_parquet(cache_file)\n except Exception:\n pass\n\n logger.info(f\"Fetched {len(df):,} rows, {df['symbol'].nunique()} symbols, \"\n f\"{len(failed)} failed\")\n return df\n\n\n# =============================================================================\n# Market Data Index\n# =============================================================================\n\nclass MarketIndex:\n def __init__(self, df):\n self._data = {}\n for sym in df['symbol'].unique():\n sdf = df[df['symbol'] == sym].sort_values('trade_date')\n self._data[sym] = {\n 'dates': sdf['trade_date'].values,\n 'close': sdf['close'].values.astype(np.float64),\n 'volume': sdf['volume'].values.astype(np.float64),\n }\n\n def prices(self, sym, as_of):\n if sym not in self._data: return None\n d = self._data[sym]\n idx = np.searchsorted(d['dates'], np.datetime64(as_of), side='right')\n return d['close'][:idx] if idx > 0 else None\n\n def price_on(self, sym, dt):\n if sym not in self._data: return None\n d = self._data[sym]\n idx = np.searchsorted(d['dates'], np.datetime64(dt), side='right')\n return float(d['close'][idx-1]) if idx > 0 else None\n\n def avg_volume(self, sym, dt, n=20):\n if sym not in self._data: return 1e6\n d = self._data[sym]\n idx = np.searchsorted(d['dates'], np.datetime64(dt), side='right')\n if idx == 0: return 1e6\n vols = d['volume'][max(0, idx-n):idx]\n return float(np.mean(vols)) if len(vols) > 0 else 1e6\n\n def realized_vol(self, sym, dt, lookback=21):\n p = self.prices(sym, dt)\n if p is None or len(p) < lookback + 1:\n return 0.20\n rets = np.diff(p[-lookback-1:]) / p[-lookback-1:-1]\n return float(np.std(rets) * np.sqrt(252))\n\n def momentum(self, sym, dt, days=63):\n p = self.prices(sym, dt)\n if p is None or len(p) < days:\n return 0.0\n return float(p[-1] / p[-days] - 1)\n\n def rolling_correlation(self, sym1, sym2, dt, lookback=63):\n p1 = self.prices(sym1, dt)\n p2 = self.prices(sym2, dt)\n if p1 is None or p2 is None:\n return 0.0\n n = min(len(p1), len(p2), lookback + 1)\n if n < 22:\n return 0.0\n r1 = np.diff(p1[-n:]) / p1[-n:-1]\n r2 = np.diff(p2[-n:]) / p2[-n:-1]\n if len(r1) != len(r2):\n mn = min(len(r1), len(r2))\n r1, r2 = r1[-mn:], r2[-mn:]\n if np.std(r1) < 1e-8 or np.std(r2) < 1e-8:\n return 0.0\n return float(np.corrcoef(r1, r2)[0, 1])\n\n @property\n def symbols(self):\n return list(self._data.keys())\n\n\n# =============================================================================\n# Sector Map\n# =============================================================================\n\nSECTOR_MAP = {}\n\ndef build_sector_map(symbols):\n known = {\n 'Tech': ['AAPL','MSFT','NVDA','AMZN','GOOGL','META','TSLA','AVGO','ADBE',\n 'CRM','CSCO','ORCL','ACN','AMD','INTC','IBM','TXN','QCOM','AMAT',\n 'LRCX','MU','NOW','INTU','SNPS','CDNS','KLAC','ADI','MCHP','FTNT',\n 'HPQ','DELL','NXPI','MRVL','ON','CTSH','KEYS','ANSS','PTC','FICO'],\n 'Fin': ['JPM','BAC','WFC','GS','MS','AXP','C','USB','BK','PNC','SCHW','BLK',\n 'MET','PRU','TRV','ALL','AFL','AIG','COF','DFS','TROW','SPGI','MCO',\n 'ICE','CME','MMC','AON','AJG','CINF','HIG','FITB','HBAN','KEY','CFG',\n 'RF','MTB'],\n 'HC': ['JNJ','UNH','PFE','MRK','ABBV','LLY','TMO','DHR','ABT','BMY','AMGN',\n 'GILD','MDT','SYK','BSX','BDX','ISRG','IDXX','EW','ZBH','BAX','DXCM',\n 'ALGN','HOLX','WAT','A','IQV','CI','HUM','CVS','MCK','CAH','CNC','MOH','HCA'],\n 'Staples': ['PG','KO','PEP','WMT','COST','PM','MO','MDLZ','CL','KMB','GIS',\n 'K','CPB','HSY','MKC','CHD','CAG','SYY','KR','EL','CLX','STZ','ADM','TSN'],\n 'Disc': ['HD','LOW','TGT','MCD','SBUX','NKE','TJX','ROST','DG','DLTR','BBY',\n 'YUM','DRI','CMG','GPC','GM','F','BKNG','MAR','HLT','DHI','LEN','PHM',\n 'NVR','POOL','TSCO'],\n 'Ind': ['CAT','DE','HON','MMM','GE','BA','LMT','RTX','NOC','GD','UNP','CSX',\n 'NSC','UPS','FDX','EMR','ROK','ITW','PCAR','CTAS','FAST','PH','ETN',\n 'AME','XYL','IR','DOV','TT','CARR','OTIS','JCI','GWW','ROP','VRSK','PAYX'],\n 'Energy': ['XOM','CVX','COP','EOG','SLB','MPC','VLO','PSX','OXY','HES','DVN',\n 'HAL','BKR','WMB','KMI','OKE'],\n 'Util': ['NEE','DUK','SO','D','AEP','EXC','SRE','XEL','WEC','ED','ES','DTE',\n 'CMS','ATO','AES','PEG','EIX','PPL','FE','CEG','AWK'],\n 'Mat': ['LIN','APD','ECL','SHW','PPG','NEM','FCX','NUE','CF','ALB','DD','MLM',\n 'VMC','PKG','AVY'],\n 'Comm': ['DIS','CMCSA','T','VZ','CHTR','NFLX','TMUS','EA','TTWO','OMC','FOX','FOXA'],\n 'REIT': ['AMT','PLD','CCI','EQIX','SPG','PSA','O','DLR','WELL','AVB','EQR',\n 'VTR','ARE','ESS','MAA','IRM','SBAC','CBRE','VICI'],\n }\n global SECTOR_MAP\n SECTOR_MAP = {}\n for sector, syms in known.items():\n for s in syms:\n SECTOR_MAP[s] = sector\n return SECTOR_MAP\n\n\n# =============================================================================\n# Scoring\n# =============================================================================\n\ndef score_stock(prices):\n if prices is None or len(prices) < 260:\n return None, None\n p_now = prices[-MOM_SKIP]\n p_12m = prices[-MOM_LOOKBACK]\n if p_12m <= 0:\n return None, None\n mom = p_now / p_12m - 1\n n = min(63, len(prices) - 1)\n rets = np.diff(prices[-n-1:]) / prices[-n-1:-1]\n vol = np.std(rets) * np.sqrt(252) if len(rets) > 0 else 0.3\n return mom, vol\n\n\n# =============================================================================\n# DeepSeek R1 LLM Interface\n# =============================================================================\n\nclass LLMAnalyzer:\n \"\"\"\n Calls DeepSeek R1 for causal reasoning about market conditions.\n Strict no-lookahead: only provides numerical data, no dates/events.\n \"\"\"\n\n def __init__(self):\n self.api_key = DEEPSEEK_API_KEY\n self.cache_dir = Path.home() / \".alpha_research\" / \"llm_cache_v8\"\n self.cache_dir.mkdir(parents=True, exist_ok=True)\n self.call_count = 0\n self.total_tokens = 0\n\n def _cache_key(self, prompt):\n return hashlib.md5(prompt.encode()).hexdigest()[:16]\n\n def _call_r1(self, prompt, max_retries=3):\n \"\"\"Call DeepSeek R1 with retry logic.\"\"\"\n if not HAS_REQUESTS:\n logger.warning(\"requests not installed, returning default\")\n return None\n\n cache_key = self._cache_key(prompt)\n cache_file = self.cache_dir / f\"r1_{cache_key}.json\"\n\n if cache_file.exists():\n try:\n with open(cache_file) as f:\n return json.load(f)\n except Exception:\n pass\n\n headers = {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n }\n payload = {\n \"model\": DEEPSEEK_R1_MODEL,\n \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n \"temperature\": 0.1,\n \"max_tokens\": 2000,\n }\n\n for attempt in range(max_retries):\n try:\n resp = requests.post(\n DEEPSEEK_R1_URL, headers=headers,\n json=payload, timeout=120\n )\n if resp.status_code == 200:\n data = resp.json()\n content = data['choices'][0]['message']['content']\n usage = data.get('usage', {})\n self.call_count += 1\n self.total_tokens += usage.get('total_tokens', 0)\n\n result = {'content': content, 'usage': usage}\n try:\n with open(cache_file, 'w') as f:\n json.dump(result, f)\n except Exception:\n pass\n return result\n else:\n logger.warning(f\"R1 API error {resp.status_code}: {resp.text[:200]}\")\n except Exception as e:\n logger.warning(f\"R1 API call failed (attempt {attempt+1}): {e}\")\n\n if attempt < max_retries - 1:\n time.sleep(2 ** (attempt + 1))\n\n return None\n\n def analyze_market(self, idx, d, top_stocks, sector_data):\n \"\"\"\n Build market snapshot and ask R1 for analysis.\n Returns dict with risk_level, allocation_adj, sector_preference.\n \"\"\"\n # Build data snapshot (NO dates, NO event names — prevent lookahead)\n spy_1m = idx.momentum('SPY', d, 21)\n spy_3m = idx.momentum('SPY', d, 63)\n spy_6m = idx.momentum('SPY', d, 126)\n spy_vol = idx.realized_vol('SPY', d, 21)\n spy_vol_3m = idx.realized_vol('SPY', d, 63)\n\n ief_1m = idx.momentum('IEF', d, 21)\n ief_3m = idx.momentum('IEF', d, 63)\n tlt_1m = idx.momentum('TLT', d, 21)\n tlt_3m = idx.momentum('TLT', d, 63)\n\n gld_1m = idx.momentum('GLD', d, 21)\n gld_3m = idx.momentum('GLD', d, 63)\n\n sb_corr = idx.rolling_correlation('SPY', 'TLT', d, 63)\n sg_corr = idx.rolling_correlation('SPY', 'GLD', d, 63)\n\n # Sector momentum summary\n sector_mom = {}\n for sector, syms in sector_data.items():\n moms = []\n for sym in syms[:5]: # top 5 per sector\n m = idx.momentum(sym, d, 63)\n if m != 0:\n moms.append(m)\n if moms:\n sector_mom[sector] = f\"{np.mean(moms):+.1%}\"\n\n # Top stocks info\n stock_info = []\n for s in top_stocks[:10]:\n stock_info.append(f\"{s['symbol']}({s['sector']}): mom={s['momentum']:+.1%}, vol={s['vol']:.0%}\")\n\n prompt = f\"\"\"You are a quantitative portfolio risk manager. Analyze the following market data and provide investment guidance.\n\nIMPORTANT: You must respond with ONLY a valid JSON object. No explanation before or after.\nDo NOT include any markdown formatting, code blocks, or backticks.\n\nMARKET DATA (all numerical, no dates to prevent bias):\n- Equity index: 1m return {spy_1m:+.1%}, 3m {spy_3m:+.1%}, 6m {spy_6m:+.1%}\n- Equity vol (21d): {spy_vol:.1%}, (63d): {spy_vol_3m:.1%}\n- Medium-term bonds: 1m {ief_1m:+.1%}, 3m {ief_3m:+.1%}\n- Long-term bonds: 1m {tlt_1m:+.1%}, 3m {tlt_3m:+.1%}\n- Gold: 1m {gld_1m:+.1%}, 3m {gld_3m:+.1%}\n- Stock-Bond correlation (63d): {sb_corr:+.2f}\n- Stock-Gold correlation (63d): {sg_corr:+.2f}\n\nSECTOR 3M MOMENTUM:\n{json.dumps(sector_mom, indent=2)}\n\nTOP MOMENTUM STOCKS:\n{chr(10).join(stock_info)}\n\nTASKS:\n1. Assess macro regime (risk-on/neutral/risk-off) based on cross-asset momentum and volatility\n2. Identify if stock-bond correlation is abnormal (positive = diversification breakdown)\n3. Evaluate if current momentum leaders are concentrated in few sectors (risk)\n4. Recommend risk level adjustment\n\nRespond with exactly this JSON structure:\n{{\"risk_level\": <0-100, where 50=neutral, 0=max_risk_off, 100=max_risk_on>,\n\"stock_adj\": <-0.20 to +0.20, adjustment to stock weight>,\n\"bond_adj\": <-0.20 to +0.20, adjustment to bond weight>,\n\"gold_adj\": <-0.20 to +0.20, adjustment to gold weight>,\n\"cash_adj\": <-0.20 to +0.20, adjustment to cash weight>,\n\"preferred_sectors\": [<top 3 sector names>],\n\"avoided_sectors\": [<bottom 2 sector names>],\n\"reasoning\": \"<one sentence summary>\"}}\"\"\"\n\n result = self._call_r1(prompt)\n if result is None:\n return self._default_signal()\n\n try:\n content = result['content'].strip()\n # Try to extract JSON from response\n # Handle potential markdown code blocks\n if '```json' in content:\n content = content.split('```json')[1].split('```')[0].strip()\n elif '```' in content:\n content = content.split('```')[1].split('```')[0].strip()\n\n parsed = json.loads(content)\n # Validate and clamp values\n parsed['risk_level'] = max(0, min(100, int(parsed.get('risk_level', 50))))\n for key in ['stock_adj', 'bond_adj', 'gold_adj', 'cash_adj']:\n parsed[key] = max(-0.20, min(0.20, float(parsed.get(key, 0))))\n return parsed\n except (json.JSONDecodeError, KeyError, ValueError) as e:\n logger.warning(f\"R1 parse error: {e}, content: {content[:200]}\")\n return self._default_signal()\n\n def _default_signal(self):\n return {\n 'risk_level': 50,\n 'stock_adj': 0.0,\n 'bond_adj': 0.0,\n 'gold_adj': 0.0,\n 'cash_adj': 0.0,\n 'preferred_sectors': [],\n 'avoided_sectors': [],\n 'reasoning': 'Default (no LLM signal)',\n }\n\n\n# =============================================================================\n# Multi-Asset Engine\n# =============================================================================\n\nclass MultiAssetEngine:\n def __init__(self, capital=DEFAULT_CAPITAL):\n self.capital = capital\n self.cash = capital\n self.positions = {}\n self.snapshots = []\n self.trades = []\n self.hwm = capital\n self.total_costs = 0\n self.nav_history = []\n\n def nav(self, idx, d):\n v = self.cash\n for sym, shares in self.positions.items():\n p = idx.price_on(sym, d)\n if p:\n v += shares * p\n return v\n\n def portfolio_dd(self, current_nav):\n self.hwm = max(self.hwm, current_nav)\n return (self.hwm - current_nav) / self.hwm if self.hwm > 0 else 0\n\n def _trade_cost(self, shares, price, avg_vol):\n participation = abs(shares) / max(1, avg_vol)\n slip_pct = (SLIPPAGE_BPS / 10000) * np.sqrt(participation * 100)\n slip_pct = min(slip_pct, 0.02)\n slippage = abs(shares) * price * slip_pct\n commission = max(1.0, abs(shares) * COMMISSION_PER_SHARE)\n return slippage + commission\n\n def execute_trade(self, d, sym, target_shares, idx):\n cur = self.positions.get(sym, 0)\n delta = target_shares - cur\n if delta == 0:\n return\n p = idx.price_on(sym, d)\n if not p:\n return\n vol = idx.avg_volume(sym, d)\n cost = self._trade_cost(delta, p, vol)\n self.total_costs += cost\n if delta > 0:\n self.cash -= delta * p + cost\n else:\n self.cash += abs(delta) * p - cost\n new_shares = cur + delta\n if new_shares <= 0:\n self.positions.pop(sym, None)\n else:\n self.positions[sym] = new_shares\n self.trades.append((d, sym, delta, p, cost))\n\n def compute_vol_scale(self, target_vol=VOL_TARGET, lookback=FAST_VOL_LOOKBACK):\n if len(self.nav_history) < lookback + 1:\n return 1.0\n recent = np.array(self.nav_history[-lookback - 1:])\n rets = np.diff(recent) / recent[:-1]\n realized_vol = np.std(rets) * np.sqrt(252)\n if realized_vol < 0.01:\n return 1.5\n scale = target_vol / realized_vol\n return max(0.05, min(1.50, scale))\n\n def record(self, d, idx, prev_nav):\n n = self.nav(idx, d)\n dr = (n - prev_nav) / prev_nav if prev_nav > 0 else 0\n dd = self.portfolio_dd(n)\n self.nav_history.append(n)\n self.snapshots.append({\n 'date': d, 'nav': n, 'daily_return': dr, 'drawdown': dd,\n })\n return n\n\n def results(self, name, start, end):\n if not self.snapshots:\n return None\n rets = pd.Series(\n [s['daily_return'] for s in self.snapshots],\n index=pd.DatetimeIndex([pd.Timestamp(s['date']) for s in self.snapshots])\n )\n final = self.snapshots[-1]['nav']\n total_ret = (final - self.capital) / self.capital\n n_years = (end - start).days / 365.25\n ann_ret = (1 + total_ret) ** (1/n_years) - 1 if n_years > 0 else total_ret\n ann_vol = rets.std() * np.sqrt(252)\n sharpe = (ann_ret - RISK_FREE_RATE) / ann_vol if ann_vol > 0 else 0\n\n down = rets[rets < 0]\n down_vol = down.std() * np.sqrt(252) if len(down) > 0 else ann_vol\n sortino = (ann_ret - RISK_FREE_RATE) / down_vol if down_vol > 0 else 0\n\n max_dd = max(s['drawdown'] for s in self.snapshots)\n calmar = ann_ret / max_dd if max_dd > 0 else 0\n\n return {\n 'strategy': name,\n 'ann_return': ann_ret,\n 'ann_vol': ann_vol,\n 'sharpe': sharpe,\n 'sortino': sortino,\n 'calmar': calmar,\n 'max_dd': max_dd,\n 'total_return': total_ret,\n 'final_nav': final,\n 'trades': len(self.trades),\n 'costs': self.total_costs,\n }\n\n\n# =============================================================================\n# Asset Allocation\n# =============================================================================\n\ndef risk_parity_weights_ief(idx, d):\n spy_vol = max(idx.realized_vol('SPY', d, 63), 0.05)\n ief_vol = max(idx.realized_vol('IEF', d, 63), 0.05)\n gld_vol = max(idx.realized_vol('GLD', d, 63), 0.05)\n inv = np.array([1/spy_vol, 1/ief_vol, 1/gld_vol])\n w = inv / inv.sum()\n return float(w[0]), float(w[1]), float(w[2])\n\n\ndef bond_momentum_ok(idx, d, bond_sym='IEF'):\n return idx.momentum(bond_sym, d, days=63) >= 0\n\n\ndef compute_quant_weights(variant, idx, d):\n \"\"\"\n Returns (stock_w, ief_w, gold_w, cash_w) for quant-only strategies.\n \"\"\"\n sw, bw, gw = risk_parity_weights_ief(idx, d)\n cash_w = 0.0\n\n # Bond momentum filter\n use_bmom = variant in ('rp_ief_bmom', 'max_safe',\n 'llm_conservative', 'llm_tactical', 'llm_full')\n if use_bmom and not bond_momentum_ok(idx, d, 'IEF'):\n cash_w += bw * 0.7\n gw += bw * 0.3\n bw = 0.0\n\n # Correlation regime\n corr = idx.rolling_correlation('SPY', 'TLT', d, 63)\n if corr > 0.15:\n stock_red = sw * 0.30\n bond_red = bw * 0.50\n sw -= stock_red\n bw *= 0.50\n shifted = stock_red + bond_red\n gw += shifted * 0.4\n cash_w += shifted * 0.6\n\n total = sw + bw + gw + cash_w\n if total > 0:\n sw /= total; bw /= total; gw /= total; cash_w /= total\n\n return sw, bw, gw, cash_w\n\n\ndef apply_llm_overlay(variant, quant_weights, llm_signal):\n \"\"\"\n Apply LLM adjustments to quant weights.\n\n Conservative: LLM can only REDUCE risk (lower stock weight)\n Tactical: LLM adjusts weights ±20%\n Full: LLM controls risk level + sector + allocation\n \"\"\"\n sw, bw, gw, cw = quant_weights\n\n if llm_signal is None:\n return sw, bw, gw, cw\n\n risk_level = llm_signal.get('risk_level', 50)\n\n if variant == 'llm_conservative':\n # Can only reduce stock exposure, shift to cash\n if risk_level < 40:\n # Risk-off: reduce stocks by up to 30%\n reduction = sw * (0.30 * (40 - risk_level) / 40)\n sw -= reduction\n cw += reduction\n # If risk_level >= 40, no change (conservative = don't increase)\n\n elif variant == 'llm_tactical':\n # Apply adjustments within ±20%\n sw += llm_signal.get('stock_adj', 0)\n bw += llm_signal.get('bond_adj', 0)\n gw += llm_signal.get('gold_adj', 0)\n cw += llm_signal.get('cash_adj', 0)\n # Clamp to non-negative\n sw = max(0.05, sw)\n bw = max(0.0, bw)\n gw = max(0.05, gw)\n cw = max(0.0, cw)\n\n elif variant == 'llm_full':\n # Risk level controls overall exposure\n # risk_level 0 = 30% invested, 100 = 100% invested\n exposure = 0.30 + 0.70 * (risk_level / 100)\n # Apply adjustments\n sw += llm_signal.get('stock_adj', 0)\n bw += llm_signal.get('bond_adj', 0)\n gw += llm_signal.get('gold_adj', 0)\n sw = max(0.05, sw)\n bw = max(0.0, bw)\n gw = max(0.05, gw)\n # Normalize non-cash, then scale by exposure\n nc = sw + bw + gw\n if nc > 0:\n sw = sw / nc * exposure\n bw = bw / nc * exposure\n gw = gw / nc * exposure\n cw = 1.0 - sw - bw - gw\n\n # Normalize\n total = sw + bw + gw + cw\n if total > 0:\n sw /= total; bw /= total; gw /= total; cw /= total\n\n return sw, bw, gw, cw\n\n\n# =============================================================================\n# Run single backtest\n# =============================================================================\n\ndef run_single(variant, idx, bt_start, bt_end, llm_analyzer=None,\n sector_groups=None, use_llm=False):\n cal = trading_calendar(bt_start, bt_end)\n rebal_dates = set(monthly_rebalance_dates(bt_start, bt_end))\n rebal_list = sorted(rebal_dates)\n\n if len(cal) < 60:\n return None, []\n\n use_fvt = variant in ('max_safe', 'llm_conservative', 'llm_tactical', 'llm_full')\n vol_target = VOL_TARGET_TIGHT if variant == 'max_safe' else VOL_TARGET\n\n engine = MultiAssetEngine()\n prev_nav = DEFAULT_CAPITAL\n last_vol_scale = 1.0\n llm_signals = []\n\n for d in cal:\n # Daily fast vol targeting\n if use_fvt and len(engine.nav_history) > FAST_VOL_LOOKBACK + 1:\n last_vol_scale = engine.compute_vol_scale(\n target_vol=vol_target, lookback=FAST_VOL_LOOKBACK\n )\n\n if d in rebal_dates:\n signal_date = d - timedelta(days=1)\n current_nav = engine.nav(idx, d)\n if current_nav <= 0:\n continue\n\n # Score stocks\n scored = []\n for sym in idx.symbols:\n if sym in ('SPY', 'TLT', 'IEF', 'GLD'):\n continue\n p = idx.prices(sym, signal_date)\n mom, vol = score_stock(p)\n if mom is None or mom <= 0:\n continue\n scored.append({\n 'symbol': sym, 'momentum': mom, 'vol': vol,\n 'sector': SECTOR_MAP.get(sym, 'Other'),\n })\n\n scored.sort(key=lambda x: x['momentum'], reverse=True)\n\n # Quant weights\n quant_w = compute_quant_weights(variant, idx, signal_date)\n\n # LLM overlay\n llm_signal = None\n if use_llm and llm_analyzer:\n llm_signal = llm_analyzer.analyze_market(\n idx, signal_date, scored[:15], sector_groups or {}\n )\n llm_signals.append({\n 'date': str(d),\n 'risk_level': llm_signal.get('risk_level', 50),\n 'reasoning': llm_signal.get('reasoning', ''),\n 'stock_adj': llm_signal.get('stock_adj', 0),\n })\n sw, bw, gw, cw = apply_llm_overlay(variant, quant_w, llm_signal)\n else:\n sw, bw, gw, cw = quant_w\n\n # Vol scale\n scale = last_vol_scale if use_fvt else 1.0\n investable = current_nav * (1.0 - cw) * scale\n\n non_cash = sw + bw + gw\n if non_cash > 0:\n sw_n, bw_n, gw_n = sw/non_cash, bw/non_cash, gw/non_cash\n else:\n sw_n, bw_n, gw_n = 0, 0, 0\n\n # Stock selection with LLM sector preference\n max_per_sector = max(2, int(N_HOLDINGS * MAX_SECTOR_PCT))\n selected_stocks = []\n sector_counts = {}\n\n # If LLM provided sector preferences, boost/penalize\n preferred = set(llm_signal.get('preferred_sectors', [])) if llm_signal else set()\n avoided = set(llm_signal.get('avoided_sectors', [])) if llm_signal else set()\n\n # Sort with sector preference bonus\n if use_llm and (preferred or avoided):\n for s in scored:\n bonus = 0\n if s['sector'] in preferred:\n bonus = 0.05 # 5% momentum bonus\n elif s['sector'] in avoided:\n bonus = -0.10 # 10% penalty\n s['adj_momentum'] = s['momentum'] + bonus\n scored.sort(key=lambda x: x.get('adj_momentum', x['momentum']), reverse=True)\n\n for s in scored:\n sec = s['sector']\n if sector_counts.get(sec, 0) >= max_per_sector:\n continue\n selected_stocks.append(s)\n sector_counts[sec] = sector_counts.get(sec, 0) + 1\n if len(selected_stocks) >= N_HOLDINGS:\n break\n\n # Build target positions\n target_positions = {}\n stock_alloc = investable * sw_n\n if selected_stocks and stock_alloc > 0:\n n = len(selected_stocks)\n weight = min(1.0 / n, MAX_POSITION_WEIGHT)\n for sig in selected_stocks:\n sym = sig['symbol']\n p = idx.price_on(sym, d)\n if p and p > 0:\n alloc = stock_alloc * weight\n shares = int(alloc / p)\n if shares > 0:\n target_positions[sym] = shares\n\n if bw_n > 0:\n alloc = investable * bw_n\n p = idx.price_on('IEF', d)\n if p and p > 0:\n shares = int(alloc / p)\n if shares > 0:\n target_positions['IEF'] = shares\n\n if gw_n > 0:\n alloc = investable * gw_n\n p = idx.price_on('GLD', d)\n if p and p > 0:\n shares = int(alloc / p)\n if shares > 0:\n target_positions['GLD'] = shares\n\n all_syms = set(engine.positions) | set(target_positions)\n for sym in all_syms:\n target = target_positions.get(sym, 0)\n engine.execute_trade(d, sym, target, idx)\n\n prev_nav = engine.record(d, idx, prev_nav)\n\n name = VARIANT_NAMES[variant]\n return engine.results(name, bt_start, bt_end), llm_signals\n\n\n# =============================================================================\n# Main\n# =============================================================================\n\ndef main():\n print(\"=\" * 95)\n print(\"V8 MULTI-ASSET MOMENTUM + LLM CAUSAL REASONING\")\n print(\"TARGET: Sharpe > 1.0 | MaxDD < 15%\")\n print(\"=\" * 95)\n print(\"BASE: V7 best strategies (RP+IEF+BondMom, MaxSafe)\")\n print(\"LLM: DeepSeek R1 monthly macro/sector/risk reasoning\")\n print(\"HONEST: LLM tested only on recent 12 months (no backtest lookahead)\")\n print(f\"Quant backtest: {TIMEFRAMES_QUANT} years | LLM overlay: {LLM_TEST_MONTHS} months\")\n print(\"=\" * 95)\n\n tickers = get_sp500_tickers()\n for t in ['SPY', 'TLT', 'IEF', 'GLD']:\n if t not in tickers:\n tickers.append(t)\n\n longest = max(TIMEFRAMES_QUANT)\n data_start = date(END_DATE.year - longest - 2, 1, 1)\n\n print(f\"\\nFetching data from {data_start} to {END_DATE}...\")\n fetcher = DataFetcher()\n df = fetcher.fetch(tickers, data_start, END_DATE)\n\n idx = MarketIndex(df)\n build_sector_map(idx.symbols)\n actual_end = df['trade_date'].max()\n print(f\"Symbols: {len(idx.symbols)}, Data through: {actual_end}\")\n\n # Build sector groups for LLM\n sector_groups = {}\n for sym, sector in SECTOR_MAP.items():\n if sym in idx.symbols:\n sector_groups.setdefault(sector, []).append(sym)\n\n # =========================================================================\n # PART 1: Quant-only backtest (all timeframes)\n # =========================================================================\n print(f\"\\n{'=' * 95}\")\n print(\"PART 1: QUANT-ONLY BACKTEST (no LLM)\")\n print(f\"{'=' * 95}\")\n\n quant_results = []\n quant_variants = ['rp_ief_bmom', 'max_safe']\n\n for years in TIMEFRAMES_QUANT:\n bt_start_raw = date(END_DATE.year - years, END_DATE.month, 1)\n bt_start = max(bt_start_raw, df['trade_date'].min() + timedelta(days=380))\n\n print(f\"\\n{'─' * 95}\")\n print(f\"{years}-YEAR PERIOD: {bt_start} to {actual_end}\")\n print(f\"{'─' * 95}\")\n\n for variant in quant_variants:\n res, _ = run_single(variant, idx, bt_start, actual_end)\n if res:\n res['years'] = years\n res['variant'] = variant\n quant_results.append(res)\n print(f\" {VARIANT_NAMES[variant]:25s} | \"\n f\"Sharpe {res['sharpe']:+.2f} | \"\n f\"Return {res['ann_return']:+.1%} | \"\n f\"MaxDD {res['max_dd']:.1%} | \"\n f\"Sortino {res['sortino']:+.2f}\")\n\n # =========================================================================\n # PART 2: LLM overlay test (recent 12 months only)\n # =========================================================================\n print(f\"\\n\\n{'=' * 95}\")\n print(f\"PART 2: LLM OVERLAY TEST (recent {LLM_TEST_MONTHS} months)\")\n print(\"NOTE: This is the only honest LLM test — no backtest lookahead\")\n print(f\"{'=' * 95}\")\n\n llm_analyzer = LLMAnalyzer()\n llm_start = date(END_DATE.year - 1, END_DATE.month, 1) # 1 year back\n llm_start = max(llm_start, df['trade_date'].min() + timedelta(days=380))\n\n print(f\"\\nLLM test period: {llm_start} to {actual_end}\")\n print(f\"Expected R1 API calls: ~{LLM_TEST_MONTHS * 3} (3 LLM variants x {LLM_TEST_MONTHS} months)\")\n print()\n\n llm_results = []\n all_llm_signals = {}\n\n # First run quant baselines for same period\n for variant in quant_variants:\n res, _ = run_single(variant, idx, llm_start, actual_end)\n if res:\n res['years'] = 1\n res['variant'] = variant\n llm_results.append(res)\n print(f\" {VARIANT_NAMES[variant]:25s} | \"\n f\"Sharpe {res['sharpe']:+.2f} | \"\n f\"Return {res['ann_return']:+.1%} | \"\n f\"MaxDD {res['max_dd']:.1%} | \"\n f\"Sortino {res['sortino']:+.2f} | (quant baseline)\")\n\n # Now run LLM variants\n llm_variants = ['llm_conservative', 'llm_tactical', 'llm_full']\n for variant in llm_variants:\n print(f\"\\n Running {VARIANT_NAMES[variant]} with R1 calls...\")\n res, signals = run_single(\n variant, idx, llm_start, actual_end,\n llm_analyzer=llm_analyzer, sector_groups=sector_groups,\n use_llm=True\n )\n if res:\n res['years'] = 1\n res['variant'] = variant\n llm_results.append(res)\n all_llm_signals[variant] = signals\n print(f\" {VARIANT_NAMES[variant]:25s} | \"\n f\"Sharpe {res['sharpe']:+.2f} | \"\n f\"Return {res['ann_return']:+.1%} | \"\n f\"MaxDD {res['max_dd']:.1%} | \"\n f\"Sortino {res['sortino']:+.2f} | \"\n f\"R1 calls: {len(signals)}\")\n\n # =========================================================================\n # PART 3: Summary\n # =========================================================================\n print(f\"\\n\\n{'=' * 95}\")\n print(\"SUMMARY: QUANT-ONLY (ALL TIMEFRAMES)\")\n print(f\"{'=' * 95}\")\n\n q_lookup = {}\n for r in quant_results:\n q_lookup[(r['variant'], r['years'])] = r\n\n header = f\"{'Strategy':25s}\"\n for y in TIMEFRAMES_QUANT:\n header += f\" | {y:>5d}y\"\n header += \" | Avg\"\n print(header)\n print(\"-\" * len(header))\n\n for variant in quant_variants:\n name = VARIANT_NAMES[variant]\n row_s = f\"{name:25s}\"\n row_d = f\"{'':25s}\"\n s_vals, d_vals = [], []\n for y in TIMEFRAMES_QUANT:\n r = q_lookup.get((variant, y))\n if r:\n row_s += f\" | {r['sharpe']:+5.2f}\"\n row_d += f\" | {r['max_dd']:5.1%}\"\n s_vals.append(r['sharpe'])\n d_vals.append(r['max_dd'])\n else:\n row_s += \" | --\"\n row_d += \" | --\"\n row_s += f\" | {np.mean(s_vals):+5.2f}\" if s_vals else \" | --\"\n row_d += f\" | {np.mean(d_vals):5.1%}\" if d_vals else \" | --\"\n print(f\" Sharpe: {row_s}\")\n print(f\" MaxDD: {row_d}\")\n print()\n\n print(f\"\\n{'=' * 95}\")\n print(f\"COMPARISON: QUANT vs LLM (recent {LLM_TEST_MONTHS} months)\")\n print(f\"{'=' * 95}\")\n print(f\"{'Strategy':25s} | {'Sharpe':>7s} | {'Return':>7s} | {'MaxDD':>7s} | {'Sortino':>8s} | {'Calmar':>7s}\")\n print(\"-\" * 80)\n\n for r in llm_results:\n tag = \" (quant)\" if r['variant'] in quant_variants else \" (LLM)\"\n print(f\" {r['strategy']:25s} | {r['sharpe']:+6.2f} | \"\n f\"{r['ann_return']:+6.1%} | {r['max_dd']:6.1%} | \"\n f\"{r['sortino']:+7.2f} | {r['calmar']:6.2f}{tag}\")\n\n # LLM signal details\n print(f\"\\n{'=' * 95}\")\n print(\"LLM R1 SIGNAL DETAILS\")\n print(f\"{'=' * 95}\")\n\n for variant, signals in all_llm_signals.items():\n name = VARIANT_NAMES[variant]\n print(f\"\\n {name}:\")\n if not signals:\n print(\" No signals generated\")\n continue\n for sig in signals:\n risk = sig.get('risk_level', '?')\n reason = sig.get('reasoning', 'N/A')[:60]\n sadj = sig.get('stock_adj', 0)\n print(f\" {sig['date']} | Risk: {risk:>3} | \"\n f\"StockAdj: {sadj:+.2f} | {reason}\")\n\n # API usage\n print(f\"\\n{'=' * 95}\")\n print(\"LLM API USAGE\")\n print(f\"{'=' * 95}\")\n print(f\" Total R1 calls: {llm_analyzer.call_count}\")\n print(f\" Total tokens: {llm_analyzer.total_tokens:,}\")\n est_cost = llm_analyzer.total_tokens * 0.000004 # rough estimate\n print(f\" Est. cost: ${est_cost:.2f}\")\n\n # Final verdict\n print(f\"\\n{'=' * 95}\")\n print(\"FINAL VERDICT\")\n print(f\"{'=' * 95}\")\n\n quant_best = None\n llm_best = None\n for r in llm_results:\n if r['variant'] in quant_variants:\n if quant_best is None or r['sharpe'] > quant_best['sharpe']:\n quant_best = r\n else:\n if llm_best is None or r['sharpe'] > llm_best['sharpe']:\n llm_best = r\n\n if quant_best and llm_best:\n sharpe_diff = llm_best['sharpe'] - quant_best['sharpe']\n dd_diff = llm_best['max_dd'] - quant_best['max_dd']\n print(f\" Best quant: {quant_best['strategy']:25s} — \"\n f\"Sharpe {quant_best['sharpe']:+.2f}, DD {quant_best['max_dd']:.1%}\")\n print(f\" Best LLM: {llm_best['strategy']:25s} — \"\n f\"Sharpe {llm_best['sharpe']:+.2f}, DD {llm_best['max_dd']:.1%}\")\n print(f\" Sharpe diff: {sharpe_diff:+.2f}\")\n print(f\" DD diff: {dd_diff:+.1%}\")\n if sharpe_diff > 0.05 and dd_diff <= 0:\n print(f\" VERDICT: LLM overlay IMPROVES risk-adjusted returns\")\n elif sharpe_diff < -0.05:\n print(f\" VERDICT: LLM overlay HURTS performance — stick with quant\")\n else:\n print(f\" VERDICT: LLM overlay has MARGINAL impact — quant sufficient\")\n\n print(f\"\\n{'=' * 95}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}}} |