Upload 42 files
Browse files- __pycache__/kotak_neo.cpython-311.pyc +0 -0
- kotak_neo.py +97 -24
__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
|
|
|
kotak_neo.py
CHANGED
|
@@ -5,6 +5,7 @@ import threading
|
|
| 5 |
from csv import DictReader
|
| 6 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 7 |
from datetime import date, datetime, time, timezone
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
from time import monotonic
|
| 10 |
from typing import Any
|
|
@@ -16,6 +17,11 @@ import json
|
|
| 16 |
import pandas as pd
|
| 17 |
import requests
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
SESSION_BASE_URL = "https://mis.kotaksecurities.com"
|
| 21 |
QUOTE_PATH_TEMPLATE = "script-details/1.0/quotes/neosymbol/{neo_symbols}/{quote_type}"
|
|
@@ -28,6 +34,8 @@ KOTAK_ACTIVITY_LOG_PATH = DATA_DIR / "kotak_activity_log.txt"
|
|
| 28 |
NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
|
| 29 |
NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
|
| 30 |
IST = ZoneInfo("Asia/Kolkata")
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
class KotakNeoError(Exception):
|
|
@@ -85,6 +93,50 @@ def _normalize_frame_dates(frame: pd.DataFrame) -> pd.Series:
|
|
| 85 |
return values
|
| 86 |
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
def _first_text(*values: Any) -> str | None:
|
| 89 |
for value in values:
|
| 90 |
if isinstance(value, dict):
|
|
@@ -432,7 +484,10 @@ class KotakNeoManager:
|
|
| 432 |
item = items[0]
|
| 433 |
now_ist = datetime.now(IST)
|
| 434 |
last_traded_price = _first_market_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv"))
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
| 436 |
close = live_stats["previous_close"]
|
| 437 |
change_base = live_stats["return_base"]
|
| 438 |
change = None
|
|
@@ -442,7 +497,7 @@ class KotakNeoManager:
|
|
| 442 |
change_pct = (change / change_base) * 100.0
|
| 443 |
high = live_stats["range_high"]
|
| 444 |
low = live_stats["range_low"]
|
| 445 |
-
open_price =
|
| 446 |
|
| 447 |
payload = {
|
| 448 |
"symbol": "NIFTY 50",
|
|
@@ -464,6 +519,7 @@ class KotakNeoManager:
|
|
| 464 |
"low": low,
|
| 465 |
"return_basis": live_stats["return_basis"],
|
| 466 |
"market_open": live_stats["market_open"],
|
|
|
|
| 467 |
"exchange_feed_time": _first_text(item.get("tvalue"), item.get("updRecvTm"), item.get("hsUpTm")),
|
| 468 |
"as_of": _utc_now_iso(),
|
| 469 |
"source": {
|
|
@@ -483,46 +539,62 @@ class KotakNeoManager:
|
|
| 483 |
}
|
| 484 |
return payload
|
| 485 |
|
| 486 |
-
def _load_nifty50_reference_stats(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
today = now_ist.date()
|
| 488 |
-
|
|
|
|
| 489 |
|
| 490 |
-
daily =
|
| 491 |
-
daily["date"] = pd.to_datetime(daily["date"], errors="coerce").dt.date
|
| 492 |
-
daily = daily.dropna(subset=["date"]).sort_values("date")
|
| 493 |
|
| 494 |
previous_close = None
|
|
|
|
|
|
|
| 495 |
today_daily = daily[daily["date"] == today]
|
| 496 |
previous_daily = daily[daily["date"] < today]
|
| 497 |
if not previous_daily.empty:
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
today_open = _to_float(today_daily.iloc[-1]["open"]) if not today_daily.empty else None
|
| 501 |
today_high = _to_float(today_daily.iloc[-1]["high"]) if not today_daily.empty else None
|
| 502 |
today_low = _to_float(today_daily.iloc[-1]["low"]) if not today_daily.empty else None
|
| 503 |
today_close = _to_float(today_daily.iloc[-1]["close"]) if not today_daily.empty else None
|
| 504 |
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
range_high = max([value for value in [today_high, last_traded_price] if value is not None], default=None)
|
| 520 |
range_low = min([value for value in [today_low, last_traded_price] if value is not None], default=None)
|
| 521 |
return_base = today_open
|
| 522 |
return_basis = "open"
|
| 523 |
else:
|
| 524 |
-
range_high =
|
| 525 |
-
range_low =
|
| 526 |
return_base = previous_close
|
| 527 |
return_basis = "previous_close"
|
| 528 |
if last_traded_price is None:
|
|
@@ -535,6 +607,7 @@ class KotakNeoManager:
|
|
| 535 |
"range_high": range_high,
|
| 536 |
"range_low": range_low,
|
| 537 |
"market_open": market_open,
|
|
|
|
| 538 |
}
|
| 539 |
|
| 540 |
def _ensure_authenticated_locked(self) -> None:
|
|
|
|
| 5 |
from csv import DictReader
|
| 6 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 7 |
from datetime import date, datetime, time, timezone
|
| 8 |
+
from functools import lru_cache
|
| 9 |
from pathlib import Path
|
| 10 |
from time import monotonic
|
| 11 |
from typing import Any
|
|
|
|
| 17 |
import pandas as pd
|
| 18 |
import requests
|
| 19 |
|
| 20 |
+
try:
|
| 21 |
+
import pandas_market_calendars as mcal
|
| 22 |
+
except Exception: # pragma: no cover - deployed environments may fall back to weekdays
|
| 23 |
+
mcal = None
|
| 24 |
+
|
| 25 |
|
| 26 |
SESSION_BASE_URL = "https://mis.kotaksecurities.com"
|
| 27 |
QUOTE_PATH_TEMPLATE = "script-details/1.0/quotes/neosymbol/{neo_symbols}/{quote_type}"
|
|
|
|
| 34 |
NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
|
| 35 |
NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
|
| 36 |
IST = ZoneInfo("Asia/Kolkata")
|
| 37 |
+
MARKET_OPEN_TIME = time(9, 15)
|
| 38 |
+
MARKET_CLOSE_TIME = time(15, 30)
|
| 39 |
|
| 40 |
|
| 41 |
class KotakNeoError(Exception):
|
|
|
|
| 93 |
return values
|
| 94 |
|
| 95 |
|
| 96 |
+
@lru_cache(maxsize=1)
|
| 97 |
+
def _nse_calendar():
|
| 98 |
+
if mcal is None:
|
| 99 |
+
return None
|
| 100 |
+
for name in ("XNSE", "NSE", "BSE"):
|
| 101 |
+
try:
|
| 102 |
+
return mcal.get_calendar(name)
|
| 103 |
+
except Exception:
|
| 104 |
+
continue
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@lru_cache(maxsize=64)
|
| 109 |
+
def _is_nse_trading_day(day: date) -> bool:
|
| 110 |
+
calendar = _nse_calendar()
|
| 111 |
+
if calendar is None:
|
| 112 |
+
return day.weekday() < 5
|
| 113 |
+
return not calendar.schedule(start_date=day, end_date=day).empty
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _file_version(path: Path) -> tuple[str, int | None, int | None]:
|
| 117 |
+
try:
|
| 118 |
+
stat = path.stat()
|
| 119 |
+
return (str(path), stat.st_mtime_ns, stat.st_size)
|
| 120 |
+
except OSError:
|
| 121 |
+
return (str(path), None, None)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@lru_cache(maxsize=4)
|
| 125 |
+
def _load_nifty_daily_frame(file_version: tuple[str, int | None, int | None]) -> pd.DataFrame:
|
| 126 |
+
path = Path(file_version[0])
|
| 127 |
+
daily = pd.read_parquet(path, columns=["date", "open", "high", "low", "close"]).copy()
|
| 128 |
+
daily["date"] = pd.to_datetime(daily["date"], errors="coerce").dt.date
|
| 129 |
+
return daily.dropna(subset=["date"]).sort_values("date")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@lru_cache(maxsize=4)
|
| 133 |
+
def _load_nifty_minute_frame(file_version: tuple[str, int | None, int | None]) -> pd.DataFrame:
|
| 134 |
+
path = Path(file_version[0])
|
| 135 |
+
minute = pd.read_parquet(path, columns=["date", "open", "high", "low", "close"]).copy()
|
| 136 |
+
minute["date"] = _normalize_frame_dates(minute)
|
| 137 |
+
return minute.dropna(subset=["date"]).sort_values("date")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
def _first_text(*values: Any) -> str | None:
|
| 141 |
for value in values:
|
| 142 |
if isinstance(value, dict):
|
|
|
|
| 484 |
item = items[0]
|
| 485 |
now_ist = datetime.now(IST)
|
| 486 |
last_traded_price = _first_market_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv"))
|
| 487 |
+
quote_open = _first_market_number(item.get("openingPrice"), item.get("open"), item.get("o"))
|
| 488 |
+
quote_high = _first_market_number(item.get("high"), item.get("highPrice"), item.get("h"))
|
| 489 |
+
quote_low = _first_market_number(item.get("low"), item.get("lowPrice"), item.get("l"))
|
| 490 |
+
live_stats = self._load_nifty50_reference_stats(now_ist, last_traded_price, quote_open, quote_high, quote_low)
|
| 491 |
close = live_stats["previous_close"]
|
| 492 |
change_base = live_stats["return_base"]
|
| 493 |
change = None
|
|
|
|
| 497 |
change_pct = (change / change_base) * 100.0
|
| 498 |
high = live_stats["range_high"]
|
| 499 |
low = live_stats["range_low"]
|
| 500 |
+
open_price = quote_open
|
| 501 |
|
| 502 |
payload = {
|
| 503 |
"symbol": "NIFTY 50",
|
|
|
|
| 519 |
"low": low,
|
| 520 |
"return_basis": live_stats["return_basis"],
|
| 521 |
"market_open": live_stats["market_open"],
|
| 522 |
+
"is_trading_session": live_stats["is_trading_session"],
|
| 523 |
"exchange_feed_time": _first_text(item.get("tvalue"), item.get("updRecvTm"), item.get("hsUpTm")),
|
| 524 |
"as_of": _utc_now_iso(),
|
| 525 |
"source": {
|
|
|
|
| 539 |
}
|
| 540 |
return payload
|
| 541 |
|
| 542 |
+
def _load_nifty50_reference_stats(
|
| 543 |
+
self,
|
| 544 |
+
now_ist: datetime,
|
| 545 |
+
last_traded_price: float | None,
|
| 546 |
+
live_open: float | None = None,
|
| 547 |
+
live_high: float | None = None,
|
| 548 |
+
live_low: float | None = None,
|
| 549 |
+
) -> dict[str, Any]:
|
| 550 |
today = now_ist.date()
|
| 551 |
+
is_trading_session = _is_nse_trading_day(today)
|
| 552 |
+
market_open = is_trading_session and MARKET_OPEN_TIME <= now_ist.time() < MARKET_CLOSE_TIME
|
| 553 |
|
| 554 |
+
daily = _load_nifty_daily_frame(_file_version(NIFTY_1D_PATH))
|
|
|
|
|
|
|
| 555 |
|
| 556 |
previous_close = None
|
| 557 |
+
previous_high = None
|
| 558 |
+
previous_low = None
|
| 559 |
today_daily = daily[daily["date"] == today]
|
| 560 |
previous_daily = daily[daily["date"] < today]
|
| 561 |
if not previous_daily.empty:
|
| 562 |
+
previous_row = previous_daily.iloc[-1]
|
| 563 |
+
previous_close = _to_float(previous_row["close"])
|
| 564 |
+
previous_high = _to_float(previous_row["high"])
|
| 565 |
+
previous_low = _to_float(previous_row["low"])
|
| 566 |
|
| 567 |
today_open = _to_float(today_daily.iloc[-1]["open"]) if not today_daily.empty else None
|
| 568 |
today_high = _to_float(today_daily.iloc[-1]["high"]) if not today_daily.empty else None
|
| 569 |
today_low = _to_float(today_daily.iloc[-1]["low"]) if not today_daily.empty else None
|
| 570 |
today_close = _to_float(today_daily.iloc[-1]["close"]) if not today_daily.empty else None
|
| 571 |
|
| 572 |
+
session_started = is_trading_session and now_ist.time() >= MARKET_OPEN_TIME
|
| 573 |
+
|
| 574 |
+
if session_started:
|
| 575 |
+
today_open = live_open or today_open
|
| 576 |
+
today_high = live_high or today_high
|
| 577 |
+
today_low = live_low or today_low
|
| 578 |
+
|
| 579 |
+
if today_open is None or today_high is None or today_low is None:
|
| 580 |
+
minute = _load_nifty_minute_frame(_file_version(NIFTY_1M_PATH))
|
| 581 |
+
today_minute = minute[minute["date"].dt.date == today]
|
| 582 |
+
if not today_minute.empty:
|
| 583 |
+
today_open = _to_float(today_minute.iloc[0]["open"]) or today_open
|
| 584 |
+
minute_high = pd.to_numeric(today_minute["high"], errors="coerce").max()
|
| 585 |
+
minute_low = pd.to_numeric(today_minute["low"], errors="coerce").min()
|
| 586 |
+
today_high = _to_float(minute_high) or today_high
|
| 587 |
+
today_low = _to_float(minute_low) or today_low
|
| 588 |
+
today_close = _to_float(today_minute.iloc[-1]["close"]) or today_close
|
| 589 |
+
|
| 590 |
+
if session_started:
|
| 591 |
range_high = max([value for value in [today_high, last_traded_price] if value is not None], default=None)
|
| 592 |
range_low = min([value for value in [today_low, last_traded_price] if value is not None], default=None)
|
| 593 |
return_base = today_open
|
| 594 |
return_basis = "open"
|
| 595 |
else:
|
| 596 |
+
range_high = previous_high
|
| 597 |
+
range_low = previous_low
|
| 598 |
return_base = previous_close
|
| 599 |
return_basis = "previous_close"
|
| 600 |
if last_traded_price is None:
|
|
|
|
| 607 |
"range_high": range_high,
|
| 608 |
"range_low": range_low,
|
| 609 |
"market_open": market_open,
|
| 610 |
+
"is_trading_session": is_trading_session,
|
| 611 |
}
|
| 612 |
|
| 613 |
def _ensure_authenticated_locked(self) -> None:
|