Khanna, Videh Rakesh Rakesh Claude Sonnet 4.6 commited on
Commit
eb227a8
·
1 Parent(s): b306f4b

fix: holiday-aware prediction validation — skip NSE holidays in target dates

Browse files

- database._trading_deadline + save_prediction_snapshot: replace weekend-only
`while weekday >= 5` loop with next_trading_day() from market_calendar, which
handles both weekends and all NSE holidays
- database._migrate: add Python post-processing loop after SQL weekend patches
to fix any existing PENDING rows whose target_date landed on a weekday holiday
- app.validation_execute: guard at top — return {skipped, reason, next_trading_day}
when today is a holiday so validation never runs on non-trading days
- app.py: add next_trading_day to market_calendar import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +14 -1
  2. database.py +60 -8
app.py CHANGED
@@ -27,7 +27,11 @@ from universe import get_universe, refresh_universe # get_universe used by sear
27
  from data_sources import fetch_ohlcv, fetch_live_price
28
  import database as db
29
  from top5_picker import get_top5_picks
30
- from market_calendar import market_status as nse_market_status, is_trading_day as nse_is_trading_day
 
 
 
 
31
 
32
  app = Flask(__name__)
33
  app.config["JSON_SORT_KEYS"] = False
@@ -2275,6 +2279,15 @@ def _intraday_target_hit(direction: str, window_high, window_low, close_price,
2275
  @app.route("/api/validation/execute", methods=["POST"])
2276
  def validation_execute():
2277
  """Execute validation for pending predictions — parallel OHLCV fetches to avoid timeouts."""
 
 
 
 
 
 
 
 
 
2278
  pending = db.get_validation_pending(limit=500, due_only=True)
2279
 
2280
  # Pre-filter: handle NO TRADE / bad entry immediately (no I/O needed)
 
27
  from data_sources import fetch_ohlcv, fetch_live_price
28
  import database as db
29
  from top5_picker import get_top5_picks
30
+ from market_calendar import (
31
+ market_status as nse_market_status,
32
+ is_trading_day as nse_is_trading_day,
33
+ next_trading_day as nse_next_trading_day,
34
+ )
35
 
36
  app = Flask(__name__)
37
  app.config["JSON_SORT_KEYS"] = False
 
2279
  @app.route("/api/validation/execute", methods=["POST"])
2280
  def validation_execute():
2281
  """Execute validation for pending predictions — parallel OHLCV fetches to avoid timeouts."""
2282
+ _IST = timezone(timedelta(hours=5, minutes=30))
2283
+ today_ist = datetime.now(timezone.utc).astimezone(_IST).date()
2284
+ if not nse_is_trading_day(today_ist):
2285
+ return _json_no_store({
2286
+ "skipped": True,
2287
+ "reason": "today_is_holiday",
2288
+ "next_trading_day": nse_next_trading_day(today_ist).isoformat(),
2289
+ })
2290
+
2291
  pending = db.get_validation_pending(limit=500, due_only=True)
2292
 
2293
  # Pre-filter: handle NO TRADE / bad entry immediately (no I/O needed)
database.py CHANGED
@@ -276,6 +276,60 @@ def _migrate() -> None:
276
  except Exception:
277
  pass
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  cols = {row[1] for row in conn.execute("PRAGMA table_info(trades)").fetchall()}
280
 
281
  if "snapshot_id" not in cols:
@@ -682,15 +736,14 @@ def get_postmortems() -> list[dict]:
682
  # ── PREDICTION SNAPSHOTS (audit trail) ───────────────────────────────────────
683
 
684
  def _trading_deadline(timeframe: str) -> str:
685
- """Return the ISO date string when a prediction for the given timeframe expires (weekends skipped)."""
686
  from datetime import datetime, timedelta, timezone
 
687
  tf_offset = {"INTRADAY": 0, "1D": 1, "3D": 3, "5D": 5, "1W": 7}
688
  days_offset = tf_offset.get(timeframe, 1)
689
  now_ist = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=5, minutes=30)))
690
  target_dt = now_ist + timedelta(days=days_offset)
691
- while target_dt.weekday() >= 5:
692
- target_dt += timedelta(days=1)
693
- return target_dt.strftime("%Y-%m-%d")
694
 
695
 
696
  def save_prediction_snapshot(
@@ -714,16 +767,15 @@ def save_prediction_snapshot(
714
  if "." not in ticker:
715
  ticker += ".NS"
716
 
717
- # Calculate validation target date based on timeframe, skipping weekends.
718
  # INTRADAY (offset 0) validates same-day — target_date == today (a trading day).
 
719
  tf_offset = {"INTRADAY": 0, "1D": 1, "3D": 3, "5D": 5, "1W": 7}
720
  days_offset = tf_offset.get(timeframe, 1)
721
  now_utc = datetime.now(timezone.utc)
722
  now_ist = now_utc.astimezone(timezone(timedelta(hours=5, minutes=30)))
723
  target_dt = now_ist + timedelta(days=days_offset)
724
- while target_dt.weekday() >= 5: # 5=Sat, 6=Sun — NSE is closed
725
- target_dt += timedelta(days=1)
726
- target_date = target_dt.strftime("%Y-%m-%d")
727
 
728
  with _conn() as conn:
729
  # Dedup: skip if the same ticker/timeframe/direction/target_date was already saved today.
 
276
  except Exception:
277
  pass
278
 
279
+ # Fix any PENDING snapshots whose target date landed on an NSE weekday holiday.
280
+ # SQL cannot access _NSE_HOLIDAYS, so we use a Python loop with next_trading_day().
281
+ # Idempotent — rows already on a trading day are skipped.
282
+ try:
283
+ from market_calendar import next_trading_day as _ntd
284
+ from datetime import date as _date
285
+ _holiday_rows = conn.execute(
286
+ "SELECT id, validation_target_date FROM prediction_snapshots "
287
+ "WHERE validation_status = 'PENDING'"
288
+ ).fetchall()
289
+ for _row in _holiday_rows:
290
+ _raw = _row[1]
291
+ if not _raw:
292
+ continue
293
+ try:
294
+ _d = _date.fromisoformat(_raw)
295
+ except ValueError:
296
+ continue
297
+ _fixed = _ntd(_d)
298
+ if _fixed != _d:
299
+ conn.execute(
300
+ "UPDATE prediction_snapshots SET validation_target_date = ? WHERE id = ?",
301
+ (_fixed.isoformat(), _row[0]),
302
+ )
303
+ except Exception:
304
+ pass
305
+
306
+ # Fix any PENDING snapshots whose target date landed on an NSE weekday holiday.
307
+ # SQL cannot access _NSE_HOLIDAYS, so we use a Python loop with next_trading_day().
308
+ # Idempotent — rows already on a trading day are skipped (next_trading_day returns d unchanged).
309
+ try:
310
+ from market_calendar import next_trading_day as _ntd
311
+ from datetime import date as _date
312
+ holiday_rows = conn.execute(
313
+ "SELECT id, validation_target_date FROM prediction_snapshots "
314
+ "WHERE validation_status = 'PENDING'"
315
+ ).fetchall()
316
+ for _row in holiday_rows:
317
+ _raw = _row[1]
318
+ if not _raw:
319
+ continue
320
+ try:
321
+ _d = _date.fromisoformat(_raw)
322
+ except ValueError:
323
+ continue
324
+ _fixed = _ntd(_d)
325
+ if _fixed != _d:
326
+ conn.execute(
327
+ "UPDATE prediction_snapshots SET validation_target_date = ? WHERE id = ?",
328
+ (_fixed.isoformat(), _row[0]),
329
+ )
330
+ except Exception:
331
+ pass
332
+
333
  cols = {row[1] for row in conn.execute("PRAGMA table_info(trades)").fetchall()}
334
 
335
  if "snapshot_id" not in cols:
 
736
  # ── PREDICTION SNAPSHOTS (audit trail) ───────────────────────────────────────
737
 
738
  def _trading_deadline(timeframe: str) -> str:
739
+ """Return the ISO date string when a prediction for the given timeframe expires (weekends + NSE holidays skipped)."""
740
  from datetime import datetime, timedelta, timezone
741
+ from market_calendar import next_trading_day
742
  tf_offset = {"INTRADAY": 0, "1D": 1, "3D": 3, "5D": 5, "1W": 7}
743
  days_offset = tf_offset.get(timeframe, 1)
744
  now_ist = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=5, minutes=30)))
745
  target_dt = now_ist + timedelta(days=days_offset)
746
+ return next_trading_day(target_dt.date()).isoformat()
 
 
747
 
748
 
749
  def save_prediction_snapshot(
 
767
  if "." not in ticker:
768
  ticker += ".NS"
769
 
770
+ # Calculate validation target date based on timeframe, skipping weekends and NSE holidays.
771
  # INTRADAY (offset 0) validates same-day — target_date == today (a trading day).
772
+ from market_calendar import next_trading_day
773
  tf_offset = {"INTRADAY": 0, "1D": 1, "3D": 3, "5D": 5, "1W": 7}
774
  days_offset = tf_offset.get(timeframe, 1)
775
  now_utc = datetime.now(timezone.utc)
776
  now_ist = now_utc.astimezone(timezone(timedelta(hours=5, minutes=30)))
777
  target_dt = now_ist + timedelta(days=days_offset)
778
+ target_date = next_trading_day(target_dt.date()).isoformat()
 
 
779
 
780
  with _conn() as conn:
781
  # Dedup: skip if the same ticker/timeframe/direction/target_date was already saved today.