Spaces:
Running
Running
Khanna, Videh Rakesh Rakesh
AI range: containment mode + Target-below-range fix + ML un-floor
dd0818f | #!/usr/bin/env python3 | |
| """ | |
| research/blend_backtest.py — Does the LLM's OWN (tighter) range hit as well as the ATR band, | |
| especially when the LLM is HIGH-confidence? Tests the user's "blend: use AI when confident, | |
| else ATR band" idea with real LLM calls. | |
| For each (ticker, date, tf) it runs the production fast-mode forecast and compares, against the | |
| realized forward move: | |
| ATR band — fc.target_price_lo/hi (what production shows) | |
| AI raw — the LLM's own range (fc.raw_llm_return_lo/hi, before the ATR clamp) | |
| Grades target_hit (favorable move reached the range) + graded_hit (range touched), split by | |
| confidence (HIGH vs MEDIUM/LOW) so we can see if an AI-when-HIGH blend keeps the hit rate. | |
| Run: python research/blend_backtest.py | |
| """ | |
| from __future__ import annotations | |
| import sys, os, time | |
| 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, pandas as pd | |
| from backtest import (fetch_data, _compute_indicators, _fwd_intraday_moves, _fwd_returns, | |
| _vix_nifty_series, _simple_ml_prob) | |
| from ai_forecast import get_ai_forecast | |
| from experiment_features import ExperimentContextBuilder, ExperimentalConfig | |
| _exp = ExperimentContextBuilder(ExperimentalConfig(enable_alt_sentiment=False, enable_fundamentals=False)) | |
| # Big array so we sweep many regimes/volatilities; env-overridable. | |
| TICKERS = (os.getenv("BLEND_TICKERS") or | |
| "RELIANCE.NS,SBIN.NS,TATASTEEL.NS,DLF.NS,HINDALCO.NS,BEL.NS,AXISBANK.NS,MARUTI.NS," | |
| "VEDL.NS,INFY.NS,TCS.NS,ICICIBANK.NS,ADANIENT.NS,JSWSTEEL.NS,SUNPHARMA.NS,LT.NS," | |
| "ONGC.NS,COALINDIA.NS,PNB.NS,ASHOKLEY.NS,BANKBARODA.NS,TATAPOWER.NS,IRFC.NS,NMDC.NS" | |
| ).split(",") | |
| DATA_START, DATA_END = "2024-06-01", "2026-07-17" | |
| TEST_START = os.getenv("BLEND_TEST_START", "2026-01-01") | |
| STEP = int(os.getenv("BLEND_STEP", "8")) | |
| TFS = ["INTRADAY", "1D"] | |
| PACE = float(os.getenv("BLEND_PACE_SECS", "1")) | |
| MAX_FORECASTS = int(os.getenv("BLEND_MAX", "400")) | |
| RESULTS_CSV = os.path.join(os.path.dirname(__file__), "blend_backtest_results.csv") | |
| def _hit(direction, price, lo, hi, max_up, min_down, tf, ret_tf): | |
| """Return (midpoint_hit, range_entered) — did the favorable move reach the range midpoint / | |
| near bound. Same grading applied to both the ATR band and the AI raw range for a fair compare.""" | |
| if not lo or not hi or price <= 0: | |
| return 0, 0 | |
| lo_pct = (min(lo, hi) / price - 1) * 100 | |
| hi_pct = (max(lo, hi) / price - 1) * 100 | |
| mid_pct = ((lo + hi) / 2 / price - 1) * 100 | |
| if direction == "BULLISH": | |
| near = lo_pct # conservative (low) target for a long | |
| return int(max_up >= mid_pct), int(max_up >= near) | |
| else: # BEARISH — favorable move is downward (min_down, negative) | |
| near = hi_pct # shallow (high) target for a short | |
| return int(min_down <= mid_pct), int(min_down <= near) | |
| def main(): | |
| print(f"Fetching {len(TICKERS)} tickers …") | |
| sc, sh, sl, sv, nc, vc = fetch_data(TICKERS, DATA_START, DATA_END) | |
| nifty_ema200, vix_slope = _vix_nifty_series(nc, vc) | |
| ts = pd.Timestamp(TEST_START) | |
| rows = [] | |
| n = 0 | |
| for t in TICKERS: | |
| if t not in sc.columns: | |
| continue | |
| c = sc[t].dropna() | |
| for date in c.index[c.index >= ts][::STEP]: | |
| try: | |
| price = float(c.loc[:date].iloc[-1]) | |
| inds = _compute_indicators(sc[t], sh[t], sl[t], sv[t], date) | |
| if not price or not inds.get("atr14"): | |
| continue | |
| vix = float(vc.loc[:date].dropna().iloc[-1]); nif = float(nc.loc[:date].dropna().iloc[-1]) | |
| nema = float(nifty_ema200.loc[:date].dropna().iloc[-1]); nok = nif > nema | |
| vdec = float(vix_slope.loc[:date].dropna().iloc[-1]) < 0 | |
| r1, r3, r5 = _fwd_returns(sc, date, t) | |
| up0, dn0, up1, dn1, *_ = _fwd_intraday_moves(sc, sh, sl, date, t) | |
| mlp = _simple_ml_prob(sc[t], sv[t], date) | |
| i2 = c.index.searchsorted(date, side="right") | |
| ohlcv = pd.DataFrame({"High": sh[t].iloc[max(0,i2-20):i2].values, | |
| "Low": sl[t].iloc[max(0,i2-20):i2].values, | |
| "Close": sc[t].iloc[max(0,i2-20):i2].values, | |
| "Volume": sv[t].iloc[max(0,i2-20):i2].values}).dropna() | |
| except Exception: | |
| continue | |
| for tf in TFS: | |
| time.sleep(PACE) | |
| try: | |
| fc = get_ai_forecast( | |
| ticker=t, company=t.replace(".NS",""), tf_label=tf, | |
| ml={"probability": mlp, "upgraded": mlp>0.62, "score": int(mlp*100), "features": {}}, | |
| nifty_ok=nok, macro_ok=(nok and vix<20), vix_level=vix, | |
| news=_exp.build_news_bundle(t, t.replace(".NS","")), | |
| current_price=price, indicators=inds, ohlcv_df=ohlcv, vix_declining=vdec, | |
| _fast_mode=True, _tight_test_ranges=False, | |
| _forecast_date=str(date.date()), _fast_fail_on_rate_limit=True, | |
| _enable_backtest_openrouter=True, | |
| ) | |
| except Exception: | |
| continue | |
| d = fc.get("direction", "NEUTRAL") | |
| if d not in ("BULLISH", "BEARISH"): | |
| continue | |
| ret_tf = 0.0 if tf == "INTRADAY" else r1 | |
| mx = up0 if tf == "INTRADAY" else up1 | |
| mn = dn0 if tf == "INTRADAY" else dn1 | |
| # ATR band (production) | |
| a_lo, a_hi = fc.get("target_price_lo",0.0), fc.get("target_price_hi",0.0) | |
| at_th, at_gr = _hit(d, price, a_lo, a_hi, mx, mn, tf, ret_tf) | |
| # AI raw range | |
| rlo, rhi = fc.get("raw_llm_return_lo"), fc.get("raw_llm_return_hi") | |
| if rlo is None or rhi is None: | |
| continue | |
| ai_lo = round(price*(1+rlo/100),2); ai_hi = round(price*(1+rhi/100),2) | |
| ai_th, ai_gr = _hit(d, price, ai_lo, ai_hi, mx, mn, tf, ret_tf) | |
| rows.append(dict(date=str(date.date()), ticker=t, tf=tf, conf=fc.get("confidence","LOW"), dirn=d, | |
| src=fc.get("source",""), | |
| atr_w=round(abs(fc.get("predicted_return_hi",0)-fc.get("predicted_return_lo",0)),3), | |
| ai_w=round(abs(rhi-rlo),3), | |
| atr_th=at_th, atr_gr=at_gr, ai_th=ai_th, ai_gr=ai_gr)) | |
| n += 1 | |
| # Incremental CSV so a long/rate-limited run keeps whatever it captured. | |
| pd.DataFrame(rows).to_csv(RESULTS_CSV, index=False) | |
| if n % 10 == 0: | |
| _c = pd.DataFrame(rows).conf.value_counts().to_dict() | |
| print(f" … {n} forecasts (conf {_c})", flush=True) | |
| if n >= MAX_FORECASTS: | |
| print(f" reached MAX_FORECASTS={MAX_FORECASTS}", flush=True) | |
| break | |
| else: | |
| continue | |
| break | |
| else: | |
| continue | |
| break | |
| df = pd.DataFrame(rows) | |
| if df.empty: | |
| print("No directional forecasts captured (LLM unavailable?).") | |
| return | |
| print(f"\nDirectional forecasts: {len(df)} | confidence mix: {df.conf.value_counts().to_dict()}\n") | |
| def block(sub, label): | |
| if len(sub) < 3: | |
| print(f"{label:<26} n={len(sub):3d} (too few)"); return | |
| print(f"{label:<26} n={len(sub):3d} " | |
| f"ATR band: target_hit={sub.atr_th.mean()*100:3.0f}% graded={sub.atr_gr.mean()*100:3.0f}% w={sub.atr_w.mean():.2f}% || " | |
| f"AI raw: target_hit={sub.ai_th.mean()*100:3.0f}% graded={sub.ai_gr.mean()*100:3.0f}% w={sub.ai_w.mean():.2f}%") | |
| print("=" * 110) | |
| block(df, "ALL") | |
| for tf in TFS: | |
| block(df[df.tf==tf], f" {tf}") | |
| print("-" * 110) | |
| block(df[df.conf=="HIGH"], "HIGH confidence only") | |
| block(df[df.conf!="HIGH"], "MEDIUM/LOW confidence") | |
| print("=" * 110) | |
| print("\nBlend simulation (AI raw when HIGH-conf, ATR band otherwise):") | |
| blend_th = np.where(df.conf=="HIGH", df.ai_th, df.atr_th).mean()*100 | |
| blend_gr = np.where(df.conf=="HIGH", df.ai_gr, df.atr_gr).mean()*100 | |
| blend_w = np.where(df.conf=="HIGH", df.ai_w, df.atr_w ).mean() | |
| print(f" BLEND target_hit={blend_th:.0f}% graded={blend_gr:.0f}% avg width={blend_w:.2f}%") | |
| print(f" ALL-ATR target_hit={df.atr_th.mean()*100:.0f}% graded={df.atr_gr.mean()*100:.0f}% avg width={df.atr_w.mean():.2f}%") | |
| print(f" ALL-AI-raw target_hit={df.ai_th.mean()*100:.0f}% graded={df.ai_gr.mean()*100:.0f}% avg width={df.ai_w.mean():.2f}%") | |
| if __name__ == "__main__": | |
| main() | |