Upload 35 files
Browse files- __pycache__/app.cpython-311.pyc +0 -0
- app.py +27 -0
- data/tplus1_test_predictions.parquet +3 -0
- models/nifty_1420_tplus1_logistic_model.joblib +3 -0
- models/tplus1_latest_prediction.csv +2 -0
- models/tplus1_summary.json +37 -0
- nifty_backend/__pycache__/runtime.cpython-311.pyc +0 -0
- nifty_backend/runtime.py +226 -0
__pycache__/app.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ
|
|
|
app.py
CHANGED
|
@@ -15,10 +15,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
| 15 |
from nifty_backend.runtime import (
|
| 16 |
CLOSE_REFRESH_READY,
|
| 17 |
IST,
|
|
|
|
| 18 |
close_refresh_due,
|
| 19 |
dashboard_payload,
|
| 20 |
is_trading_day,
|
| 21 |
latest_saved_prediction,
|
|
|
|
| 22 |
next_trading_day,
|
| 23 |
refresh_daily_data,
|
| 24 |
refresh_first5_prediction,
|
|
@@ -74,7 +76,13 @@ def current_market_state(now: datetime | None = None) -> dict:
|
|
| 74 |
trading_day = is_trading_day(today)
|
| 75 |
latest_date = latest_prediction_date()
|
| 76 |
market_is_open_for_t5 = trading_day and FIRST5_READY <= current_time < MARKET_CLOSE
|
|
|
|
| 77 |
has_current_first5 = market_is_open_for_t5 and latest_date == today
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
next_session = today if trading_day and current_time < MARKET_CLOSE else next_trading_day(today + timedelta(days=1))
|
| 79 |
|
| 80 |
if not trading_day:
|
|
@@ -113,6 +121,9 @@ def current_market_state(now: datetime | None = None) -> dict:
|
|
| 113 |
"latest_prediction_date": latest_date.isoformat() if latest_date else None,
|
| 114 |
"t5_available": has_current_first5,
|
| 115 |
"market_is_open_for_t5": market_is_open_for_t5,
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
|
|
@@ -123,7 +134,9 @@ def attach_market_state(payload: dict) -> dict:
|
|
| 123 |
|
| 124 |
t5_latest = payload.get("latest") or {}
|
| 125 |
tomorrow_latest = payload.get("tomorrow_latest") or {}
|
|
|
|
| 126 |
t5_available = bool(state["t5_available"] and t5_latest.get("prediction"))
|
|
|
|
| 127 |
market_closed = state["market_status"] == "Market Closed"
|
| 128 |
unavailable_reason = "Market Closed" if market_closed else state["market_status"]
|
| 129 |
tomorrow_available = bool(tomorrow_latest.get("prediction"))
|
|
@@ -163,6 +176,20 @@ def attach_market_state(payload: dict) -> dict:
|
|
| 163 |
"validation_accuracy": (payload.get("summary") or {}).get("validation_accuracy"),
|
| 164 |
"test_accuracy": (payload.get("summary") or {}).get("test_accuracy"),
|
| 165 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
return payload
|
| 168 |
|
|
|
|
| 15 |
from nifty_backend.runtime import (
|
| 16 |
CLOSE_REFRESH_READY,
|
| 17 |
IST,
|
| 18 |
+
TPLUS1_READY,
|
| 19 |
close_refresh_due,
|
| 20 |
dashboard_payload,
|
| 21 |
is_trading_day,
|
| 22 |
latest_saved_prediction,
|
| 23 |
+
latest_tplus1_prediction,
|
| 24 |
next_trading_day,
|
| 25 |
refresh_daily_data,
|
| 26 |
refresh_first5_prediction,
|
|
|
|
| 76 |
trading_day = is_trading_day(today)
|
| 77 |
latest_date = latest_prediction_date()
|
| 78 |
market_is_open_for_t5 = trading_day and FIRST5_READY <= current_time < MARKET_CLOSE
|
| 79 |
+
market_is_open_for_tplus1 = trading_day and TPLUS1_READY <= current_time < MARKET_CLOSE
|
| 80 |
has_current_first5 = market_is_open_for_t5 and latest_date == today
|
| 81 |
+
try:
|
| 82 |
+
tplus1_latest_date = date.fromisoformat(str(latest_tplus1_prediction().get("input_date"))[:10])
|
| 83 |
+
except Exception:
|
| 84 |
+
tplus1_latest_date = None
|
| 85 |
+
has_current_tplus1 = market_is_open_for_tplus1 and tplus1_latest_date == today
|
| 86 |
next_session = today if trading_day and current_time < MARKET_CLOSE else next_trading_day(today + timedelta(days=1))
|
| 87 |
|
| 88 |
if not trading_day:
|
|
|
|
| 121 |
"latest_prediction_date": latest_date.isoformat() if latest_date else None,
|
| 122 |
"t5_available": has_current_first5,
|
| 123 |
"market_is_open_for_t5": market_is_open_for_t5,
|
| 124 |
+
"tplus1_available": has_current_tplus1,
|
| 125 |
+
"market_is_open_for_tplus1": market_is_open_for_tplus1,
|
| 126 |
+
"latest_tplus1_prediction_date": tplus1_latest_date.isoformat() if tplus1_latest_date else None,
|
| 127 |
}
|
| 128 |
|
| 129 |
|
|
|
|
| 134 |
|
| 135 |
t5_latest = payload.get("latest") or {}
|
| 136 |
tomorrow_latest = payload.get("tomorrow_latest") or {}
|
| 137 |
+
tplus1_latest = payload.get("tplus1_latest") or {}
|
| 138 |
t5_available = bool(state["t5_available"] and t5_latest.get("prediction"))
|
| 139 |
+
tplus1_available = bool(state["tplus1_available"] and tplus1_latest.get("prediction"))
|
| 140 |
market_closed = state["market_status"] == "Market Closed"
|
| 141 |
unavailable_reason = "Market Closed" if market_closed else state["market_status"]
|
| 142 |
tomorrow_available = bool(tomorrow_latest.get("prediction"))
|
|
|
|
| 176 |
"validation_accuracy": (payload.get("summary") or {}).get("validation_accuracy"),
|
| 177 |
"test_accuracy": (payload.get("summary") or {}).get("test_accuracy"),
|
| 178 |
},
|
| 179 |
+
"tplus1": {
|
| 180 |
+
"available": tplus1_available,
|
| 181 |
+
"status": "Ready" if tplus1_available else unavailable_reason,
|
| 182 |
+
"reason": None if tplus1_available else state["market_detail"],
|
| 183 |
+
"target_date": tplus1_latest.get("target_date") or state["next_session_date"],
|
| 184 |
+
"input_date": tplus1_latest.get("input_date"),
|
| 185 |
+
"prediction": tplus1_latest.get("prediction") if tplus1_available else None,
|
| 186 |
+
"prob_up": tplus1_latest.get("prob_up") if tplus1_available else None,
|
| 187 |
+
"confidence": tplus1_latest.get("confidence") if tplus1_available else None,
|
| 188 |
+
"threshold": tplus1_latest.get("threshold") if tplus1_available else None,
|
| 189 |
+
"model_name": tplus1_latest.get("model_name"),
|
| 190 |
+
"validation_accuracy": (payload.get("tplus1_summary") or {}).get("validation_accuracy"),
|
| 191 |
+
"test_accuracy": (payload.get("tplus1_summary") or {}).get("test_accuracy"),
|
| 192 |
+
},
|
| 193 |
}
|
| 194 |
return payload
|
| 195 |
|
data/tplus1_test_predictions.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bd7f63239d5705969cbe5423169c9fdcb88d59b838b02913d0aa5c455a7ca41c
|
| 3 |
+
size 13681
|
models/nifty_1420_tplus1_logistic_model.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77fea2ef4be30e6355377c1badadc94e9cb941fb62a31e731fad83b0f171c63e
|
| 3 |
+
size 5321
|
models/tplus1_latest_prediction.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
input_date,target_date,forecast_for,prediction,prob_up,confidence,threshold,model_name,decision_overlay,validation_accuracy,test_accuracy,accuracy_goal
|
| 2 |
+
2026-05-21,2026-05-22,next trading session after 2026-05-21,UP,0.620604654276822,0.620604654276822,0.578,logistic_regression_l1_C0.35_balanced,prev_target_mean10_le_0.4_up;m02_range_1m_ge_0.000479116_up,0.66,0.6368421052631579,0.63
|
models/tplus1_summary.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"target": "T+1 NIFTY 50 close greater than T 14:20 close",
|
| 3 |
+
"exchange_instrument": "NSE NIFTY 50 index",
|
| 4 |
+
"window_start": "14:00",
|
| 5 |
+
"window_end": "14:20",
|
| 6 |
+
"lookback_days_requested": 1000,
|
| 7 |
+
"supervised_rows": 1000,
|
| 8 |
+
"train_rows": 660,
|
| 9 |
+
"valid_rows": 150,
|
| 10 |
+
"test_rows": 190,
|
| 11 |
+
"train_start": "2022-04-28",
|
| 12 |
+
"train_end": "2024-12-31",
|
| 13 |
+
"valid_start": "2025-01-01",
|
| 14 |
+
"valid_end": "2025-08-06",
|
| 15 |
+
"test_start": "2025-08-07",
|
| 16 |
+
"test_end": "2026-05-20",
|
| 17 |
+
"model_name": "logistic_regression_l1_C0.35_balanced",
|
| 18 |
+
"threshold": 0.578,
|
| 19 |
+
"validation_accuracy": 0.66,
|
| 20 |
+
"test_accuracy": 0.6368421052631579,
|
| 21 |
+
"baseline_test_accuracy": 0.5052631578947369,
|
| 22 |
+
"validation_auc": 0.5085348506401137,
|
| 23 |
+
"test_auc": 0.54864804964539,
|
| 24 |
+
"test_log_loss": 0.715200083567372,
|
| 25 |
+
"test_brier": 0.2580990249203714,
|
| 26 |
+
"accuracy_goal": 0.63,
|
| 27 |
+
"accuracy_goal_met_on_test": true,
|
| 28 |
+
"feature_count": 40,
|
| 29 |
+
"latest_input_date": "2026-05-21",
|
| 30 |
+
"latest_window_rows": 21,
|
| 31 |
+
"latest_forecast_for": "next trading session after 2026-05-21",
|
| 32 |
+
"latest_prob_up": 0.620604654276822,
|
| 33 |
+
"latest_prediction": "UP",
|
| 34 |
+
"latest_confidence": 0.620604654276822,
|
| 35 |
+
"decision_overlay": "prev_target_mean10_le_0.4_up;m02_range_1m_ge_0.000479116_up",
|
| 36 |
+
"top_features": 40
|
| 37 |
+
}
|
nifty_backend/__pycache__/runtime.cpython-311.pyc
CHANGED
|
Binary files a/nifty_backend/__pycache__/runtime.cpython-311.pyc and b/nifty_backend/__pycache__/runtime.cpython-311.pyc differ
|
|
|
nifty_backend/runtime.py
CHANGED
|
@@ -26,6 +26,7 @@ IST = ZoneInfo("Asia/Kolkata")
|
|
| 26 |
YAHOO_NIFTY_SYMBOL = "^NSEI"
|
| 27 |
MARKET_CLOSE = time(15, 30)
|
| 28 |
CLOSE_REFRESH_READY = time(15, 45)
|
|
|
|
| 29 |
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
| 30 |
DATA_DIR = BACKEND_ROOT / "data"
|
| 31 |
MODEL_DIR = BACKEND_ROOT / "models"
|
|
@@ -39,6 +40,10 @@ TOMORROW_MODEL_PATH = MODEL_DIR / "nifty_tomorrow_direction_model.joblib"
|
|
| 39 |
TOMORROW_LATEST_PATH = MODEL_DIR / "tomorrow_latest_prediction.csv"
|
| 40 |
TOMORROW_SUMMARY_PATH = MODEL_DIR / "tomorrow_summary.json"
|
| 41 |
TOMORROW_TEST_PREDICTIONS_PATH = DATA_DIR / "tomorrow_test_predictions.parquet"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
REFRESH_STATE_PATH = MODEL_DIR / "refresh_state.json"
|
| 43 |
REFRESH_WAITING = "waiting_second_payload"
|
| 44 |
REFRESH_REFRESHING = "refreshing"
|
|
@@ -522,6 +527,194 @@ def latest_tomorrow_prediction() -> dict[str, Any]:
|
|
| 522 |
}
|
| 523 |
|
| 524 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
def _tomorrow_probability_from_daily(daily: pd.DataFrame, fallback_prob: float) -> float:
|
| 526 |
if daily.empty or len(daily) < 5:
|
| 527 |
return float(fallback_prob)
|
|
@@ -630,6 +823,16 @@ def load_tomorrow_test_predictions() -> pd.DataFrame:
|
|
| 630 |
return df.sort_values(sort_col).reset_index(drop=True)
|
| 631 |
|
| 632 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
def dashboard_payload() -> dict[str, Any]:
|
| 634 |
key = (
|
| 635 |
_file_cache_key(MODEL_DIR / "summary.json"),
|
|
@@ -639,6 +842,10 @@ def dashboard_payload() -> dict[str, Any]:
|
|
| 639 |
_file_cache_key(TOMORROW_LATEST_PATH),
|
| 640 |
_file_cache_key(TOMORROW_TEST_PREDICTIONS_PATH),
|
| 641 |
_file_cache_key(TOMORROW_MODEL_PATH),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
_file_cache_key(REFRESH_STATE_PATH),
|
| 643 |
_file_cache_key(NIFTY_1D_PATH),
|
| 644 |
_file_cache_key(OPENING_DATASET_PATH),
|
|
@@ -659,9 +866,12 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 659 |
t5_latest = _latest_saved_prediction_uncached()
|
| 660 |
tomorrow_summary = load_tomorrow_summary()
|
| 661 |
tomorrow_latest = latest_tomorrow_prediction()
|
|
|
|
|
|
|
| 662 |
refresh_state = load_refresh_state()
|
| 663 |
t5_test = load_test_predictions()
|
| 664 |
tomorrow_test = load_tomorrow_test_predictions()
|
|
|
|
| 665 |
daily = pd.read_parquet(NIFTY_1D_PATH)
|
| 666 |
daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
|
| 667 |
daily = daily.sort_values("date").tail(180)
|
|
@@ -706,6 +916,16 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 706 |
"recent_accuracy": tomorrow_accuracy,
|
| 707 |
"test_rows": int(tomorrow_summary.get("n_test") or len(tomorrow_test) or 0),
|
| 708 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
{
|
| 710 |
"id": "t5",
|
| 711 |
"label": "T+5",
|
|
@@ -733,9 +953,11 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 733 |
return {
|
| 734 |
"latest": t5_latest,
|
| 735 |
"tomorrow_latest": tomorrow_latest,
|
|
|
|
| 736 |
"metrics": metrics,
|
| 737 |
"summary": summary,
|
| 738 |
"tomorrow_summary": tomorrow_summary,
|
|
|
|
| 739 |
"candidates": load_candidate_results(),
|
| 740 |
"charts": {
|
| 741 |
"daily_close": _json_ready_frame(daily[["date", "open", "high", "low", "close"]]),
|
|
@@ -745,6 +967,7 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 745 |
"recent_predictions": _json_ready_frame(recent_predictions),
|
| 746 |
"t5_recent_predictions": _json_ready_frame(recent_predictions),
|
| 747 |
"tomorrow_recent_predictions": _json_ready_frame(tomorrow_recent),
|
|
|
|
| 748 |
},
|
| 749 |
"data_status": {
|
| 750 |
"nifty_1m_rows": int(len(pd.read_parquet(NIFTY_1M_PATH, columns=["date"]))),
|
|
@@ -752,6 +975,7 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 752 |
"training_rows": int(len(dataset)),
|
| 753 |
"test_prediction_rows": int(len(t5_test)),
|
| 754 |
"tomorrow_test_prediction_rows": int(len(tomorrow_test)),
|
|
|
|
| 755 |
"latest_daily_date": pd.to_datetime(daily["date"]).max().date().isoformat(),
|
| 756 |
"refresh_phase": refresh_state.get("phase", REFRESH_NORMAL),
|
| 757 |
"refresh_state": refresh_state,
|
|
@@ -853,6 +1077,7 @@ def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any
|
|
| 853 |
minute_frame = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
|
| 854 |
daily_info = refresh_daily_data()
|
| 855 |
t5_prediction = refresh_first5_prediction(session_date=session_date, minutes=minutes)
|
|
|
|
| 856 |
outcomes = update_opening_outcomes_from_daily()
|
| 857 |
tomorrow_prediction = refresh_tomorrow_prediction(session_date=session_date)
|
| 858 |
state = save_refresh_state(REFRESH_READY, session_date=session_date)
|
|
@@ -864,6 +1089,7 @@ def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any
|
|
| 864 |
"daily": daily_info,
|
| 865 |
"opening_dataset": outcomes,
|
| 866 |
"t5_prediction": t5_prediction.to_dict(),
|
|
|
|
| 867 |
"tomorrow_prediction": tomorrow_prediction,
|
| 868 |
"refresh_state": state,
|
| 869 |
}
|
|
|
|
| 26 |
YAHOO_NIFTY_SYMBOL = "^NSEI"
|
| 27 |
MARKET_CLOSE = time(15, 30)
|
| 28 |
CLOSE_REFRESH_READY = time(15, 45)
|
| 29 |
+
TPLUS1_READY = time(14, 30)
|
| 30 |
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
| 31 |
DATA_DIR = BACKEND_ROOT / "data"
|
| 32 |
MODEL_DIR = BACKEND_ROOT / "models"
|
|
|
|
| 40 |
TOMORROW_LATEST_PATH = MODEL_DIR / "tomorrow_latest_prediction.csv"
|
| 41 |
TOMORROW_SUMMARY_PATH = MODEL_DIR / "tomorrow_summary.json"
|
| 42 |
TOMORROW_TEST_PREDICTIONS_PATH = DATA_DIR / "tomorrow_test_predictions.parquet"
|
| 43 |
+
TPLUS1_MODEL_PATH = MODEL_DIR / "nifty_1420_tplus1_logistic_model.joblib"
|
| 44 |
+
TPLUS1_LATEST_PATH = MODEL_DIR / "tplus1_latest_prediction.csv"
|
| 45 |
+
TPLUS1_SUMMARY_PATH = MODEL_DIR / "tplus1_summary.json"
|
| 46 |
+
TPLUS1_TEST_PREDICTIONS_PATH = DATA_DIR / "tplus1_test_predictions.parquet"
|
| 47 |
REFRESH_STATE_PATH = MODEL_DIR / "refresh_state.json"
|
| 48 |
REFRESH_WAITING = "waiting_second_payload"
|
| 49 |
REFRESH_REFRESHING = "refreshing"
|
|
|
|
| 527 |
}
|
| 528 |
|
| 529 |
|
| 530 |
+
def load_tplus1_summary() -> dict[str, Any]:
|
| 531 |
+
if TPLUS1_SUMMARY_PATH.exists():
|
| 532 |
+
return json.loads(TPLUS1_SUMMARY_PATH.read_text(encoding="utf-8"))
|
| 533 |
+
return {
|
| 534 |
+
"model_name": "logistic_regression_l1_C0.35_balanced",
|
| 535 |
+
"target": "T+1 NIFTY 50 close greater than T 14:20 close",
|
| 536 |
+
"window_start": "14:00",
|
| 537 |
+
"window_end": "14:20",
|
| 538 |
+
"threshold": 0.578,
|
| 539 |
+
"validation_accuracy": 0.66,
|
| 540 |
+
"test_accuracy": 0.6368421052631579,
|
| 541 |
+
"baseline_test_accuracy": 0.5052631578947369,
|
| 542 |
+
"test_rows": 190,
|
| 543 |
+
"feature_count": 40,
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def latest_tplus1_prediction() -> dict[str, Any]:
|
| 548 |
+
if TPLUS1_LATEST_PATH.exists():
|
| 549 |
+
row = pd.read_csv(TPLUS1_LATEST_PATH).iloc[-1].to_dict()
|
| 550 |
+
return {k: (None if pd.isna(v) else v) for k, v in row.items()}
|
| 551 |
+
summary = load_tplus1_summary()
|
| 552 |
+
return {
|
| 553 |
+
"input_date": summary.get("latest_input_date"),
|
| 554 |
+
"target_date": None,
|
| 555 |
+
"forecast_for": summary.get("latest_forecast_for"),
|
| 556 |
+
"prediction": summary.get("latest_prediction"),
|
| 557 |
+
"prob_up": summary.get("latest_prob_up"),
|
| 558 |
+
"confidence": summary.get("latest_confidence"),
|
| 559 |
+
"threshold": summary.get("threshold"),
|
| 560 |
+
"model_name": summary.get("model_name", "logistic_regression_l1_C0.35_balanced"),
|
| 561 |
+
"validation_accuracy": summary.get("validation_accuracy"),
|
| 562 |
+
"test_accuracy": summary.get("test_accuracy"),
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def _minute_frame_for_tplus1() -> pd.DataFrame:
|
| 567 |
+
minute = pd.read_parquet(NIFTY_1M_PATH)
|
| 568 |
+
minute = minute.copy()
|
| 569 |
+
minute["dt"] = pd.to_datetime(minute["date"], errors="coerce")
|
| 570 |
+
for col in ("open", "high", "low", "close", "volume"):
|
| 571 |
+
if col in minute.columns:
|
| 572 |
+
minute[col] = pd.to_numeric(minute[col], errors="coerce")
|
| 573 |
+
minute = minute.dropna(subset=["dt", "open", "high", "low", "close"]).sort_values("dt").reset_index(drop=True)
|
| 574 |
+
minute["session_date"] = minute["dt"].dt.normalize()
|
| 575 |
+
minute["time"] = minute["dt"].dt.strftime("%H:%M")
|
| 576 |
+
return minute
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def _build_tplus1_session_features(minute: pd.DataFrame) -> pd.DataFrame:
|
| 580 |
+
window = minute[(minute["time"] >= "14:00") & (minute["time"] <= "14:20")].copy()
|
| 581 |
+
window["minute_offset"] = window.groupby("session_date", sort=True).cumcount()
|
| 582 |
+
grouped = window.groupby("session_date", sort=True)
|
| 583 |
+
base = grouped.agg(
|
| 584 |
+
window_start=("dt", "first"),
|
| 585 |
+
window_end=("dt", "last"),
|
| 586 |
+
window_rows=("close", "size"),
|
| 587 |
+
w_open=("open", "first"),
|
| 588 |
+
w_high=("high", "max"),
|
| 589 |
+
w_low=("low", "min"),
|
| 590 |
+
w_close=("close", "last"),
|
| 591 |
+
w_volume=("volume", "sum") if "volume" in window.columns else ("close", "size"),
|
| 592 |
+
).reset_index().rename(columns={"session_date": "date"})
|
| 593 |
+
base = base[base["window_rows"] == 21].copy()
|
| 594 |
+
base["w_return"] = safe_div(base["w_close"] - base["w_open"], base["w_open"])
|
| 595 |
+
base["w_range"] = safe_div(base["w_high"] - base["w_low"], base["w_open"])
|
| 596 |
+
base["w_body_to_range"] = safe_div(base["w_close"] - base["w_open"], base["w_high"] - base["w_low"])
|
| 597 |
+
base["w_close_location"] = safe_div(base["w_close"] - base["w_low"], base["w_high"] - base["w_low"])
|
| 598 |
+
window["ret_1m"] = window.groupby("session_date")["close"].pct_change(fill_method=None)
|
| 599 |
+
window["range_1m"] = safe_div(window["high"] - window["low"], window["open"])
|
| 600 |
+
window["body_1m"] = safe_div(window["close"] - window["open"], window["open"])
|
| 601 |
+
minute_features = window.pivot(
|
| 602 |
+
index="session_date",
|
| 603 |
+
columns="minute_offset",
|
| 604 |
+
values=["open", "high", "low", "close", "ret_1m", "range_1m", "body_1m"],
|
| 605 |
+
)
|
| 606 |
+
minute_features.columns = [f"m{int(offset):02d}_{field}" for field, offset in minute_features.columns]
|
| 607 |
+
minute_features = minute_features.reset_index().rename(columns={"session_date": "date"})
|
| 608 |
+
session_close = (
|
| 609 |
+
minute.groupby("session_date", sort=True)
|
| 610 |
+
.agg(day_close=("close", "last"))
|
| 611 |
+
.reset_index()
|
| 612 |
+
.rename(columns={"session_date": "date"})
|
| 613 |
+
)
|
| 614 |
+
frame = base.merge(minute_features, on="date", how="left").merge(session_close, on="date", how="left")
|
| 615 |
+
for offset in range(21):
|
| 616 |
+
close_col = f"m{offset:02d}_close"
|
| 617 |
+
open_col = f"m{offset:02d}_open"
|
| 618 |
+
if close_col in frame.columns:
|
| 619 |
+
frame[f"m{offset:02d}_close_vs_window_open"] = safe_div(frame[close_col] - frame["w_open"], frame["w_open"])
|
| 620 |
+
if open_col in frame.columns and close_col in frame.columns:
|
| 621 |
+
frame[f"m{offset:02d}_close_vs_minute_open"] = safe_div(frame[close_col] - frame[open_col], frame[open_col])
|
| 622 |
+
frame["ret_first_5m"] = safe_div(frame["m04_close"] - frame["m00_open"], frame["m00_open"])
|
| 623 |
+
frame["ret_last_5m"] = safe_div(frame["m20_close"] - frame["m16_open"], frame["m16_open"])
|
| 624 |
+
frame["ret_mid_11m"] = safe_div(frame["m15_close"] - frame["m05_open"], frame["m05_open"])
|
| 625 |
+
frame["last5_minus_first5"] = frame["ret_last_5m"] - frame["ret_first_5m"]
|
| 626 |
+
frame["abs_window_return"] = frame["w_return"].abs()
|
| 627 |
+
frame["dow"] = frame["date"].dt.dayofweek
|
| 628 |
+
frame["dom"] = frame["date"].dt.day
|
| 629 |
+
frame["month"] = frame["date"].dt.month
|
| 630 |
+
return frame.sort_values("date").reset_index(drop=True)
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def _add_tplus1_target_features(features: pd.DataFrame) -> pd.DataFrame:
|
| 634 |
+
frame = features.copy()
|
| 635 |
+
frame["target_date"] = frame["date"].shift(-1)
|
| 636 |
+
frame["target_close"] = frame["day_close"].shift(-1)
|
| 637 |
+
frame["target_return_from_1420"] = safe_div(frame["target_close"] - frame["w_close"], frame["w_close"])
|
| 638 |
+
frame["target"] = (frame["target_return_from_1420"] > 0).astype("float64")
|
| 639 |
+
frame.loc[frame["target_close"].isna(), "target"] = np.nan
|
| 640 |
+
for lag in (1, 2, 3, 5, 10):
|
| 641 |
+
frame[f"prev_target_lag{lag}"] = frame["target"].shift(lag)
|
| 642 |
+
frame[f"prev_target_return_lag{lag}"] = frame["target_return_from_1420"].shift(lag)
|
| 643 |
+
for window in (3, 5, 10, 20, 40):
|
| 644 |
+
min_periods = max(2, window // 2)
|
| 645 |
+
frame[f"prev_target_mean{window}"] = frame["target"].shift(1).rolling(window, min_periods=min_periods).mean()
|
| 646 |
+
shifted_return = frame["target_return_from_1420"].shift(1)
|
| 647 |
+
frame[f"prev_target_return_mean{window}"] = shifted_return.rolling(window, min_periods=min_periods).mean()
|
| 648 |
+
frame[f"prev_target_return_std{window}"] = shifted_return.rolling(window, min_periods=min_periods).std()
|
| 649 |
+
return frame
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def _apply_tplus1_overlays(pred: np.ndarray, frame: pd.DataFrame, overlays: list[dict[str, Any]]) -> np.ndarray:
|
| 653 |
+
adjusted = np.asarray(pred, dtype="int64").copy()
|
| 654 |
+
for overlay in overlays:
|
| 655 |
+
feature = str(overlay.get("feature", ""))
|
| 656 |
+
if feature not in frame.columns:
|
| 657 |
+
continue
|
| 658 |
+
series = pd.to_numeric(frame[feature], errors="coerce")
|
| 659 |
+
value = float(overlay.get("value", 0.0))
|
| 660 |
+
if overlay.get("op") == "<=":
|
| 661 |
+
mask = (series <= value).fillna(False).to_numpy(dtype=bool)
|
| 662 |
+
else:
|
| 663 |
+
mask = (series >= value).fillna(False).to_numpy(dtype=bool)
|
| 664 |
+
action = overlay.get("action")
|
| 665 |
+
if action == "up":
|
| 666 |
+
adjusted[mask] = 1
|
| 667 |
+
elif action == "down":
|
| 668 |
+
adjusted[mask] = 0
|
| 669 |
+
elif action == "flip":
|
| 670 |
+
adjusted[mask] = 1 - adjusted[mask]
|
| 671 |
+
return adjusted
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any]:
|
| 675 |
+
if not TPLUS1_MODEL_PATH.exists():
|
| 676 |
+
raise FileNotFoundError(f"Missing T+1 model artifact: {TPLUS1_MODEL_PATH}")
|
| 677 |
+
payload = joblib.load(TPLUS1_MODEL_PATH)
|
| 678 |
+
features = payload["features"]
|
| 679 |
+
threshold = float(payload["threshold"])
|
| 680 |
+
frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
|
| 681 |
+
if session_date is not None:
|
| 682 |
+
row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
|
| 683 |
+
else:
|
| 684 |
+
row = frame.tail(1)
|
| 685 |
+
if row.empty:
|
| 686 |
+
raise RuntimeError("No complete 14:00-14:20 window is available for T+1 prediction.")
|
| 687 |
+
missing = [col for col in features if col not in row.columns]
|
| 688 |
+
if missing:
|
| 689 |
+
raise RuntimeError(f"T+1 feature row is missing model features: {missing[:5]}")
|
| 690 |
+
prob_up = predict_proba_up(payload["model"], row[features])
|
| 691 |
+
raw_pred = (prob_up >= threshold).astype("int64")
|
| 692 |
+
overlay_payload = payload.get("decision_overlay")
|
| 693 |
+
overlays = overlay_payload.get("overlays", []) if isinstance(overlay_payload, dict) else []
|
| 694 |
+
pred_int = int(_apply_tplus1_overlays(raw_pred, row, overlays)[0])
|
| 695 |
+
prediction = "UP" if pred_int == 1 else "DOWN"
|
| 696 |
+
input_day = pd.to_datetime(row["date"].iloc[0]).date()
|
| 697 |
+
target_day = next_trading_day(input_day + timedelta(days=1))
|
| 698 |
+
summary = load_tplus1_summary()
|
| 699 |
+
out = {
|
| 700 |
+
"input_date": input_day.isoformat(),
|
| 701 |
+
"target_date": target_day.isoformat(),
|
| 702 |
+
"forecast_for": f"next trading session after {input_day.isoformat()}",
|
| 703 |
+
"prediction": prediction,
|
| 704 |
+
"prob_up": float(prob_up[0]),
|
| 705 |
+
"confidence": float(max(prob_up[0], 1.0 - prob_up[0])),
|
| 706 |
+
"threshold": threshold,
|
| 707 |
+
"model_name": str(payload.get("model_name", summary.get("model_name", "nifty_1420_tplus1_logistic_model"))),
|
| 708 |
+
"decision_overlay": summary.get("decision_overlay"),
|
| 709 |
+
"validation_accuracy": summary.get("validation_accuracy"),
|
| 710 |
+
"test_accuracy": summary.get("test_accuracy"),
|
| 711 |
+
"accuracy_goal": summary.get("accuracy_goal"),
|
| 712 |
+
}
|
| 713 |
+
pd.DataFrame([out]).to_csv(TPLUS1_LATEST_PATH, index=False)
|
| 714 |
+
clear_dashboard_payload_cache()
|
| 715 |
+
return out
|
| 716 |
+
|
| 717 |
+
|
| 718 |
def _tomorrow_probability_from_daily(daily: pd.DataFrame, fallback_prob: float) -> float:
|
| 719 |
if daily.empty or len(daily) < 5:
|
| 720 |
return float(fallback_prob)
|
|
|
|
| 823 |
return df.sort_values(sort_col).reset_index(drop=True)
|
| 824 |
|
| 825 |
|
| 826 |
+
def load_tplus1_test_predictions() -> pd.DataFrame:
|
| 827 |
+
if not TPLUS1_TEST_PREDICTIONS_PATH.exists():
|
| 828 |
+
return pd.DataFrame()
|
| 829 |
+
df = pd.read_parquet(TPLUS1_TEST_PREDICTIONS_PATH)
|
| 830 |
+
for col in ("date", "target_date"):
|
| 831 |
+
if col in df.columns:
|
| 832 |
+
df[col] = pd.to_datetime(df[col], errors="coerce")
|
| 833 |
+
return df.sort_values("date").reset_index(drop=True)
|
| 834 |
+
|
| 835 |
+
|
| 836 |
def dashboard_payload() -> dict[str, Any]:
|
| 837 |
key = (
|
| 838 |
_file_cache_key(MODEL_DIR / "summary.json"),
|
|
|
|
| 842 |
_file_cache_key(TOMORROW_LATEST_PATH),
|
| 843 |
_file_cache_key(TOMORROW_TEST_PREDICTIONS_PATH),
|
| 844 |
_file_cache_key(TOMORROW_MODEL_PATH),
|
| 845 |
+
_file_cache_key(TPLUS1_SUMMARY_PATH),
|
| 846 |
+
_file_cache_key(TPLUS1_LATEST_PATH),
|
| 847 |
+
_file_cache_key(TPLUS1_TEST_PREDICTIONS_PATH),
|
| 848 |
+
_file_cache_key(TPLUS1_MODEL_PATH),
|
| 849 |
_file_cache_key(REFRESH_STATE_PATH),
|
| 850 |
_file_cache_key(NIFTY_1D_PATH),
|
| 851 |
_file_cache_key(OPENING_DATASET_PATH),
|
|
|
|
| 866 |
t5_latest = _latest_saved_prediction_uncached()
|
| 867 |
tomorrow_summary = load_tomorrow_summary()
|
| 868 |
tomorrow_latest = latest_tomorrow_prediction()
|
| 869 |
+
tplus1_summary = load_tplus1_summary()
|
| 870 |
+
tplus1_latest = latest_tplus1_prediction()
|
| 871 |
refresh_state = load_refresh_state()
|
| 872 |
t5_test = load_test_predictions()
|
| 873 |
tomorrow_test = load_tomorrow_test_predictions()
|
| 874 |
+
tplus1_test = load_tplus1_test_predictions()
|
| 875 |
daily = pd.read_parquet(NIFTY_1D_PATH)
|
| 876 |
daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
|
| 877 |
daily = daily.sort_values("date").tail(180)
|
|
|
|
| 916 |
"recent_accuracy": tomorrow_accuracy,
|
| 917 |
"test_rows": int(tomorrow_summary.get("n_test") or len(tomorrow_test) or 0),
|
| 918 |
},
|
| 919 |
+
{
|
| 920 |
+
"id": "tplus1",
|
| 921 |
+
"label": "T+1",
|
| 922 |
+
"model_name": tplus1_summary.get("model_name", "nifty_1420_tplus1_logistic_model"),
|
| 923 |
+
"source_model": "14:00-14:20 logistic forecaster",
|
| 924 |
+
"validation_accuracy": tplus1_summary.get("validation_accuracy"),
|
| 925 |
+
"test_accuracy": tplus1_summary.get("test_accuracy"),
|
| 926 |
+
"recent_accuracy": float(tplus1_test.tail(40)["correct"].mean()) if not tplus1_test.empty and "correct" in tplus1_test.columns else tplus1_summary.get("test_accuracy"),
|
| 927 |
+
"test_rows": int(tplus1_summary.get("test_rows") or len(tplus1_test) or 0),
|
| 928 |
+
},
|
| 929 |
{
|
| 930 |
"id": "t5",
|
| 931 |
"label": "T+5",
|
|
|
|
| 953 |
return {
|
| 954 |
"latest": t5_latest,
|
| 955 |
"tomorrow_latest": tomorrow_latest,
|
| 956 |
+
"tplus1_latest": tplus1_latest,
|
| 957 |
"metrics": metrics,
|
| 958 |
"summary": summary,
|
| 959 |
"tomorrow_summary": tomorrow_summary,
|
| 960 |
+
"tplus1_summary": tplus1_summary,
|
| 961 |
"candidates": load_candidate_results(),
|
| 962 |
"charts": {
|
| 963 |
"daily_close": _json_ready_frame(daily[["date", "open", "high", "low", "close"]]),
|
|
|
|
| 967 |
"recent_predictions": _json_ready_frame(recent_predictions),
|
| 968 |
"t5_recent_predictions": _json_ready_frame(recent_predictions),
|
| 969 |
"tomorrow_recent_predictions": _json_ready_frame(tomorrow_recent),
|
| 970 |
+
"tplus1_recent_predictions": _json_ready_frame(tplus1_test.tail(40)),
|
| 971 |
},
|
| 972 |
"data_status": {
|
| 973 |
"nifty_1m_rows": int(len(pd.read_parquet(NIFTY_1M_PATH, columns=["date"]))),
|
|
|
|
| 975 |
"training_rows": int(len(dataset)),
|
| 976 |
"test_prediction_rows": int(len(t5_test)),
|
| 977 |
"tomorrow_test_prediction_rows": int(len(tomorrow_test)),
|
| 978 |
+
"tplus1_test_prediction_rows": int(len(tplus1_test)),
|
| 979 |
"latest_daily_date": pd.to_datetime(daily["date"]).max().date().isoformat(),
|
| 980 |
"refresh_phase": refresh_state.get("phase", REFRESH_NORMAL),
|
| 981 |
"refresh_state": refresh_state,
|
|
|
|
| 1077 |
minute_frame = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
|
| 1078 |
daily_info = refresh_daily_data()
|
| 1079 |
t5_prediction = refresh_first5_prediction(session_date=session_date, minutes=minutes)
|
| 1080 |
+
tplus1_prediction = refresh_tplus1_prediction(session_date=session_date)
|
| 1081 |
outcomes = update_opening_outcomes_from_daily()
|
| 1082 |
tomorrow_prediction = refresh_tomorrow_prediction(session_date=session_date)
|
| 1083 |
state = save_refresh_state(REFRESH_READY, session_date=session_date)
|
|
|
|
| 1089 |
"daily": daily_info,
|
| 1090 |
"opening_dataset": outcomes,
|
| 1091 |
"t5_prediction": t5_prediction.to_dict(),
|
| 1092 |
+
"tplus1_prediction": tplus1_prediction,
|
| 1093 |
"tomorrow_prediction": tomorrow_prediction,
|
| 1094 |
"refresh_state": state,
|
| 1095 |
}
|