Spaces:
Running
Running
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>
- app.py +14 -1
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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.
|