PaperTrade / research /tight_range_backtest.py
Khanna, Videh Rakesh Rakesh
Tight directional bands for INTRADAY + 1D
d1c2739
Raw
History Blame Contribute Delete
6.64 kB
#!/usr/bin/env python3
"""
research/tight_range_backtest.py β€” Deterministic reachability backtest for the TIGHT INTRADAY/1D
directional bands (user request 2026-07-30: "ranges should be short like 1.0–1.25 so the mean is
clear β€” check, fix, backtest").
The tight band is a deterministic function of (direction, ATR%) applied AFTER the LLM/ML direction
call (predictor_core / ai_forecast._TIGHT_BAND). Direction accuracy is unchanged by the band shape,
so we isolate the *range* quality here β€” no LLM calls, fast β€” by measuring, over real NSE OHLCV, how
often the tight band's targets are actually reached when the direction is right:
mean_hit = P(favorable move >= band midpoint) ← the actionable "did the mean get hit"
near_hit = P(favorable move >= band near bound) ← band entered at all
For 1D we also condition on the realized close direction (r1) to reflect the directional call.
It also prints the OLD band's metric for context (INTRADAY old = wide floored band; 1D old = the flat
Β±1% range-only containment) so the accuracy trade-off of tightening is explicit.
Run: python research/tight_range_backtest.py
"""
from __future__ import annotations
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.dirname(__file__))
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
from backtest import fetch_data, _compute_indicators, _fwd_intraday_moves, _fwd_returns
import predictor_core as pc
DATA_START = "2024-06-01"
DATA_END = "2026-07-17"
TEST_START = "2025-01-01"
STEP = 5 # every 5 trading days
# A diversified fixed set across sectors/cap tiers (+ current watchlist).
BASE_TICKERS = [
"RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS", "SBIN.NS",
"TATASTEEL.NS", "HINDALCO.NS", "AXISBANK.NS", "MARUTI.NS", "TATAMOTORS.NS",
"SUNPHARMA.NS", "ITC.NS", "LT.NS", "DLF.NS", "ADANIENT.NS", "BAJFINANCE.NS",
"WIPRO.NS", "HCLTECH.NS", "ONGC.NS", "COALINDIA.NS", "JSWSTEEL.NS",
"POWERGRID.NS", "NTPC.NS", "GRASIM.NS", "HINDZINC.NS", "VEDL.NS",
"ASHOKLEY.NS", "IDEA.NS", "YESBANK.NS", "PNB.NS", "IRFC.NS",
]
def _watchlist():
try:
import database as db
return [w["ticker"] for w in db.get_watchlist()]
except Exception:
return []
def _old_intraday_band(atr_pct: float):
"""Reconstruct the PRE-tight INTRADAY band (both bounds floored to >=1%, far volatility-scaled).
near ~ 0.148*ATR% floored to 1; far ~ 0.47*ATR% floored to >=1 & capped 2, spread>=0.15."""
near = 0.12 * (1.31 ** 0.83) * atr_pct # ~0.148*ATR%
far = 0.156 * (5.70 ** 0.6354) * atr_pct # ~0.47*ATR%
far = min(2.0, far)
if far - near < 0.15:
far = near + 0.15
near = max(near, 1.0)
far = max(far, near + 0.15)
far = min(2.0, far)
return near, far
def main():
tickers = sorted(set(BASE_TICKERS + _watchlist()))
print(f"Fetching {len(tickers)} tickers {DATA_START}β†’{DATA_END} …")
sc, sh, sl, sv, nc, vc = fetch_data(tickers, DATA_START, DATA_END)
test_start = pd.Timestamp(TEST_START)
rows = []
for ticker in tickers:
if ticker not in sc.columns:
continue
c = sc[ticker].dropna()
dates = c.index[c.index >= test_start][::STEP]
for date in dates:
try:
price = float(c.loc[:date].iloc[-1])
inds = _compute_indicators(sc[ticker], sh[ticker], sl[ticker], sv[ticker], date)
atr14 = inds.get("atr14")
if not price or not atr14:
continue
atr_pct = atr14 / price * 100.0
up0, dn0, up1, dn1, up3, dn3, up5, dn5 = _fwd_intraday_moves(sc, sh, sl, date, ticker)
r1, r3, r5 = _fwd_returns(sc, date, ticker)
rows.append(dict(ticker=ticker, date=date, atr_pct=atr_pct,
up0=up0, dn0=dn0, up1=up1, dn1=dn1, r1=r1))
except Exception:
continue
df = pd.DataFrame(rows).replace([np.inf, -np.inf], np.nan)
print(f"Rows: {len(df)}\n")
# ── INTRADAY ────────────────────────────────────────────────────────────
d = df.dropna(subset=["up0", "atr_pct"])
near = d["atr_pct"].apply(lambda a: pc._tight_band_mag("INTRADAY", a)[0])
far = d["atr_pct"].apply(lambda a: pc._tight_band_mag("INTRADAY", a)[1])
mid = (near + far) / 2.0
old = d["atr_pct"].apply(lambda a: _old_intraday_band(a))
old_mid = old.apply(lambda t: (t[0] + t[1]) / 2.0)
print("=" * 68)
print("INTRADAY (favorable move = same-day High vs close, up0)")
print("=" * 68)
print(f" avg band width : {(far-near).mean():.3f}% avg mid: {mid.mean():.3f}% "
f"(OLD avg mid {old_mid.mean():.3f}%, width {(old.apply(lambda t:t[1]-t[0])).mean():.3f}%)")
print(f" NEW mean_hit : {(d['up0'] >= mid).mean()*100:5.1f}% "
f"near_hit {(d['up0'] >= near).mean()*100:5.1f}% far_hit {(d['up0'] >= far).mean()*100:5.1f}%")
print(f" OLD mean_hit : {(d['up0'] >= old_mid).mean()*100:5.1f}% "
f"(same up0, wider band β†’ higher raw hit but fuzzier mean)")
# ── 1D ─────────────────────────────────────────────────────────────────
d = df.dropna(subset=["up1", "r1", "atr_pct"])
near = d["atr_pct"].apply(lambda a: pc._tight_band_mag("1D", a)[0])
far = d["atr_pct"].apply(lambda a: pc._tight_band_mag("1D", a)[1])
mid = (near + far) / 2.0
up_day = d["r1"] > 0
print("\n" + "=" * 68)
print("1D (directional: favorable move = next-day High vs close, up1)")
print("=" * 68)
print(f" avg band width : {(far-near).mean():.3f}% avg mid: {mid.mean():.3f}%")
# Directional target_hit: called BULLISH, closed up, and intraday high reached the mean.
tgt = up_day & (d["up1"] >= mid)
print(f" NEW dir target_hit (closed up & high>=mid): {tgt.mean()*100:5.1f}%")
print(f" NEW mean_hit | up-day : {(d.loc[up_day,'up1'] >= mid.loc[up_day]).mean()*100:5.1f}%")
print(f" P(closed up) : {up_day.mean()*100:5.1f}%")
# OLD 1D = flat +/-1% range-only containment claim.
print(f" OLD 1D range-only hit P(|r1| <= 1%) : {(d['r1'].abs() <= 1.0).mean()*100:5.1f}%")
if __name__ == "__main__":
main()