Spaces:
Running
Running
| #!/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() | |