Files
NexQuant/scripts/nexquant_daily_strategies.py
T
TPTBusiness d4611b530e feat: model-track bias + daily/portfolio tools
- Bandit: model arm prior bias 2.0, prior_var 5.0 → 77% model preference
- Default first action: model (was factor)
- Daily strategy generator: Kronos + factor grid search on daily resolution
- Grid search tool: fixed template, no LLM, deterministic
- Portfolio optimizer: greedy correlation-aware selection, leverage scaling
2026-05-17 20:09:46 +02:00

279 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Daily Strategy Generator — Kronos factors at daily resolution.
Daily timeframe eliminates 1-min noise and transaction cost overhead.
Factors with daily IC translate directly to daily trading edge.
"""
import json
import os
import time
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT = Path(__file__).resolve().parent.parent
FACTORS_DIR = PROJECT / "results" / "factors"
VALUES_DIR = FACTORS_DIR / "values"
RESULTS_DIR = PROJECT / "results" / "strategies_new"
OHLCV_PATH = Path(os.getenv("PREDIX_OHLCV_PATH",
str(PROJECT / "git_ignore_folder" / "intraday_pv_all.h5")))
MIN_MONTHLY = 5.0 # Raw backtest target (conservative for daily)
MIN_SHARPE = 1.0
MAX_DD = -0.20
MIN_TRADES = 30
def load_kronos(name: str) -> pd.Series:
s = pd.read_parquet(VALUES_DIR / f"{name}.parquet")
col = s.columns[0]
return s.xs("EURUSD", level="instrument")[col]
def load_factor_ic(name: str) -> float:
jf = FACTORS_DIR / f"{name}.json"
if jf.exists():
return float(json.loads(jf.read_text()).get("ic", 0))
return 0.0
def daily_backtest(close_daily: pd.Series, signal_daily: pd.Series) -> dict:
"""Simple daily backtest — no intraday noise, no 1-min costs."""
common = close_daily.index.intersection(signal_daily.index)
c = close_daily.loc[common]
s = signal_daily.loc[common].clip(-1, 1)
rets = c.pct_change().shift(-1) # Next day's return
strat_rets = s.shift(1) * rets # Today's signal × tomorrow's return
strat_rets = strat_rets.dropna()
if len(strat_rets) < 10:
return {"sharpe": 0, "monthly_pct": 0, "max_dd": 0, "n_trades": 0, "win_rate": 0}
# Trade-level stats
trades = []
in_trade = False
trade_ret = 0.0
wins = 0
for r, sig in zip(strat_rets, s.loc[strat_rets.index]):
if sig != 0:
if not in_trade:
in_trade = True
trade_ret = r
else:
trade_ret += r
elif in_trade:
in_trade = False
trades.append(trade_ret)
if trade_ret > 0:
wins += 1
trade_ret = 0.0
if in_trade:
trades.append(trade_ret)
if trade_ret > 0:
wins += 1
n_trades = len(trades)
if n_trades < 5:
return {"sharpe": 0, "monthly_pct": 0, "max_dd": 0, "n_trades": n_trades, "win_rate": 0}
t_arr = np.array(trades)
sharpe = float(t_arr.mean() / t_arr.std() * np.sqrt(n_trades)) if t_arr.std() > 0 else 0.0
win_rate = wins / n_trades
# Equity curve
eq = (1 + pd.Series(trades)).cumprod()
peak = eq.cummax()
dd = float(((eq - peak) / peak).min())
total_ret = eq.iloc[-1] - 1 if len(eq) > 0 else 0.0
n_days = (close_daily.index[-1] - close_daily.index[0]).days
n_months = n_days / 30.44
monthly = float((1 + total_ret) ** (1 / max(n_months, 1)) - 1)
return {
"sharpe": sharpe, "monthly_pct": monthly * 100,
"max_dd": dd, "n_trades": n_trades, "win_rate": win_rate,
"total_return": total_ret, "n_months": n_months,
}
def build_signal(daily_factor: pd.Series, ic: float, threshold_sigma: float,
session: str = "all") -> pd.Series:
"""Build daily signal from a single factor."""
sigma = daily_factor.std()
thresh = threshold_sigma * sigma
# Invert if IC is negative
sign = -1 if ic < 0 else 1
signal = pd.Series(0, index=daily_factor.index, dtype=int)
signal[daily_factor > thresh] = sign
signal[daily_factor < -thresh] = -sign
# Smooth: keep signal for min_hold days to avoid whipsaw
signal = signal.replace(0, np.nan).ffill(limit=1).fillna(0).astype(int)
return signal
def combine_signals(s1: pd.Series, s2: pd.Series, mode: str = "confirm") -> pd.Series:
"""Combine two daily signals."""
common = s1.index.intersection(s2.index)
s1c = s1.loc[common]
s2c = s2.loc[common]
if mode == "confirm":
result = pd.Series(0, index=common, dtype=int)
result[(s1c == s2c) & (s1c != 0)] = s1c
return result
elif mode == "any":
result = s1c.copy()
result[(result == 0) & (s2c != 0)] = s2c
return result
else:
return s1c
def main():
print("=" * 60)
print(" Daily Strategy Generator")
print("=" * 60)
# Load OHLCV → daily
print("\nLoading OHLCV...")
df = pd.read_hdf(OHLCV_PATH, key="data")
close = df.xs("EURUSD", level="instrument")["$close"].sort_index()
close_daily = close.resample("D").last().dropna()
print(f" Daily bars: {len(close_daily)} ({close_daily.index[0].date()}{close_daily.index[-1].date()})")
# Load Kronos factors → daily
print("\nLoading Kronos factors...")
kronos = {}
for name in ["KronosPredReturn_p96", "KronosPredReturn_p24", "KronosPredReturn_p48"]:
series = load_kronos(name)
ic = load_factor_ic(name)
daily = series.resample("D").last().dropna()
# Align to close_daily
daily = daily.reindex(close_daily.index)
kronos[name] = {"series": daily, "ic": ic, "std": daily.std()}
print(f" {name}: IC={ic:+.4f} daily_rows={daily.dropna().sum()}")
# Load top daily factors
print("\nLoading top daily factors...")
daily_factors = {}
for f in sorted(FACTORS_DIR.glob("*.json")):
d = json.loads(f.read_text())
if not isinstance(d, dict):
continue
ic = float(d.get("ic") or 0)
if abs(ic) < 0.06:
continue
fname = d.get("factor_name") or d.get("name") or f.stem
safe = fname.replace("/", "_").replace("\\", "_")[:150]
parq = VALUES_DIR / f"{safe}.parquet"
if not parq.exists():
continue
series = pd.read_parquet(str(parq))
if isinstance(series.index, pd.MultiIndex):
series = series.xs("EURUSD", level="instrument")[series.columns[0]]
daily = series.resample("D").last().dropna().reindex(close_daily.index)
daily_factors[fname] = {"series": daily, "ic": ic, "std": daily.std()}
names = list(daily_factors.keys())
print(f" Loaded {len(names)} factors (IC ≥ 0.06)")
# Grid search
thresholds = [1.0, 1.5, 2.0, 2.5, 3.0]
results = []
t0 = time.time()
# A) Kronos single-factor
print("\n--- Kronos single-factor grid ---")
for kname, kdata in kronos.items():
ks = kdata["series"]
for thresh in thresholds:
signal = build_signal(ks, kdata["ic"], thresh)
bt = daily_backtest(close_daily, signal)
bt["strategy"] = f"{kname} t={thresh}σ"
bt["factors"] = [kname]
bt["threshold"] = thresh
results.append(bt)
# B) Kronos + daily factor (confirmation)
print("--- Kronos + daily factor combinations ---")
for kname, kdata in kronos.items():
ks = kdata["series"]
for fname, fdata in daily_factors.items():
for thresh_k in [1.5, 2.0]:
for thresh_f in [1.0, 1.5, 2.0]:
s1 = build_signal(ks, kdata["ic"], thresh_k)
s2 = build_signal(fdata["series"], fdata["ic"], thresh_f)
signal = combine_signals(s1, s2, "confirm")
bt = daily_backtest(close_daily, signal)
bt["strategy"] = f"{kname}(t={thresh_k}) + {fname}(t={thresh_f})"
bt["factors"] = [kname, fname]
bt["threshold"] = f"{thresh_k}/{thresh_f}"
results.append(bt)
# C) Two daily factors (no Kronos)
print("--- Daily factor pairs ---")
name_list = list(daily_factors.keys())
for i in range(min(len(name_list), 10)):
for j in range(i + 1, min(len(name_list), 10)):
f1, f2 = name_list[i], name_list[j]
for t1 in [1.0, 1.5, 2.0]:
for t2 in [1.0, 1.5, 2.0]:
s1 = build_signal(daily_factors[f1]["series"], daily_factors[f1]["ic"], t1)
s2 = build_signal(daily_factors[f2]["series"], daily_factors[f2]["ic"], t2)
signal = combine_signals(s1, s2, "confirm")
bt = daily_backtest(close_daily, signal)
bt["strategy"] = f"{f1[:20]}(t={t1}) + {f2[:20]}(t={t2})"
bt["factors"] = [f1, f2]
bt["threshold"] = f"{t1}/{t2}"
results.append(bt)
# Filter & sort
print(f"\n{'=' * 60}")
print(f" Total evaluations: {len(results)} Time: {time.time()-t0:.0f}s")
print(f"{'=' * 60}")
valid = [r for r in results
if r["sharpe"] >= MIN_SHARPE
and r["max_dd"] >= MAX_DD
and r["n_trades"] >= MIN_TRADES
and r["monthly_pct"] >= MIN_MONTHLY]
valid.sort(key=lambda r: r["monthly_pct"], reverse=True)
print(f"\n Meeting: Sharpe≥{MIN_SHARPE} DD≥{MAX_DD} Tr≥{MIN_TRADES} Mon≥{MIN_MONTHLY}%")
print(f" → {len(valid)} strategies\n")
fmt = "{:3s} {:55s} {:>7s} {:>7s} {:>7s} {:>5s} {:>6s}"
print(fmt.format("#", "Strategy", "Sharpe", "Mon%", "MaxDD", "Tr", "WinRt"))
print("-" * 90)
for i, r in enumerate(valid[:30], 1):
print(fmt.format(str(i), r["strategy"][:55],
f'{r["sharpe"]:.2f}', f'{r["monthly_pct"]:.1f}%',
f'{r["max_dd"]:.3f}', str(r["n_trades"]),
f'{r["win_rate"]:.1%}'))
if not valid:
results.sort(key=lambda r: r["monthly_pct"], reverse=True)
print("\n Top 10 by monthly return:")
for i, r in enumerate(results[:10], 1):
print(f" {i:2d}. {r['strategy'][:50]} Mon={r['monthly_pct']:.1f}% Sh={r['sharpe']:.2f} Tr={r['n_trades']}")
# Save
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
out = RESULTS_DIR / f"daily_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
out.write_text(json.dumps(valid[:50] if valid else results[:50], indent=2, default=str))
print(f"\n Saved → {out}")
if __name__ == "__main__":
main()