Upload 42 files
Browse files- .gitattributes +1 -0
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/kotak_neo.cpython-311.pyc +0 -0
- app.py +12 -0
- data/nifty50_1d.parquet +2 -2
- data/nifty50_1m.parquet +2 -2
- data/opening_direction_training_dataset.parquet +2 -2
- kotak_neo.py +178 -0
- models/latest_prediction.csv +1 -1
- models/tplus1_latest_prediction.csv +1 -1
- models/yahoo_history_cache.sqlite3 +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
models/yahoo_history_cache.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
__pycache__/app.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ
|
|
|
__pycache__/kotak_neo.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/kotak_neo.cpython-311.pyc and b/__pycache__/kotak_neo.cpython-311.pyc differ
|
|
|
app.py
CHANGED
|
@@ -452,6 +452,18 @@ def kotak_account() -> dict:
|
|
| 452 |
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
| 453 |
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
@app.get("/kotak/activity-log")
|
| 456 |
def kotak_activity_log() -> dict:
|
| 457 |
try:
|
|
|
|
| 452 |
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
| 453 |
|
| 454 |
|
| 455 |
+
@app.get("/kotak/nifty50-quote")
|
| 456 |
+
def kotak_nifty50_quote() -> dict:
|
| 457 |
+
try:
|
| 458 |
+
return kotak_neo_manager.fetch_nifty50_quote()
|
| 459 |
+
except KotakNeoConfigError as exc:
|
| 460 |
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
| 461 |
+
except KotakNeoSessionRequired as exc:
|
| 462 |
+
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
| 463 |
+
except KotakNeoError as exc:
|
| 464 |
+
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
| 465 |
+
|
| 466 |
+
|
| 467 |
@app.get("/kotak/activity-log")
|
| 468 |
def kotak_activity_log() -> dict:
|
| 469 |
try:
|
data/nifty50_1d.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d0830f9d3cfa91f02ce717b556983beb12f897bc537a623c67e38f22ce05caf
|
| 3 |
+
size 78366
|
data/nifty50_1m.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5d6022df273daa1f020676214ec35536426fd84f2d7efb669797287e27ffa2d
|
| 3 |
+
size 18589635
|
data/opening_direction_training_dataset.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:722c8225451abcb49463d2a57354bc0c9b5eb519f31a25fa3c5242adc4dbbada
|
| 3 |
+
size 4463631
|
kotak_neo.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import os
|
| 4 |
import threading
|
|
|
|
| 5 |
from csv import DictReader
|
| 6 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 7 |
from datetime import datetime, timezone
|
|
@@ -22,6 +23,7 @@ DEFAULT_TIMEOUT_SECONDS = 20
|
|
| 22 |
ACCOUNT_TIMEOUT_SECONDS = 7
|
| 23 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 24 |
KOTAK_ACTIVITY_LOG_PATH = DATA_DIR / "kotak_activity_log.txt"
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
class KotakNeoError(Exception):
|
|
@@ -133,6 +135,9 @@ class KotakNeoManager:
|
|
| 133 |
self.activity_log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 134 |
self._seen_activity_keys: set[str] = set()
|
| 135 |
self._scrip_cache: dict[str, list[dict[str, str]]] = {}
|
|
|
|
|
|
|
|
|
|
| 136 |
self._load_existing_activity_keys()
|
| 137 |
self._clear_session_locked()
|
| 138 |
|
|
@@ -376,6 +381,42 @@ class KotakNeoManager:
|
|
| 376 |
"quotes": list(quote_map.values()),
|
| 377 |
}
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
def _ensure_authenticated_locked(self) -> None:
|
| 380 |
if not self.edit_token or not self.edit_sid or not self.base_url:
|
| 381 |
raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
|
|
@@ -491,6 +532,143 @@ class KotakNeoManager:
|
|
| 491 |
timeout=DEFAULT_TIMEOUT_SECONDS,
|
| 492 |
)
|
| 493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
def _fetch_quotes_with_context(
|
| 495 |
self,
|
| 496 |
context: dict[str, str],
|
|
|
|
| 2 |
|
| 3 |
import os
|
| 4 |
import threading
|
| 5 |
+
import time
|
| 6 |
from csv import DictReader
|
| 7 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 8 |
from datetime import datetime, timezone
|
|
|
|
| 23 |
ACCOUNT_TIMEOUT_SECONDS = 7
|
| 24 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 25 |
KOTAK_ACTIVITY_LOG_PATH = DATA_DIR / "kotak_activity_log.txt"
|
| 26 |
+
NIFTY50_QUOTE_CACHE_SECONDS = 1.0
|
| 27 |
|
| 28 |
|
| 29 |
class KotakNeoError(Exception):
|
|
|
|
| 135 |
self.activity_log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 136 |
self._seen_activity_keys: set[str] = set()
|
| 137 |
self._scrip_cache: dict[str, list[dict[str, str]]] = {}
|
| 138 |
+
self._quote_cache_lock = threading.Lock()
|
| 139 |
+
self._nifty50_quote_cache: dict[str, Any] | None = None
|
| 140 |
+
self._nifty50_quote_cached_at = 0.0
|
| 141 |
self._load_existing_activity_keys()
|
| 142 |
self._clear_session_locked()
|
| 143 |
|
|
|
|
| 381 |
"quotes": list(quote_map.values()),
|
| 382 |
}
|
| 383 |
|
| 384 |
+
def fetch_nifty50_quote(self, *, max_age_seconds: float = NIFTY50_QUOTE_CACHE_SECONDS) -> dict[str, Any]:
|
| 385 |
+
if not self._configured():
|
| 386 |
+
raise KotakNeoConfigError("Kotak Neo environment variables are incomplete.")
|
| 387 |
+
|
| 388 |
+
with self._lock:
|
| 389 |
+
context = self._context_locked()
|
| 390 |
+
cached = self._nifty50_quote_cache
|
| 391 |
+
cached_at = self._nifty50_quote_cached_at
|
| 392 |
+
cache_age = time.monotonic() - cached_at
|
| 393 |
+
if cached and cache_age < max_age_seconds:
|
| 394 |
+
return {
|
| 395 |
+
**cached,
|
| 396 |
+
"cache": {"hit": True, "max_age_seconds": max_age_seconds, "age_seconds": round(cache_age, 3)},
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
with self._quote_cache_lock:
|
| 400 |
+
with self._lock:
|
| 401 |
+
context = self._context_locked()
|
| 402 |
+
cached = self._nifty50_quote_cache
|
| 403 |
+
cached_at = self._nifty50_quote_cached_at
|
| 404 |
+
cache_age = time.monotonic() - cached_at
|
| 405 |
+
if cached and cache_age < max_age_seconds:
|
| 406 |
+
return {
|
| 407 |
+
**cached,
|
| 408 |
+
"cache": {"hit": True, "max_age_seconds": max_age_seconds, "age_seconds": round(cache_age, 3)},
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
fresh_quote = self._fetch_nifty50_quote_with_context(context)
|
| 412 |
+
with self._lock:
|
| 413 |
+
self._nifty50_quote_cache = fresh_quote
|
| 414 |
+
self._nifty50_quote_cached_at = time.monotonic()
|
| 415 |
+
return {
|
| 416 |
+
**fresh_quote,
|
| 417 |
+
"cache": {"hit": False, "max_age_seconds": max_age_seconds, "age_seconds": 0.0},
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
def _ensure_authenticated_locked(self) -> None:
|
| 421 |
if not self.edit_token or not self.edit_sid or not self.base_url:
|
| 422 |
raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
|
|
|
|
| 532 |
timeout=DEFAULT_TIMEOUT_SECONDS,
|
| 533 |
)
|
| 534 |
|
| 535 |
+
def _fetch_nifty50_quote_with_context(self, context: dict[str, str]) -> dict[str, Any]:
|
| 536 |
+
candidates = self._nifty50_quote_candidates(context)
|
| 537 |
+
errors: list[str] = []
|
| 538 |
+
|
| 539 |
+
for candidate in candidates:
|
| 540 |
+
try:
|
| 541 |
+
payload = self._fetch_quotes_with_context(
|
| 542 |
+
context,
|
| 543 |
+
[
|
| 544 |
+
{
|
| 545 |
+
"exchange_segment": candidate["exchange_segment"],
|
| 546 |
+
"instrument_token": candidate["instrument_token"],
|
| 547 |
+
}
|
| 548 |
+
],
|
| 549 |
+
timeout=ACCOUNT_TIMEOUT_SECONDS,
|
| 550 |
+
)
|
| 551 |
+
items = _extract_items(payload)
|
| 552 |
+
if not items:
|
| 553 |
+
errors.append(
|
| 554 |
+
f"{candidate['lookup_source']}: quotes returned no items for {candidate['instrument_token']}"
|
| 555 |
+
)
|
| 556 |
+
continue
|
| 557 |
+
normalized = self._normalize_nifty50_quote(items[0], candidate)
|
| 558 |
+
if normalized.get("price") is None:
|
| 559 |
+
errors.append(
|
| 560 |
+
f"{candidate['lookup_source']}: quote payload did not include a usable last traded price"
|
| 561 |
+
)
|
| 562 |
+
continue
|
| 563 |
+
return normalized
|
| 564 |
+
except KotakNeoError as exc:
|
| 565 |
+
errors.append(f"{candidate['lookup_source']}: {exc}")
|
| 566 |
+
|
| 567 |
+
error_message = "Unable to fetch the NIFTY 50 quote from Kotak Neo."
|
| 568 |
+
if errors:
|
| 569 |
+
error_message = f"{error_message} " + " | ".join(errors)
|
| 570 |
+
raise KotakNeoError(error_message)
|
| 571 |
+
|
| 572 |
+
def _nifty50_quote_candidates(self, context: dict[str, str]) -> list[dict[str, Any]]:
|
| 573 |
+
candidates: list[dict[str, Any]] = []
|
| 574 |
+
master_candidate = self._resolve_nifty50_master_candidate(context)
|
| 575 |
+
if master_candidate:
|
| 576 |
+
candidates.append(master_candidate)
|
| 577 |
+
candidates.append(
|
| 578 |
+
{
|
| 579 |
+
"exchange_segment": "nse_cm",
|
| 580 |
+
"instrument_token": "Nifty 50",
|
| 581 |
+
"display_name": "NIFTY 50",
|
| 582 |
+
"lookup_source": "quotes_doc_fallback",
|
| 583 |
+
"master_match": None,
|
| 584 |
+
}
|
| 585 |
+
)
|
| 586 |
+
return candidates
|
| 587 |
+
|
| 588 |
+
def _resolve_nifty50_master_candidate(self, context: dict[str, str]) -> dict[str, Any] | None:
|
| 589 |
+
candidates = self._load_scrip_candidates(context, "nse_cm")
|
| 590 |
+
for item in candidates:
|
| 591 |
+
normalized_values = {
|
| 592 |
+
self._normalize_scrip_text(item.get("pSymbolName")),
|
| 593 |
+
self._normalize_scrip_text(item.get("pTrdSymbol")),
|
| 594 |
+
self._normalize_scrip_text(item.get("pDesc")),
|
| 595 |
+
self._normalize_scrip_text(item.get("pCombinedSymbol")),
|
| 596 |
+
self._normalize_scrip_text(item.get("pScripRefKey")),
|
| 597 |
+
}
|
| 598 |
+
normalized_values.discard("")
|
| 599 |
+
if not normalized_values:
|
| 600 |
+
continue
|
| 601 |
+
if "NIFTY50" not in normalized_values and not any(
|
| 602 |
+
value.startswith("NIFTY50") or "NIFTY50INDEX" in value for value in normalized_values
|
| 603 |
+
):
|
| 604 |
+
continue
|
| 605 |
+
instrument_token = _first_text(
|
| 606 |
+
item.get("pSymbol"),
|
| 607 |
+
item.get("pTrdSymbol"),
|
| 608 |
+
item.get("pSymbolName"),
|
| 609 |
+
)
|
| 610 |
+
if not instrument_token:
|
| 611 |
+
continue
|
| 612 |
+
return {
|
| 613 |
+
"exchange_segment": "nse_cm",
|
| 614 |
+
"instrument_token": instrument_token,
|
| 615 |
+
"display_name": _first_text(item.get("pDesc"), item.get("pSymbolName"), item.get("pTrdSymbol"))
|
| 616 |
+
or "NIFTY 50",
|
| 617 |
+
"lookup_source": "masterscrip",
|
| 618 |
+
"master_match": {
|
| 619 |
+
"pSymbol": item.get("pSymbol"),
|
| 620 |
+
"pSymbolName": item.get("pSymbolName"),
|
| 621 |
+
"pTrdSymbol": item.get("pTrdSymbol"),
|
| 622 |
+
"pDesc": item.get("pDesc"),
|
| 623 |
+
},
|
| 624 |
+
}
|
| 625 |
+
return None
|
| 626 |
+
|
| 627 |
+
def _normalize_scrip_text(self, value: Any) -> str:
|
| 628 |
+
return "".join(ch for ch in str(value or "").upper() if ch.isalnum())
|
| 629 |
+
|
| 630 |
+
def _normalize_nifty50_quote(
|
| 631 |
+
self,
|
| 632 |
+
item: dict[str, Any],
|
| 633 |
+
candidate: dict[str, Any],
|
| 634 |
+
) -> dict[str, Any]:
|
| 635 |
+
price = _first_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv"))
|
| 636 |
+
previous_close = _first_number(item.get("close"), item.get("c"), item.get("ic"))
|
| 637 |
+
change = _first_number(item.get("change"), item.get("cng"))
|
| 638 |
+
if change is None and price is not None and previous_close is not None:
|
| 639 |
+
change = price - previous_close
|
| 640 |
+
|
| 641 |
+
change_pct = None
|
| 642 |
+
if change is not None and previous_close not in (None, 0):
|
| 643 |
+
change_pct = change / previous_close
|
| 644 |
+
if change_pct is None:
|
| 645 |
+
change_pct = _first_number(item.get("net_change_percentage"), item.get("nc"))
|
| 646 |
+
|
| 647 |
+
return {
|
| 648 |
+
"symbol": "NIFTY 50",
|
| 649 |
+
"display_name": candidate.get("display_name") or "NIFTY 50",
|
| 650 |
+
"exchange_segment": candidate.get("exchange_segment") or _first_text(item.get("exchange_segment"), item.get("e")) or "nse_cm",
|
| 651 |
+
"instrument_token": candidate.get("instrument_token") or _first_text(item.get("instrument_token"), item.get("tk")),
|
| 652 |
+
"price": price,
|
| 653 |
+
"previous_close": previous_close,
|
| 654 |
+
"change": change,
|
| 655 |
+
"change_pct": change_pct,
|
| 656 |
+
"quote_time": _first_text(
|
| 657 |
+
item.get("updRecvTm"),
|
| 658 |
+
item.get("hsUpTm"),
|
| 659 |
+
item.get("flDtTm"),
|
| 660 |
+
item.get("exTm"),
|
| 661 |
+
item.get("ltt"),
|
| 662 |
+
),
|
| 663 |
+
"fetched_at": _utc_now_iso(),
|
| 664 |
+
"source": {
|
| 665 |
+
"provider": "Kotak Neo Quotes API",
|
| 666 |
+
"lookup_source": candidate.get("lookup_source"),
|
| 667 |
+
"master_match": candidate.get("master_match"),
|
| 668 |
+
},
|
| 669 |
+
"raw": item,
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
def _fetch_quotes_with_context(
|
| 673 |
self,
|
| 674 |
context: dict[str, str],
|
models/latest_prediction.csv
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
input_date,first5_start,first5_end,prediction,prob_up,confidence,threshold,model_name,is_overridden
|
| 2 |
-
2026-05-
|
|
|
|
| 1 |
input_date,first5_start,first5_end,prediction,prob_up,confidence,threshold,model_name,is_overridden
|
| 2 |
+
2026-05-27,2026-05-27 09:15:00,2026-05-27 09:19:00,UP,0.6970470349448922,0.7720470349448922,0.425,blend_extra_trees_tight_logit_overlay,False
|
models/tplus1_latest_prediction.csv
CHANGED
|
@@ -1,2 +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-
|
|
|
|
| 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-27,2026-05-29,next trading session after 2026-05-27,UP,0.47376564177714775,0.5262343582228522,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/yahoo_history_cache.sqlite3
CHANGED
|
Binary files a/models/yahoo_history_cache.sqlite3 and b/models/yahoo_history_cache.sqlite3 differ
|
|
|