mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
cbe1c52e00
Rename all source files, scripts, tests, documentation, and configuration from Predix/predix to NexQuant/nexquant across the entire codebase.
715 lines
26 KiB
Python
715 lines
26 KiB
Python
"""
|
|
NexQuant Full Data Factor Evaluator - Evaluate factors with FULL 1min data.
|
|
|
|
Evaluates factors using the complete intraday_pv.h5 dataset (2022-2026, ~2.26M rows)
|
|
instead of the debug dataset (2024 only, ~371K rows).
|
|
|
|
Usage:
|
|
python nexquant_full_eval.py --top 100 # Evaluate top 100 factors with full data
|
|
python nexquant_full_eval.py --all # Evaluate all factors
|
|
python nexquant_full_eval.py --parallel 4 # 4 parallel workers
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import warnings
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from rich.console import Console
|
|
from rich.progress import (
|
|
Progress,
|
|
SpinnerColumn,
|
|
TextColumn,
|
|
BarColumn,
|
|
TaskProgressColumn,
|
|
TimeElapsedColumn,
|
|
)
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
|
|
console = Console()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
WORKSPACE_DIR = PROJECT_ROOT / "git_ignore_folder" / "RD-Agent_workspace"
|
|
|
|
# FULL data file (2022-2026, ~72MB)
|
|
FULL_DATA_FILE = PROJECT_ROOT / "git_ignore_folder" / "factor_implementation_source_data" / "intraday_pv.h5"
|
|
|
|
RESULTS_DIR = PROJECT_ROOT / "results"
|
|
BACKTESTS_DIR = RESULTS_DIR / "backtests"
|
|
DB_DIR = RESULTS_DIR / "db"
|
|
DB_PATH = DB_DIR / "backtest_results.db"
|
|
EVAL_SUMMARY_PATH = RESULTS_DIR / "eval_summary.json"
|
|
|
|
# Ensure directories exist
|
|
BACKTESTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
DB_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data structures
|
|
# ---------------------------------------------------------------------------
|
|
@dataclass
|
|
class FactorInfo:
|
|
"""Factor information."""
|
|
workspace_hash: str
|
|
factor_name: str
|
|
factor_code: str
|
|
|
|
|
|
@dataclass
|
|
class EvalResult:
|
|
"""Evaluation result for a single factor."""
|
|
factor_name: str
|
|
workspace_hash: str
|
|
factor_code: str = ""
|
|
factor_description: str = ""
|
|
status: str = "" # success, failed
|
|
ic: Optional[float] = None
|
|
rank_ic: Optional[float] = None
|
|
sharpe: Optional[float] = None
|
|
annualized_return: Optional[float] = None
|
|
max_drawdown: Optional[float] = None
|
|
win_rate: Optional[float] = None
|
|
non_null_count: int = 0
|
|
total_count: int = 0
|
|
error_message: Optional[str] = None
|
|
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
|
|
def to_dict(self) -> dict:
|
|
return {k: v for k, v in self.__dict__.items()}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Factor description extractor
|
|
# ---------------------------------------------------------------------------
|
|
def _extract_factor_description(code: str) -> str:
|
|
"""Extract docstring or description from factor code."""
|
|
import re
|
|
# Try to extract docstring
|
|
match = re.search(r'"""(.*?)"""', code, re.DOTALL)
|
|
if match:
|
|
return match.group(1).strip()[:500]
|
|
# Try to extract from comments
|
|
lines = code.split('\n')
|
|
desc_lines = []
|
|
for line in lines[:20]:
|
|
if line.strip().startswith('#') and not line.strip().startswith('#!'):
|
|
desc_lines.append(line.strip()[1:].strip())
|
|
if desc_lines:
|
|
return ' '.join(desc_lines)[:500]
|
|
return "No description available"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Factor scanner
|
|
# ---------------------------------------------------------------------------
|
|
def scan_factors(workspace_dir: Path, skip_evaluated: bool = True) -> List[FactorInfo]:
|
|
"""Scan workspace directories for unique factor codes.
|
|
|
|
Parameters
|
|
----------
|
|
workspace_dir : Path
|
|
Path to workspace directory
|
|
skip_evaluated : bool
|
|
If True, skip factors that already have valid results in results/factors/
|
|
|
|
Returns
|
|
-------
|
|
List[FactorInfo]
|
|
List of factor information
|
|
"""
|
|
factors = []
|
|
seen_names = set()
|
|
|
|
# Load already evaluated factors (if skip_evaluated is True)
|
|
evaluated_factors = set()
|
|
if skip_evaluated:
|
|
project_root = Path(__file__).parent
|
|
factors_dir = project_root / "results" / "factors"
|
|
if factors_dir.exists():
|
|
import json
|
|
import glob
|
|
for f in glob.glob(str(factors_dir / "*.json")):
|
|
try:
|
|
with open(f) as fh:
|
|
data = json.load(fh)
|
|
if data.get("status") == "success" and data.get("ic") is not None:
|
|
evaluated_factors.add(data.get("factor_name"))
|
|
except Exception:
|
|
pass
|
|
print(f" Found {len(evaluated_factors)} already evaluated factors - skipping")
|
|
|
|
for ws in workspace_dir.iterdir():
|
|
if not ws.is_dir():
|
|
continue
|
|
factor_file = ws / "factor.py"
|
|
result_file = ws / "result.h5"
|
|
if not factor_file.exists():
|
|
continue
|
|
|
|
# Read factor name from result.h5
|
|
factor_name = None
|
|
if result_file.exists():
|
|
try:
|
|
result = pd.read_hdf(str(result_file), key="data")
|
|
if result is not None and len(result.columns) > 0:
|
|
factor_name = result.columns[0]
|
|
except Exception:
|
|
pass
|
|
|
|
if factor_name is None:
|
|
# Try to extract from code
|
|
code = factor_file.read_text()
|
|
import re
|
|
match = re.search(r'def calculate_(\w+)', code)
|
|
if match:
|
|
factor_name = match.group(1)
|
|
else:
|
|
factor_name = f"factor_{ws.name}"
|
|
|
|
# Skip duplicates
|
|
if factor_name in seen_names:
|
|
continue
|
|
|
|
# Skip already evaluated factors
|
|
if skip_evaluated and factor_name in evaluated_factors:
|
|
continue
|
|
|
|
seen_names.add(factor_name)
|
|
|
|
factors.append(FactorInfo(
|
|
workspace_hash=ws.name,
|
|
factor_name=factor_name,
|
|
factor_code=factor_file.read_text(),
|
|
))
|
|
|
|
return factors
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Look-ahead bias detection for daily-constant factors
|
|
# ---------------------------------------------------------------------------
|
|
def _shift_daily_constant_factor_if_needed(factor_col: "pd.Series", factor_name: str) -> "pd.Series":
|
|
"""Detect daily-constant factors (look-ahead bias) and shift by 1 trading day."""
|
|
sample_days = factor_col.index.get_level_values("datetime").normalize().unique()
|
|
if len(sample_days) < 10:
|
|
return factor_col
|
|
rng = np.random.default_rng(42)
|
|
days_to_check = rng.choice(sample_days, size=min(50, len(sample_days)), replace=False)
|
|
constant_count = 0
|
|
for day in days_to_check:
|
|
day_mask = factor_col.index.get_level_values("datetime").normalize() == day
|
|
day_vals = factor_col[day_mask].dropna()
|
|
if len(day_vals) == 0:
|
|
continue
|
|
if day_vals.nunique() == 1:
|
|
constant_count += 1
|
|
fraction_constant = constant_count / len(days_to_check)
|
|
if fraction_constant < 0.90:
|
|
return factor_col
|
|
# Shift by 1 trading day per instrument
|
|
import logging
|
|
logging.getLogger(__name__).info(
|
|
"Factor '%s' is %.0f%% daily-constant — shifting 1 trading day to fix look-ahead bias",
|
|
factor_name, fraction_constant * 100,
|
|
)
|
|
instruments = factor_col.index.get_level_values("instrument").unique() if "instrument" in factor_col.index.names else [None]
|
|
shifted_parts = []
|
|
for instr in instruments:
|
|
if instr is not None:
|
|
mask = factor_col.index.get_level_values("instrument") == instr
|
|
col_instr = factor_col[mask]
|
|
else:
|
|
col_instr = factor_col
|
|
dates = col_instr.index.get_level_values("datetime").normalize()
|
|
trading_days = dates.unique().sort_values()
|
|
day_first = col_instr.groupby(dates).first()
|
|
day_first_shifted = day_first.shift(1)
|
|
day_first_shifted.index = pd.to_datetime(day_first_shifted.index)
|
|
day_map = day_first_shifted.reindex(pd.to_datetime(trading_days)).values
|
|
new_vals = pd.Series(
|
|
day_map[np.searchsorted(trading_days.values, dates.values)],
|
|
index=col_instr.index,
|
|
)
|
|
shifted_parts.append(new_vals)
|
|
return pd.concat(shifted_parts).sort_index()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Factor evaluator
|
|
# ---------------------------------------------------------------------------
|
|
def evaluate_factor_full(factor: FactorInfo, full_data: pd.DataFrame,
|
|
forward_return_bars: int = 96) -> EvalResult:
|
|
"""
|
|
Evaluate a factor using the FULL dataset.
|
|
|
|
Parameters
|
|
----------
|
|
factor : FactorInfo
|
|
Factor information with code
|
|
full_data : pd.DataFrame
|
|
Full intraday_pv.h5 data
|
|
forward_return_bars : int
|
|
Number of bars for forward return calculation
|
|
|
|
Returns
|
|
-------
|
|
EvalResult
|
|
"""
|
|
import tempfile
|
|
import subprocess
|
|
|
|
with tempfile.TemporaryDirectory(prefix="nexquant_full_") as tmp_dir:
|
|
ws = Path(tmp_dir)
|
|
|
|
try:
|
|
# Copy full data to temp workspace
|
|
import shutil
|
|
shutil.copy(str(FULL_DATA_FILE), str(ws / "intraday_pv.h5"))
|
|
|
|
# Write factor code
|
|
(ws / "factor.py").write_text(factor.factor_code, encoding="utf-8")
|
|
|
|
# Execute factor code
|
|
proc = subprocess.run(
|
|
[sys.executable, str(ws / "factor.py")],
|
|
cwd=str(ws),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
|
|
if proc.returncode != 0:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
error_message=f"Execution failed: {proc.stderr[:300]}",
|
|
)
|
|
|
|
# Read result
|
|
result_file = ws / "result.h5"
|
|
if not result_file.exists():
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
error_message="No result.h5 generated",
|
|
)
|
|
|
|
result = pd.read_hdf(str(result_file), key="data")
|
|
total_count = len(result)
|
|
factor_val = result.iloc[:, 0]
|
|
factor_val = _shift_daily_constant_factor_if_needed(factor_val, factor.factor_name)
|
|
non_null_count = factor_val.notna().sum()
|
|
|
|
if non_null_count < 1000:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
non_null_count=non_null_count,
|
|
total_count=total_count,
|
|
error_message=f"Too few valid values: {non_null_count}",
|
|
)
|
|
|
|
# Compute forward returns
|
|
col_close = "$close"
|
|
if col_close not in full_data.columns:
|
|
col_close = next((c for c in full_data.columns if "close" in c.lower()), None)
|
|
if col_close is None:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
error_message=f"No close column found",
|
|
)
|
|
|
|
close = full_data[col_close]
|
|
forward_ret = close.groupby(level="instrument").shift(-forward_return_bars) / close - 1
|
|
|
|
# Compute IC
|
|
valid_idx = factor_val.dropna().index.intersection(forward_ret.dropna().index)
|
|
if len(valid_idx) < 1000:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
non_null_count=non_null_count,
|
|
total_count=total_count,
|
|
error_message=f"Too little overlap: {len(valid_idx)}",
|
|
)
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
ic = factor_val.loc[valid_idx].corr(forward_ret.loc[valid_idx])
|
|
rank_ic = factor_val.loc[valid_idx].corr(forward_ret.loc[valid_idx], method="spearman")
|
|
|
|
# Compute strategy returns from factor signal
|
|
signal = np.where(factor_val.loc[valid_idx] > 0, 1.0, -1.0)
|
|
strategy_ret = signal * forward_ret.loc[valid_idx]
|
|
|
|
bars_per_year = 252 * 1440
|
|
ann_factor = np.sqrt(bars_per_year / forward_return_bars)
|
|
|
|
# Sharpe: annualized mean/vol of strategy returns
|
|
ret_mean = strategy_ret.mean()
|
|
ret_std = strategy_ret.std()
|
|
sharpe = float(ret_mean / ret_std * ann_factor) if ret_std > 0 else 0.0
|
|
|
|
# Annualized return
|
|
annualized_return = float(ret_mean * bars_per_year / forward_return_bars * 100)
|
|
|
|
# Max drawdown on equity curve
|
|
equity = (1.0 + strategy_ret).cumprod()
|
|
running_max = equity.expanding().max()
|
|
drawdown = (equity - running_max) / running_max.replace(0, np.nan)
|
|
max_drawdown = float(drawdown.min()) if len(drawdown) > 0 else 0.0
|
|
|
|
# Win rate: fraction of positive strategy returns
|
|
win_rate = float((strategy_ret > 0).sum()) / len(strategy_ret) if len(strategy_ret) > 0 else 0.0
|
|
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
factor_code=factor.factor_code,
|
|
factor_description=_extract_factor_description(factor.factor_code),
|
|
status="success",
|
|
ic=float(ic) if ic is not None and not np.isnan(ic) else None,
|
|
rank_ic=float(rank_ic) if rank_ic is not None and not np.isnan(rank_ic) else None,
|
|
sharpe=float(sharpe) if sharpe is not None and not np.isnan(sharpe) else None,
|
|
annualized_return=annualized_return,
|
|
max_drawdown=max_drawdown,
|
|
win_rate=win_rate,
|
|
non_null_count=non_null_count,
|
|
total_count=total_count,
|
|
)
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
error_message="Execution timeout (120s)",
|
|
)
|
|
except Exception as e:
|
|
return EvalResult(
|
|
factor_name=factor.factor_name,
|
|
workspace_hash=factor.workspace_hash,
|
|
status="failed",
|
|
error_message=str(e)[:500],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parallel evaluation
|
|
# ---------------------------------------------------------------------------
|
|
def run_evaluation(
|
|
factors: List[FactorInfo],
|
|
full_data: pd.DataFrame,
|
|
n_workers: int = 4,
|
|
) -> List[EvalResult]:
|
|
"""Run factor evaluation in parallel using threads."""
|
|
results = []
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TaskProgressColumn(),
|
|
TimeElapsedColumn(),
|
|
console=console,
|
|
) as progress:
|
|
task = progress.add_task(f"Evaluating {len(factors)} factors with FULL data...", total=len(factors))
|
|
|
|
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
|
futures = {executor.submit(evaluate_factor_full, f, full_data): f for f in factors}
|
|
|
|
for future in as_completed(futures):
|
|
factor = futures[future]
|
|
result = None
|
|
try:
|
|
result = future.result(timeout=300)
|
|
results.append(result)
|
|
except Exception as e:
|
|
# Handle timeout and other exceptions
|
|
fname = str(getattr(factor, 'factor_name', 'unknown'))[:40]
|
|
results.append(EvalResult(
|
|
factor_name=fname,
|
|
workspace_hash=getattr(factor, 'workspace_hash', 'unknown'),
|
|
status="failed",
|
|
error_message=f"Exception: {str(e)[:300]}",
|
|
))
|
|
result = results[-1]
|
|
|
|
n_success = sum(1 for r in results if r.status == "success")
|
|
n_fail = sum(1 for r in results if r.status == "failed")
|
|
|
|
# Save immediately after each factor
|
|
if result is not None:
|
|
save_single_result(result)
|
|
|
|
# Update progress with safe string handling
|
|
fname = str(getattr(factor, 'factor_name', 'unknown'))[:40]
|
|
progress.update(
|
|
task,
|
|
advance=1,
|
|
description=f"Evaluating: {n_success}✅ {n_fail}❌ | {fname}",
|
|
)
|
|
|
|
return results
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Results storage
|
|
# ---------------------------------------------------------------------------
|
|
FACTORS_DIR = RESULTS_DIR / "factors"
|
|
FACTORS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
def save_single_result(r: EvalResult) -> None:
|
|
"""Save a single factor result to results/factors/."""
|
|
if r.status != "success":
|
|
return
|
|
safe_name = r.factor_name.replace("/", "_").replace("\\", "_").replace(" ", "_")[:100]
|
|
json_path = FACTORS_DIR / f"{safe_name}.json"
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump(r.to_dict(), f, indent=2, default=str)
|
|
|
|
def save_results(results: List[EvalResult]) -> None:
|
|
"""Save evaluation results to JSON and SQLite."""
|
|
successful = [r for r in results if r.status == "success"]
|
|
failed = [r for r in results if r.status == "failed"]
|
|
|
|
# Sort by IC
|
|
successful.sort(key=lambda r: abs(r.ic) if r.ic is not None else 0, reverse=True)
|
|
|
|
# Save ALL successful results to results/factors/
|
|
for r in successful:
|
|
# Safe filename (remove special chars)
|
|
safe_name = r.factor_name.replace("/", "_").replace("\\", "_").replace(" ", "_")[:100]
|
|
json_path = FACTORS_DIR / f"{safe_name}.json"
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump(r.to_dict(), f, indent=2, default=str)
|
|
|
|
# Save summary
|
|
valid_ic = [r.ic for r in results if r.ic is not None]
|
|
valid_sharpe = [r.sharpe for r in results if r.sharpe is not None]
|
|
|
|
summary = {
|
|
"generated_at": datetime.now().isoformat(),
|
|
"total_evaluated": len(results),
|
|
"successful": len(successful),
|
|
"failed": len(failed),
|
|
"success_rate": len(successful) / len(results) if results else 0,
|
|
"avg_ic": float(np.mean(valid_ic)) if valid_ic else 0,
|
|
"best_ic": float(max(valid_ic, key=abs, default=0)),
|
|
"avg_sharpe": float(np.mean(valid_sharpe)) if valid_sharpe else 0,
|
|
"best_sharpe": float(max(valid_sharpe, default=0)),
|
|
"top_20_by_ic": [r.to_dict() for r in successful[:20]],
|
|
"all_results": [r.to_dict() for r in results],
|
|
}
|
|
|
|
with open(EVAL_SUMMARY_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(summary, f, indent=2, default=str)
|
|
|
|
# Save to SQLite
|
|
try:
|
|
import sqlite3
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
c = conn.cursor()
|
|
c.execute("""CREATE TABLE IF NOT EXISTS factor_evaluations (
|
|
id INTEGER PRIMARY KEY,
|
|
factor_name TEXT,
|
|
workspace_hash TEXT,
|
|
ic REAL,
|
|
rank_ic REAL,
|
|
sharpe REAL,
|
|
annualized_return REAL,
|
|
max_drawdown REAL,
|
|
win_rate REAL,
|
|
non_null_count INTEGER,
|
|
total_count INTEGER,
|
|
status TEXT,
|
|
timestamp TEXT
|
|
)""")
|
|
|
|
for r in results:
|
|
c.execute("""INSERT INTO factor_evaluations
|
|
(factor_name, workspace_hash, ic, rank_ic, sharpe,
|
|
annualized_return, max_drawdown, win_rate,
|
|
non_null_count, total_count, status, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(r.factor_name, r.workspace_hash, r.ic, r.rank_ic, r.sharpe,
|
|
r.annualized_return, r.max_drawdown, r.win_rate,
|
|
r.non_null_count, r.total_count, r.status, r.timestamp))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
except Exception as e:
|
|
console.print(f"[yellow]SQLite save warning: {e}[/yellow]")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Display
|
|
# ---------------------------------------------------------------------------
|
|
def display_results(results: List[EvalResult]) -> None:
|
|
"""Display evaluation results as a table."""
|
|
successful = [r for r in results if r.status == "success"]
|
|
successful.sort(key=lambda r: abs(r.ic) if r.ic is not None else 0, reverse=True)
|
|
|
|
table = Table(
|
|
title="Factor Evaluation Results (FULL DATA)",
|
|
show_header=True,
|
|
header_style="bold cyan",
|
|
)
|
|
table.add_column("#", justify="center", width=4)
|
|
table.add_column("Factor", width=40)
|
|
table.add_column("IC", justify="right", width=10)
|
|
table.add_column("Rank IC", justify="right", width=10)
|
|
table.add_column("Sharpe", justify="right", width=10)
|
|
table.add_column("Ann. Ret %", justify="right", width=10)
|
|
table.add_column("Max DD", justify="right", width=10)
|
|
table.add_column("Win Rate", justify="right", width=10)
|
|
|
|
for i, r in enumerate(successful[:20], 1):
|
|
table.add_row(
|
|
str(i),
|
|
r.factor_name[:38],
|
|
f"{r.ic:.6f}" if r.ic is not None else "N/A",
|
|
f"{r.rank_ic:.6f}" if r.rank_ic is not None else "N/A",
|
|
f"{r.sharpe:.4f}" if r.sharpe is not None else "N/A",
|
|
f"{r.annualized_return:.4f}" if r.annualized_return is not None else "N/A",
|
|
f"{r.max_drawdown:.4f}" if r.max_drawdown is not None else "N/A",
|
|
f"{r.win_rate:.2%}" if r.win_rate is not None else "N/A",
|
|
)
|
|
|
|
console.print()
|
|
console.print(table)
|
|
|
|
# Summary
|
|
valid_ic = [r.ic for r in results if r.ic is not None]
|
|
valid_sharpe = [r.sharpe for r in results if r.sharpe is not None]
|
|
|
|
console.print(Panel(
|
|
f"[bold]Evaluation Summary (FULL DATA)[/bold]\n"
|
|
f"Total evaluated: {len(results)}\n"
|
|
f"Successful: {len(successful)} ✅\n"
|
|
f"Failed: {len(results) - len(successful)} ❌\n"
|
|
f"Avg IC: {np.mean(valid_ic):.6f} (n={len(valid_ic)})\n"
|
|
f"Best IC: {max(valid_ic, key=abs, default=0):.6f}\n"
|
|
f"Avg Sharpe: {np.mean(valid_sharpe):.4f} (n={len(valid_sharpe)})\n"
|
|
f"Best Sharpe: {max(valid_sharpe, default=0):.4f}\n"
|
|
f"Saved to: {EVAL_SUMMARY_PATH}\n"
|
|
f"Database: {DB_PATH}",
|
|
border_style="green",
|
|
))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
def main(
|
|
top: int = 100,
|
|
all_factors: bool = False,
|
|
parallel: int = 4,
|
|
force: bool = False,
|
|
) -> None:
|
|
"""Main entry point."""
|
|
console.print(Panel(
|
|
"[bold cyan]NexQuant Full Data Factor Evaluator[/bold cyan]\n"
|
|
f"Using FULL 1min data: {FULL_DATA_FILE}",
|
|
border_style="cyan",
|
|
))
|
|
|
|
# Load full data
|
|
if not FULL_DATA_FILE.exists():
|
|
console.print(f"[red]Full data file not found: {FULL_DATA_FILE}[/red]")
|
|
return
|
|
|
|
console.print(f"\n[dim]Loading full data...[/dim]")
|
|
full_data = pd.read_hdf(str(FULL_DATA_FILE), key="data")
|
|
console.print(f"[bold green]✓ Loaded {len(full_data):,} rows ({full_data.index.get_level_values('datetime').min()} to {full_data.index.get_level_values('datetime').max()})[/bold green]")
|
|
|
|
# Scan factors (skip already evaluated by default)
|
|
console.print(f"\n[dim]Scanning workspaces...[/dim]")
|
|
factors = scan_factors(WORKSPACE_DIR, skip_evaluated=not force)
|
|
console.print(f"[bold]Total unique factors found: {len(factors)}[/bold]")
|
|
if force:
|
|
console.print("[yellow]⚠️ Force mode: Re-evaluating ALL factors[/yellow]")
|
|
else:
|
|
console.print("[dim]Skipping already evaluated factors[/dim]")
|
|
|
|
if not factors:
|
|
console.print("[red]No factors found![/red]")
|
|
return
|
|
|
|
# Select factors to evaluate
|
|
if all_factors:
|
|
to_evaluate = factors
|
|
else:
|
|
to_evaluate = factors[:top]
|
|
|
|
console.print(f"\n[bold green]Selected {len(to_evaluate)} factors for evaluation[/bold green]")
|
|
console.print(f" Using {parallel} parallel workers")
|
|
|
|
# Run evaluation
|
|
results = run_evaluation(to_evaluate, full_data, n_workers=parallel)
|
|
|
|
# Save results
|
|
console.print(f"\n[bold cyan]Saving results...[/bold cyan]")
|
|
save_results(results)
|
|
|
|
# Display
|
|
display_results(results)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="NexQuant Full Data Factor Evaluator"
|
|
)
|
|
parser.add_argument(
|
|
"--top", "-n",
|
|
type=int,
|
|
default=100,
|
|
help="Number of factors to evaluate (default: 100)",
|
|
)
|
|
parser.add_argument(
|
|
"--all", "-a",
|
|
action="store_true",
|
|
help="Evaluate all discovered factors",
|
|
)
|
|
parser.add_argument(
|
|
"--parallel", "-p",
|
|
type=int,
|
|
default=4,
|
|
help="Number of parallel workers (default: 4)",
|
|
)
|
|
parser.add_argument(
|
|
"--force", "-f",
|
|
action="store_true",
|
|
help="Force re-evaluation of ALL factors (even already evaluated)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
main(
|
|
top=args.top,
|
|
all_factors=args.all,
|
|
parallel=args.parallel,
|
|
force=args.force,
|
|
)
|