PaperTrade / research /backtest_top5.py
Khanna, Videh Rakesh Rakesh
feat: cost-aware predictions, graded validation, per-TF AI, drop 5D
9da1a46
Raw
History Blame Contribute Delete
7.39 kB
#!/usr/bin/env python3
"""
backtest_top5.py β€” Simulate top5 pick selection on historical backtest data.
Applies the _score_5d / _score_1w scoring logic from top5_picker.py to each
(date Γ— timeframe) group in existing CSV datasets, selects the top-N picks,
and measures:
1. target_hit_for_tf β€” same intraday-touch metric as backtest.py
2. direction accuracy β€” predicted direction matches actual price movement
3. avg actual return β€” mean ret_for_tf for longs, -ret_for_tf for shorts
Usage:
python research/backtest_top5.py
"""
import os, sys
import pandas as pd
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
RESEARCH_DIR = os.path.dirname(os.path.abspath(__file__))
CONF_MULT = {"HIGH": 1.0, "MEDIUM": 0.80, "LOW": 0.55}
BEARISH_DIRS = {"BEARISH", "SLIGHTLY BEARISH"}
BULLISH_DIRS = {"BULLISH", "SLIGHTLY BULLISH"}
ACCEPTED = BULLISH_DIRS | BEARISH_DIRS
def _score(row: pd.Series) -> float:
"""Replicate top5_picker._score_5d using CSV columns."""
direction = str(row.get("direction", "NEUTRAL"))
if direction not in ACCEPTED:
return -1.0 # excluded directions sort to bottom
is_bearish = direction in BEARISH_DIRS
# Compute ret_hi / ret_lo from target prices (calibrated range values)
try:
ret_hi = ((float(row["target_price_hi"]) / float(row["entry_price"])) - 1) * 100
ret_lo = ((float(row["target_price_lo"]) / float(row["entry_price"])) - 1) * 100
except (KeyError, TypeError, ZeroDivisionError):
return -1.0
base_ret = abs(ret_lo) if is_bearish else ret_hi
if base_ret <= 0:
return -1.0
conf_mult = CONF_MULT.get(str(row.get("confidence", "LOW")), 0.55)
ml_prob = float(row.get("ml_prob", 0.5) or 0.5)
if is_bearish:
ml_factor = 1.0 + (0.5 - ml_prob) * 0.30
else:
ml_factor = 1.0 + (ml_prob - 0.5) * 0.30
return base_ret * conf_mult * ml_factor
def _direction_correct(row: pd.Series) -> int:
"""1 if AI direction matches actual price movement over the timeframe."""
direction = str(row.get("direction", "NEUTRAL"))
actual_ret = float(row.get("ret_for_tf", 0) or 0)
if direction in BULLISH_DIRS:
return 1 if actual_ret > 0 else 0
if direction in BEARISH_DIRS:
return 1 if actual_ret < 0 else 0
# NEUTRAL: hit if absolute move is within Β±1%
return 1 if abs(actual_ret) <= 1.0 else 0
def _profit_if_traded(row: pd.Series) -> float:
"""Simulated NET P&L (%) treating each pick as a long or short β€” after NSE
round-trip transaction costs. Price prediction β‰  profitable trading: a move
that doesn't clear fees is not an edge (see Stock-Prediction-Models doc)."""
direction = str(row.get("direction", "NEUTRAL"))
actual_ret = float(row.get("ret_for_tf", 0) or 0)
gross = -actual_ret if direction in BEARISH_DIRS else actual_ret
try:
from costs import cost_pct_for_timeframe
gross -= cost_pct_for_timeframe(str(row.get("timeframe", "1D")))
except Exception:
pass
return gross
def simulate(df: pd.DataFrame, top_n: int = 5, label: str = "") -> None:
df = df.copy()
df["score"] = df.apply(_score, axis=1)
df["direction_correct"] = df.apply(_direction_correct, axis=1)
df["simulated_pnl"] = df.apply(_profit_if_traded, axis=1)
selected_rows, excluded_rows = [], []
for (date, tf), grp in df.groupby(["date", "timeframe"]):
# Only eligible = BULLISH/BEARISH directions (score > 0).
# Mirrors top5_picker: NEUTRAL is never added to candidates_all.
eligible = grp[grp["score"] > 0].sort_values("score", ascending=False)
ineligible = grp[grp["score"] <= 0] # NEUTRAL / no-direction β†’ always excluded
top = eligible.head(min(top_n, len(eligible)))
bot = pd.concat([eligible.tail(max(0, len(eligible) - top_n)), ineligible])
if not top.empty:
selected_rows.append(top.assign(_sel="selected"))
if not bot.empty:
excluded_rows.append(bot.assign(_sel="excluded"))
sel = pd.concat(selected_rows, ignore_index=True) if selected_rows else pd.DataFrame()
exc = pd.concat(excluded_rows, ignore_index=True) if excluded_rows else pd.DataFrame()
all_ = pd.concat([sel, exc], ignore_index=True)
print(f"\n{'='*60}")
print(f" {label}")
print(f" Stocks/date: {df.groupby(['date','timeframe'])['ticker'].count().mean():.0f} "
f" Dates: {df['date'].nunique()} "
f" TFs: {df['timeframe'].nunique()}")
print(f"{'='*60}")
print(f" {'Group':12s} {'N':>5} {'TargetHit':>9} {'DirAcc':>7} {'AvgP&L':>7}")
print(f" {'-'*50}")
for grp_name, grp_df in [("selected", sel), ("excluded", exc), ("all", all_)]:
if grp_df.empty:
print(f" {grp_name:12s} {'β€”':>5}")
continue
n = len(grp_df)
hit = grp_df["target_hit_for_tf"].mean()
dir_acc = grp_df["direction_correct"].mean()
avg_pnl = grp_df["simulated_pnl"].mean()
print(f" {grp_name:12s} {n:5d} {hit:9.1%} {dir_acc:7.1%} {avg_pnl:+6.2f}%")
# Break selected down by direction
if not sel.empty:
print("\n Selected β€” by direction:")
for dname, dgrp in sel.groupby("direction"):
n = len(dgrp)
hit = dgrp["target_hit_for_tf"].mean()
dir_acc = dgrp["direction_correct"].mean()
avg_pnl = dgrp["simulated_pnl"].mean()
print(f" {dname:18s} n={n:3d} hit={hit:.1%} dir={dir_acc:.1%} pnl={avg_pnl:+.2f}%")
# Break selected down by timeframe
if not sel.empty and sel["timeframe"].nunique() > 1:
print("\n Selected β€” by timeframe:")
for tf, tfgrp in sel.groupby("timeframe"):
n = len(tfgrp)
hit = tfgrp["target_hit_for_tf"].mean()
dir_acc = tfgrp["direction_correct"].mean()
avg_pnl = tfgrp["simulated_pnl"].mean()
print(f" {tf:6s} n={n:3d} hit={hit:.1%} dir={dir_acc:.1%} pnl={avg_pnl:+.2f}%")
# ── Load datasets ──────────────────────────────────────────────────────────────
DATASETS = [
("ai_prompt_accuracy_trades.csv", "Trades CSV (6 stocks, 3 dates, 1D/3D/5D)", 5),
("ai_prompt_accuracy_sweep.csv", "Sweep CSV (6 stocks, ~10 dates, 1D/3D/5D)", 5),
("ai_prompt_accuracy_3d.csv", "3D CSV (14 stocks, 11 dates, 3D only)", 5),
("ai_prompt_accuracy_iter64.csv", "iter64 CSV (6 stocks, 46 dates, 1D/3D/5D)", 5),
]
print("Top5 Picker Backtest β€” Selection Quality Analysis")
print("=" * 60)
print("Metric definitions:")
print(" TargetHit = intraday calibrated-range touch (same as backtest.py)")
print(" DirAcc = predicted direction matches actual price move")
print(" AvgP&L = mean simulated return (long for BULLISH, short for BEARISH)")
for fname, desc, top_n in DATASETS:
path = os.path.join(RESEARCH_DIR, fname)
if not os.path.exists(path):
print(f"\n [skip] {fname} not found")
continue
try:
df = pd.read_csv(path)
simulate(df, top_n=top_n, label=desc)
except Exception as e:
print(f"\n [error] {fname}: {e}")