#!/usr/bin/env python """ Quick Daytrading Strategy Generator with CORRECT factor alignment. Uses forward-fill to align daily factors to 1-min frequency, then runs fast backtests without LLM calls. Usage: python nexquant_quick_daytrading.py 5 python nexquant_quick_daytrading.py 10 """ import json, time, subprocess, tempfile # nosec from pathlib import Path import numpy as np import pandas as pd from rich.console import Console console = Console() STRATEGIES_DIR = Path('results/strategies_new') STRATEGIES_DIR.mkdir(parents=True, exist_ok=True) FACTOR_FILES = Path('results/factors') VALUE_FILES = FACTOR_FILES / 'values' OHLCV_PATH = Path('git_ignore_folder/factor_implementation_source_data/intraday_pv.h5') # Best daytrading strategies (12-min horizon, optimized for RiskMgmt) DAYTRADING_COMBOS = [ { 'name': 'MomentumDivergence12min', 'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d'], 'code': '''mom = factors['daily_close_return_96'] div = factors['daily_session_momentum_divergence_1d'] w = 20 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) composite = (mom_z - div_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.3] = 1 signal[composite < -0.3] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'LondonSessionScalp', 'factors': ['london_mom', 'daily_session_momentum_divergence_1d'], 'code': '''mom = factors['london_mom'] div = factors['daily_session_momentum_divergence_1d'] w = 15 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) composite = (mom_z - div_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.25] = 1 signal[composite < -0.25] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'TrendReversionScalp', 'factors': ['daily_ols_slope_96', 'daily_session_momentum_divergence_1d', 'DailyTrendStrength_Raw'], 'code': '''slope = factors['daily_ols_slope_96'] div = factors['daily_session_momentum_divergence_1d'] trend = factors['DailyTrendStrength_Raw'] w = 20 slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) trend_z = (trend - trend.rolling(w).mean()) / (trend.rolling(w).std() + 1e-8) composite = (0.5 * slope_z - 0.3 * div_z + 0.2 * trend_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.3] = 1 signal[composite < -0.3] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'VolAdjMomentum12', 'factors': ['daily_ret_vol_adj_1d', 'daily_session_momentum_divergence_1d', 'DCP'], 'code': '''vol = factors['daily_ret_vol_adj_1d'] div = factors['daily_session_momentum_divergence_1d'] dcp = factors['DCP'] w = 20 vol_z = (vol - vol.rolling(w).mean()) / (vol.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8) composite = (0.5 * vol_z - 0.3 * div_z + 0.2 * dcp_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.35] = 1 signal[composite < -0.35] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'SessionMeanReversion', 'factors': ['session_momentum_diff', 'daily_norm_body', 'daily_c2c_return'], 'code': '''session = factors['session_momentum_diff'] body = factors['daily_norm_body'] c2c = factors['daily_c2c_return'] w = 15 sess_z = (session - session.rolling(w).mean()) / (session.rolling(w).std() + 1e-8) body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8) c2c_z = (c2c - c2c.rolling(w).mean()) / (c2c.rolling(w).std() + 1e-8) composite = (0.5 * sess_z + 0.3 * body_z + 0.2 * c2c_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.4] = 1 signal[composite < -0.4] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'MomentumContinuation', 'factors': ['daily_mom', 'daily_ret_1d', 'momentum_1d'], 'code': '''mom = factors['daily_mom'] ret = factors['daily_ret_1d'] mom2 = factors['momentum_1d'] w = 12 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) ret_z = (ret - ret.rolling(w).mean()) / (ret.rolling(w).std() + 1e-8) mom2_z = (mom2 - mom2.rolling(w).mean()) / (mom2.rolling(w).std() + 1e-8) composite = (0.4 * mom_z + 0.3 * ret_z + 0.3 * mom2_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.2] = 1 signal[composite < -0.2] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'HighFreqScalper', 'factors': ['daily_close_return_96', 'DCP', 'london_mom'], 'code': '''close_ret = factors['daily_close_return_96'] dcp = factors['DCP'] london = factors['london_mom'] w = 10 cr_z = (close_ret - close_ret.rolling(w).mean()) / (close_ret.rolling(w).std() + 1e-8) dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8) lon_z = (london - london.rolling(w).mean()) / (london.rolling(w).std() + 1e-8) composite = (0.4 * cr_z + 0.3 * dcp_z + 0.3 * lon_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.25] = 1 signal[composite < -0.25] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'AdaptiveMomentumMR', 'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_ols_slope_96'], 'code': '''mom = factors['daily_close_return_96'] div = factors['daily_session_momentum_divergence_1d'] slope = factors['daily_ols_slope_96'] w = 20 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) slope_z = (slope - slope.rolling(w).mean()) / (slope.rolling(w).std() + 1e-8) # Regime detection: high momentum = trend, low = mean reversion regime = (mom_z.abs() > 1.0).astype(float) composite = (regime * mom_z + (1 - regime) * (-div_z) + 0.3 * slope_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.4] = 1 signal[composite < -0.4] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'TrendPullbackScalp', 'factors': ['daily_close_return_96', 'daily_session_momentum_divergence_1d', 'daily_norm_body'], 'code': '''mom = factors['daily_close_return_96'] div = factors['daily_session_momentum_divergence_1d'] body = factors['daily_norm_body'] w = 15 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) body_z = (body - body.rolling(w).mean()) / (body.rolling(w).std() + 1e-8) # Enter on pullbacks (divergence against trend) composite = (mom_z - 0.5 * div_z * mom_z.sign() + 0.2 * body_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.35] = 1 signal[composite < -0.35] = -1 signal = signal.fillna(0).astype(int)''', }, { 'name': 'IntradayMomentumBlend', 'factors': ['daily_close_return_96', 'london_mom', 'daily_session_momentum_divergence_1d', 'DCP'], 'code': '''mom = factors['daily_close_return_96'] lon = factors['london_mom'] div = factors['daily_session_momentum_divergence_1d'] dcp = factors['DCP'] w = 20 mom_z = (mom - mom.rolling(w).mean()) / (mom.rolling(w).std() + 1e-8) lon_z = (lon - lon.rolling(w).mean()) / (lon.rolling(w).std() + 1e-8) div_z = (div - div.rolling(w).mean()) / (div.rolling(w).std() + 1e-8) dcp_z = (dcp - dcp.rolling(w).mean()) / (dcp.rolling(w).std() + 1e-8) composite = (0.3 * mom_z + 0.3 * lon_z - 0.2 * div_z + 0.2 * dcp_z).fillna(0) signal = pd.Series(0, index=close.index, name='signal') signal[composite > 0.3] = 1 signal[composite < -0.3] = -1 signal = signal.fillna(0).astype(int)''', }, ] def load_factor_series(name): """Load factor parquet and return as Series with correct index.""" safe = name.replace('/','_').replace('\\','_')[:150] pf = VALUE_FILES / f"{safe}.parquet" if not pf.exists(): return None df = pd.read_parquet(str(pf)) # Extract EURUSD if df.index.names == ['datetime', 'instrument']: df_reset = df.reset_index() if 'instrument' in df_reset.columns: df_eur = df_reset[df_reset['instrument'] == 'EURUSD'].copy() df_eur = df_eur.set_index('datetime') series = df_eur.iloc[:, -1] # Last column is the factor value series.name = name return series # If single index, just return first column series = df.iloc[:, 0] series.name = name return series def main(n_strategies=5): console.print("[bold cyan]🎯 Daytrading Strategy Generator (Quick Mode)[/bold cyan]\n") console.print(" Style: 12-minute forward returns") console.print(" Target: RiskMgmt compliant (IC>0.02, Sharpe>0.5, Trades>20, DD>-10%)\n") # Load OHLCV data if not OHLCV_PATH.exists(): console.print(f"[red]✗ OHLCV data not found: {OHLCV_PATH}[/red]") return ohlcv = pd.read_hdf(str(OHLCV_PATH), key='data') # Extract close prices with datetime-only index (not MultiIndex) if '$close' in ohlcv.columns: close = ohlcv['$close'].dropna() elif 'close' in ohlcv.columns: close = ohlcv['close'].dropna() else: close = ohlcv.select_dtypes(include=[np.number]).iloc[:, 0].dropna() # Extract datetime from MultiIndex if present if isinstance(close.index, pd.MultiIndex): close_dt_idx = close.index.get_level_values('datetime') close_series = pd.Series(close.values, index=close_dt_idx, name='close') else: close_series = close close_series = close_series.dropna() console.print(f"[green]✓[/green] Loaded {len(close_series):,} OHLCV bars") # Load all factor series and align to close index all_factor_series = {} for combo in DAYTRADING_COMBOS: for factor_name in combo['factors']: if factor_name in all_factor_series: continue series = load_factor_series(factor_name) if series is not None: # Forward fill to match close frequency series_ff = series.reindex(close_series.index).ffill() all_factor_series[factor_name] = series_ff # Create factors DataFrame df_factors = pd.DataFrame(all_factor_series) df_factors = df_factors.dropna(how='all') console.print(f"[green]✓[/green] Loaded {len(df_factors.columns)} factor series") console.print(f"[green]✓[/green] Aligned to {len(df_factors):,} bars\n") accepted = [] for i, combo in enumerate(DAYTRADING_COMBOS[:n_strategies]): console.print(f"[{i+1}/{n_strategies}] Testing {combo['name']}...") # Build factor dataframe valid_factors = [f for f in combo['factors'] if f in df_factors.columns] if len(valid_factors) < 2: console.print(f" ✗ Not enough valid factors") continue strat_factors = df_factors[valid_factors].dropna() if len(strat_factors) < 1000: console.print(f" ✗ Not enough data: {len(strat_factors)} bars") continue # Build backtest script forward_bars = 12 strategy_code = combo['code'] script = f""" import pandas as pd import numpy as np import json close = pd.read_pickle('close.pkl') # nosec factors = pd.read_pickle('factors.pkl') # nosec # Execute strategy try: {chr(10).join(' ' + l for l in strategy_code.split(chr(10)))} except Exception as e: print(f"ERROR: {{e}}") exit(1) if 'signal' not in dir(): print("ERROR: No signal generated") exit(1) signal = signal.fillna(0) # Align common_idx = close.index.intersection(signal.index) close = close.loc[common_idx] signal = signal.loc[common_idx] # Forward returns (12-min horizon for daytrading) FORWARD_BARS = {forward_bars} returns_fwd = close.pct_change(FORWARD_BARS).shift(-FORWARD_BARS) signal_aligned = signal.loc[returns_fwd.dropna().index] fwd_returns = returns_fwd.loc[signal_aligned.index] if len(signal_aligned) < 100 or len(fwd_returns) < 100: print("ERROR: Not enough data") exit(1) # Metrics ic = signal_aligned.corr(fwd_returns) strategy_returns = signal_aligned * fwd_returns sharpe = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252 * 1440 / {forward_bars}) if strategy_returns.std() > 0 else 0 cum = (1 + strategy_returns).cumprod() running_max = cum.expanding().max() drawdown = (cum - running_max) / running_max.replace(0, np.nan) max_dd = drawdown.min() if len(drawdown) > 0 else 0 win_rate = (strategy_returns > 0).sum() / len(strategy_returns) if len(strategy_returns) > 0 else 0 n_trades = int((signal_aligned != signal_aligned.shift(1)).sum()) total_return = cum.iloc[-1] - 1 n_bars = len(strategy_returns) n_months = n_bars / (252 * 1440 / {forward_bars} / 12) if n_bars > 0 else 1 monthly_return = (1 + total_return) ** (1 / n_months) - 1 if n_months > 0 and (1 + total_return) > 0 else total_return result = {{ "status": "success", "sharpe": float(sharpe), "max_drawdown": float(max_dd) if not np.isnan(max_dd) else -0.20, "win_rate": float(win_rate), "ic": float(ic) if not np.isnan(ic) else 0, "n_trades": n_trades, "total_return": float(total_return), "monthly_return_pct": float(monthly_return * 100), "n_bars": int(n_bars), "n_months": float(n_months), "signal_long": int((signal_aligned == 1).sum()), "signal_short": int((signal_aligned == -1).sum()), "signal_neutral": int((signal_aligned == 0).sum()), }} print(json.dumps(result)) """ # Run backtest import tempfile with tempfile.TemporaryDirectory() as td: tdp = Path(td) strat_close = close_series.loc[strat_factors.index] strat_close.to_pickle(str(tdp / 'close.pkl')) # nosec strat_factors.to_pickle(str(tdp / 'factors.pkl')) # nosec script_path = tdp / 'run.py' script_path.write_text(script) try: result_proc = subprocess.run( # nosec B603 [sys.executable, str(script_path)], capture_output=True, text=True, timeout=60, cwd=str(tdp) ) if result_proc.returncode != 0: console.print(f" ✗ Failed: {result_proc.stderr[:200]}") continue result = None for line in result_proc.stdout.strip().split('\n'): try: result = json.loads(line) break except: continue if not result or result.get('status') != 'success': console.print(f" ✗ Invalid result") continue except subprocess.TimeoutExpired: # nosec console.print(f" ✗ Timeout") continue except Exception as e: console.print(f" ✗ Error: {e}") continue ic = result.get('ic', 0) sharpe = result.get('sharpe', 0) trades = result.get('n_trades', 0) dd = result.get('max_drawdown', 0) # RiskMgmt criteria if abs(ic) > 0.02 and sharpe > 0.5 and trades > 20 and dd > -0.10: strategy = { 'strategy_name': combo['name'], 'factor_names': combo['factors'], 'description': f"Daytrading strategy combining {', '.join(combo['factors'])}", 'code': combo['code'], 'real_backtest': result, 'metrics': result, 'summary': { 'sharpe': sharpe, 'max_drawdown': dd, 'win_rate': result.get('win_rate', 0), 'monthly_return_pct': result.get('monthly_return_pct', 0), 'real_ic': ic, 'real_n_trades': trades, 'forward_bars': 12, 'trading_style': 'daytrading', } } fname = f"{int(time.time())}_{combo['name']}.json" with open(STRATEGIES_DIR / fname, 'w') as f: json.dump(strategy, f, indent=2, ensure_ascii=False) accepted.append(strategy) console.print(f" ✓ [green]ACCEPT[/green]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}") else: console.print(f" ✗ [red]REJECT[/red]: IC={ic:.4f}, Sharpe={sharpe:.2f}, Trades={trades}, DD={dd:.1%}") console.print(f"\n[bold green]✓ {len(accepted)}/{n_strategies} strategies accepted[/bold green]\n") if accepted: console.print("[bold]Results:[/bold]") for s in accepted: bt = s['real_backtest'] console.print(f" • {s['strategy_name']:30s} IC={bt['ic']:.4f} Sharpe={bt['sharpe']:.2f} " f"Monthly={bt['monthly_return_pct']:.2f}% Trades={bt['n_trades']}") if __name__ == '__main__': import sys n = int(sys.argv[1]) if len(sys.argv) > 1 else 5 main(n)