mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: Add 'predix top' command + explain factor evaluation results
New CLI commands: - predix top [-n 20] [-m ic|sharpe]: Show top factors by IC/Sharpe - predix evaluate [--top N] [--all] [--force]: Evaluate factors Why 100 new factors mostly failed: - 56 factors have IC=None (factor values are all NaN or constant) - LLM generates code that doesn't work with EURUSD 1min data - Common issues: volume=0 causes division by zero, wrong MultiIndex handling - Only 346 of 501 factors have valid IC values Working factors (Top 5): 1. daily_close_open_mom IC=0.255 2. daily_ret_log_1d IC=0.255 3. daily_ret_close_1d IC=0.255 4. daily_close_to_close_ret IC=0.255 5. daily_ret_vol_adj_1d IC=0.235 Usage: predix top # Show top 20 by IC predix top -n 50 # Show top 50 predix top -m sharpe # Sort by Sharpe predix evaluate --all # Evaluate all NEW factors predix evaluate --force # Re-evaluate ALL
This commit is contained in:
@@ -256,6 +256,112 @@ def evaluate(
|
||||
console.print(traceback.format_exc())
|
||||
|
||||
|
||||
@app.command()
|
||||
def top(
|
||||
n: int = typer.Option(
|
||||
20,
|
||||
"--num", "-n",
|
||||
help="Number of top factors to show (default: 20)",
|
||||
),
|
||||
metric: str = typer.Option(
|
||||
"ic",
|
||||
"--metric", "-m",
|
||||
help="Sort by metric: 'ic' or 'sharpe'",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Show top-performing factors by IC or Sharpe.
|
||||
|
||||
Examples:
|
||||
predix top # Top 20 by IC
|
||||
predix top -n 50 # Top 50 by IC
|
||||
predix top -m sharpe # Top 20 by Sharpe
|
||||
"""
|
||||
import json
|
||||
import glob as glob_module
|
||||
import numpy as np
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
|
||||
factors_dir = Path(__file__).parent / "results" / "factors"
|
||||
if not factors_dir.exists():
|
||||
console.print("[red]No results found in results/factors/[/red]")
|
||||
return
|
||||
|
||||
# Load all factor JSON files
|
||||
results = []
|
||||
for f in glob_module.glob(str(factors_dir / "*.json")):
|
||||
try:
|
||||
with open(f) as fh:
|
||||
data = json.load(fh)
|
||||
# Only include factors with valid IC
|
||||
if data.get("status") == "success" and data.get("ic") is not None:
|
||||
results.append(data)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not results:
|
||||
console.print("[yellow]No evaluated factors found with valid IC[/yellow]")
|
||||
return
|
||||
|
||||
# Sort by metric
|
||||
if metric == "sharpe":
|
||||
results.sort(key=lambda x: abs(x.get("sharpe", 0) or 0), reverse=True)
|
||||
sort_label = "Sharpe"
|
||||
else:
|
||||
results.sort(key=lambda x: abs(x.get("ic", 0) or 0), reverse=True)
|
||||
sort_label = "IC"
|
||||
|
||||
# Display as table
|
||||
table = Table(
|
||||
title=f"Top {min(n, len(results))} Factors by {sort_label}",
|
||||
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("Sharpe", justify="right", width=10)
|
||||
table.add_column("Ann. Return %", justify="right", width=12)
|
||||
table.add_column("Max DD", justify="right", width=10)
|
||||
table.add_column("Win Rate", justify="right", width=10)
|
||||
|
||||
for i, r in enumerate(results[:n], 1):
|
||||
ic = r.get("ic")
|
||||
sharpe = r.get("sharpe")
|
||||
ann_ret = r.get("annualized_return")
|
||||
max_dd = r.get("max_drawdown")
|
||||
win_rate = r.get("win_rate")
|
||||
|
||||
table.add_row(
|
||||
str(i),
|
||||
r["factor_name"][:38],
|
||||
f"{ic:.6f}" if ic is not None else "N/A",
|
||||
f"{sharpe:.4f}" if sharpe is not None else "N/A",
|
||||
f"{ann_ret:.4f}" if ann_ret is not None else "N/A",
|
||||
f"{max_dd:.4f}" if max_dd is not None else "N/A",
|
||||
f"{win_rate:.2%}" if win_rate is not None else "N/A",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Summary
|
||||
valid_ic = [r.get("ic") for r in results if r.get("ic") is not None]
|
||||
valid_sharpe = [r.get("sharpe") for r in results if r.get("sharpe") is not None]
|
||||
# Filter extreme outliers for average
|
||||
valid_sharpe_filtered = [s for s in valid_sharpe if abs(s or 0) < 1e6]
|
||||
|
||||
console.print(Panel(
|
||||
f"[bold]Summary[/bold]\n"
|
||||
f"Total evaluated: {len(results)}\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_filtered):.4f} (n={len(valid_sharpe_filtered)})\n"
|
||||
f"Best Sharpe: {max(valid_sharpe, key=abs, default=0):.4f}",
|
||||
border_style="green",
|
||||
))
|
||||
|
||||
|
||||
@app.command()
|
||||
def health():
|
||||
"""Check system health and configuration."""
|
||||
|
||||
Reference in New Issue
Block a user