diff --git a/scripts/nexquant_daily_strategies.py b/scripts/nexquant_daily_strategies.py new file mode 100644 index 00000000..3b2584c8 --- /dev/null +++ b/scripts/nexquant_daily_strategies.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +""" +NexQuant Daily Strategy Generator — systematisch, kein LLM. + +Grid-search für SMA/EMA/RSI/MACD/Momentum/Mean-Reversion auf Tagesdaten. +Speichert Top-Strategien als JSON für den Live-Trading-Workflow. + +Usage: + python scripts/nexquant_daily_strategies.py + python scripts/nexquant_daily_strategies.py --top 10 --cost 2.14 +""" + +from __future__ import annotations + +import json, sys, time +from datetime import datetime +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_ftmo + +DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5") +OUT_DIR = Path("results/strategies_daily") +OUT_DIR.mkdir(parents=True, exist_ok=True) + +TXN_COST_BPS = 2.14 +MIN_TRADES_OOS = 5 + + +def load_daily_data(): + close = pd.read_hdf(DATA_PATH, key="data")["$close"] + if isinstance(close.index, pd.MultiIndex): + close = close.droplevel(-1) + return close.sort_index().dropna().resample("1D").last().dropna() + + +def backtest(signal: pd.Series, close: pd.Series) -> dict: + if signal is None or len(signal) < 10: + return {} + sig = signal.fillna(0).replace([np.inf, -np.inf], 0) + r = backtest_signal_ftmo(close, sig, txn_cost_bps=TXN_COST_BPS, wf_rolling=True) + return { + "is_sharpe": r.get("is_sharpe", None), + "is_monthly_pct": r.get("is_monthly_return_pct", None), + "is_trades": r.get("is_n_trades", 0), + "oos_sharpe": r.get("oos_sharpe", None), + "oos_monthly_pct": r.get("oos_monthly_return_pct", None), + "oos_max_dd": r.get("oos_max_drawdown", None), + "oos_win_rate": r.get("oos_win_rate", None), + "oos_trades": r.get("oos_n_trades", 0), + "wf_sharpe": r.get("wf_oos_sharpe_mean", None), + "wf_monthly_pct": r.get("wf_oos_monthly_return_mean", None), + "wf_consistency": r.get("wf_oos_consistency", None), + "mc_pvalue": r.get("mc_pvalue", None), + "full_metrics": r, + } + + +def make_sma_signal(close, fast, slow): + f = close.rolling(fast).mean() + s = close.rolling(slow).mean() + sig = pd.Series(0.0, index=close.index) + sig[f > s] = 1 + sig[f < s] = -1 + return sig + + +def make_ema_signal(close, fast, slow): + f = close.ewm(span=fast).mean() + s = close.ewm(span=slow).mean() + sig = pd.Series(0.0, index=close.index) + sig[f > s] = 1 + sig[f < s] = -1 + return sig + + +def make_rsi_signal(close, period, oversold, overbought): + delta = close.diff() + gain = delta.clip(lower=0) + loss = -delta.clip(upper=0) + rsi = 100 - (100 / (1 + gain.rolling(period).mean() / (loss.rolling(period).mean() + 1e-8))) + sig = pd.Series(0.0, index=close.index) + sig[rsi < oversold] = 1 + sig[rsi > overbought] = -1 + return sig + + +def make_macd_signal(close, fast, slow, signal_period): + ema_fast = close.ewm(span=fast).mean() + ema_slow = close.ewm(span=slow).mean() + macd = ema_fast - ema_slow + sig_line = macd.ewm(span=signal_period).mean() + sig = pd.Series(0.0, index=close.index) + sig[macd > sig_line] = 1 + sig[macd < sig_line] = -1 + return sig + + +def make_momentum_signal(close, n): + mom = close.pct_change(n) + return pd.Series(np.sign(mom).fillna(0), index=close.index) + + +def make_meanrev_signal(close, n): + ret = close.pct_change(n) + return pd.Series(-np.sign(ret).fillna(0), index=close.index) + + +def make_bollinger_signal(close, period, std_dev): + ma = close.rolling(period).mean() + std = close.rolling(period).std() + sig = pd.Series(0.0, index=close.index) + sig[close < ma - std_dev * std] = 1 + sig[close > ma + std_dev * std] = -1 + return sig + + +def main(top_n=15, cost_bps=2.14): + global TXN_COST_BPS + TXN_COST_BPS = cost_bps + + print(f"\n{'='*60}") + print(f" NexQuant Daily Strategy Generator") + print(f" Cost: {cost_bps} bps | Saving top {top_n}") + print(f"{'='*60}") + + close = load_daily_data() + print(f"Data: {len(close):,} daily bars ({close.index[0].date()} - {close.index[-1].date()})\n") + + results = [] + + # SMA Crossovers + print("SMA crossovers...") + for fast in [5, 10, 15, 20, 30]: + for slow in [fast * 2, fast * 3, fast * 4, fast * 5]: + if slow > 250: continue + sig = make_sma_signal(close, fast, slow) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("SMA", f"SMA{fast}/{slow}", fast, slow, score, bt)) + + # EMA Crossovers + print("EMA crossovers...") + for fast in [5, 10, 15, 20, 30]: + for slow in [fast * 2, fast * 3, fast * 4, fast * 5]: + if slow > 250: continue + sig = make_ema_signal(close, fast, slow) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("EMA", f"EMA{fast}/{slow}", fast, slow, score, bt)) + + # RSI + print("RSI strategies...") + for period in [7, 10, 14, 21]: + for oversold, overbought in [(20, 80), (25, 75), (30, 70), (35, 65)]: + sig = make_rsi_signal(close, period, oversold, overbought) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("RSI", f"RSI{period}({oversold}/{overbought})", period, 0, score, bt)) + + # MACD + print("MACD...") + for fast, slow, sig_p in [(8, 17, 9), (12, 26, 9), (5, 35, 5), (10, 20, 7)]: + s = make_macd_signal(close, fast, slow, sig_p) + bt = backtest(s, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("MACD", f"MACD{fast}/{slow}/{sig_p}", fast, slow, score, bt)) + + # Momentum + print("Momentum...") + for n in [5, 10, 20, 30, 50, 60, 90, 100, 120, 150, 200]: + sig = make_momentum_signal(close, n) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("Mom", f"Mom{n}d", n, 0, score, bt)) + + # Mean Reversion + print("Mean reversion...") + for n in [3, 5, 7, 10, 15, 20, 30, 50]: + sig = make_meanrev_signal(close, n) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("MR", f"MR{n}d", n, 0, score, bt)) + + # Bollinger Bands + print("Bollinger...") + for period in [10, 20, 50]: + for std_dev in [1.5, 2.0, 2.5]: + sig = make_bollinger_signal(close, period, std_dev) + bt = backtest(sig, close) + if bt.get("oos_trades", 0) >= MIN_TRADES_OOS: + score = bt.get("oos_sharpe") or -999 + results.append(("BB", f"BB{period}/{std_dev}", period, std_dev, score, bt)) + + # Sort by OOS Sharpe + results.sort(key=lambda x: x[4] if x[4] is not None else -999, reverse=True) + + print(f"\n{'='*70}") + print(f" TOP {top_n} DAILY STRATEGIES (Cost: {cost_bps} bps)") + print(f"{'='*70}") + print(f" {'#':<3} {'Type':<6} {'Name':<22} {'OOS S':>8} {'Mon%':>7} {'DD%':>6} {'WF S':>8} {'Trades':>6}") + print(f" {'-'*68}") + + saved = [] + for i, (stype, name, p1, p2, score, bt) in enumerate(results[:top_n]): + oos_m = (bt.get("oos_monthly_pct") or 0) + oos_dd = (bt.get("oos_max_dd") or 0) * 100 + wf_s = bt.get("wf_sharpe") or 0 + trades = bt.get("oos_trades", 0) + status = "✅" if score > 0 else " " + print(f" {i+1:<3} {stype:<6} {name:<22} {score:>+8.2f} {oos_m:>+6.2f}% {oos_dd:>+5.1f}% {wf_s:>+8.2f} {trades:>6} {status}") + + entry = { + "strategy_name": name, + "type": stype, + "param1": p1, + "param2": p2, + "cost_bps": cost_bps, + "frequency": "daily", + "generated_at": datetime.now().isoformat(), + "metrics": {k: v for k, v in bt.items() if k != "full_metrics"}, + } + saved.append(entry) + + # Save individual strategy + safe_name = name.replace("(", "").replace(")", "").replace("/", "-") + fname = OUT_DIR / f"daily_{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + with open(fname, "w") as f: + json.dump(entry, f, indent=2) + + # Save summary + summary = { + "generated_at": datetime.now().isoformat(), + "cost_bps": cost_bps, + "frequency": "daily", + "n_bars": len(close), + "date_range": [str(close.index[0].date()), str(close.index[-1].date())], + "top_strategies": [ + {"name": s["strategy_name"], "oos_sharpe": s["metrics"].get("oos_sharpe"), + "oos_monthly_pct": s["metrics"].get("oos_monthly_pct")} + for s in saved[:10] + ], + } + with open(OUT_DIR / "daily_summary.json", "w") as f: + json.dump(summary, f, indent=2) + + profit_count = sum(1 for r in results if r[4] and r[4] > 0) + print(f"\n{profit_count}/{len(results)} strategies profitable ({profit_count/len(results)*100:.0f}%)") + print(f"Saved to {OUT_DIR}/") + return saved + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--top", type=int, default=15) + parser.add_argument("--cost", type=float, default=2.14) + args = parser.parse_args() + main(top_n=args.top, cost_bps=args.cost)