Upload 42 files
Browse files- __pycache__/kotak_neo.cpython-311.pyc +0 -0
- kotak_neo.py +84 -7
__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
|
@@ -4,14 +4,16 @@ import os
|
|
| 4 |
import threading
|
| 5 |
from csv import DictReader
|
| 6 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 7 |
-
from datetime import datetime, timezone
|
| 8 |
from pathlib import Path
|
| 9 |
from time import monotonic
|
| 10 |
from typing import Any
|
| 11 |
from urllib.parse import quote
|
|
|
|
| 12 |
|
| 13 |
import json
|
| 14 |
|
|
|
|
| 15 |
import requests
|
| 16 |
|
| 17 |
|
|
@@ -23,6 +25,9 @@ DEFAULT_TIMEOUT_SECONDS = 20
|
|
| 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 |
|
| 27 |
|
| 28 |
class KotakNeoError(Exception):
|
|
@@ -71,6 +76,15 @@ def _first_market_number(*values: Any) -> float | None:
|
|
| 71 |
return fallback
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def _first_text(*values: Any) -> str | None:
|
| 75 |
for value in values:
|
| 76 |
if isinstance(value, dict):
|
|
@@ -396,7 +410,7 @@ class KotakNeoManager:
|
|
| 396 |
with self._lock:
|
| 397 |
if not force_refresh:
|
| 398 |
cached = self._quote_cache.get(cache_key)
|
| 399 |
-
if cached and (monotonic() - float(cached.get("stored_at_monotonic") or 0.0)) <
|
| 400 |
return dict(cached["payload"])
|
| 401 |
context = self._context_locked()
|
| 402 |
|
|
@@ -416,12 +430,18 @@ class KotakNeoManager:
|
|
| 416 |
raise KotakNeoError("Kotak Neo did not return a NIFTY 50 quote.")
|
| 417 |
|
| 418 |
item = items[0]
|
|
|
|
| 419 |
last_traded_price = _first_market_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv"))
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
open_price = _first_market_number(item.get("openingPrice"), item.get("open"), item.get("o"))
|
| 426 |
|
| 427 |
payload = {
|
|
@@ -442,6 +462,8 @@ class KotakNeoManager:
|
|
| 442 |
"open": open_price,
|
| 443 |
"high": high,
|
| 444 |
"low": low,
|
|
|
|
|
|
|
| 445 |
"exchange_feed_time": _first_text(item.get("tvalue"), item.get("updRecvTm"), item.get("hsUpTm")),
|
| 446 |
"as_of": _utc_now_iso(),
|
| 447 |
"source": {
|
|
@@ -450,6 +472,7 @@ class KotakNeoManager:
|
|
| 450 |
"instrument_lookup": reference.get("lookup_mode"),
|
| 451 |
"master_symbol_name": reference.get("master_symbol_name"),
|
| 452 |
"master_trading_symbol": reference.get("master_trading_symbol"),
|
|
|
|
| 453 |
},
|
| 454 |
}
|
| 455 |
|
|
@@ -460,6 +483,60 @@ class KotakNeoManager:
|
|
| 460 |
}
|
| 461 |
return payload
|
| 462 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
def _ensure_authenticated_locked(self) -> None:
|
| 464 |
if not self.edit_token or not self.edit_sid or not self.base_url:
|
| 465 |
raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
|
|
|
|
| 4 |
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
|
| 11 |
from urllib.parse import quote
|
| 12 |
+
from zoneinfo import ZoneInfo
|
| 13 |
|
| 14 |
import json
|
| 15 |
|
| 16 |
+
import pandas as pd
|
| 17 |
import requests
|
| 18 |
|
| 19 |
|
|
|
|
| 25 |
ACCOUNT_TIMEOUT_SECONDS = 7
|
| 26 |
DATA_DIR = Path(__file__).resolve().parent / "data"
|
| 27 |
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):
|
|
|
|
| 76 |
return fallback
|
| 77 |
|
| 78 |
|
| 79 |
+
def _normalize_frame_dates(frame: pd.DataFrame) -> pd.Series:
|
| 80 |
+
values = pd.to_datetime(frame["date"], errors="coerce")
|
| 81 |
+
if getattr(values.dt, "tz", None) is None:
|
| 82 |
+
values = values.dt.tz_localize(IST)
|
| 83 |
+
else:
|
| 84 |
+
values = values.dt.tz_convert(IST)
|
| 85 |
+
return values
|
| 86 |
+
|
| 87 |
+
|
| 88 |
def _first_text(*values: Any) -> str | None:
|
| 89 |
for value in values:
|
| 90 |
if isinstance(value, dict):
|
|
|
|
| 410 |
with self._lock:
|
| 411 |
if not force_refresh:
|
| 412 |
cached = self._quote_cache.get(cache_key)
|
| 413 |
+
if cached and (monotonic() - float(cached.get("stored_at_monotonic") or 0.0)) < 0.25:
|
| 414 |
return dict(cached["payload"])
|
| 415 |
context = self._context_locked()
|
| 416 |
|
|
|
|
| 430 |
raise KotakNeoError("Kotak Neo did not return a NIFTY 50 quote.")
|
| 431 |
|
| 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 |
+
live_stats = self._load_nifty50_reference_stats(now_ist, last_traded_price)
|
| 436 |
+
close = live_stats["previous_close"]
|
| 437 |
+
change_base = live_stats["return_base"]
|
| 438 |
+
change = None
|
| 439 |
+
change_pct = None
|
| 440 |
+
if last_traded_price is not None and change_base not in (None, 0):
|
| 441 |
+
change = last_traded_price - change_base
|
| 442 |
+
change_pct = (change / change_base) * 100.0
|
| 443 |
+
high = live_stats["range_high"]
|
| 444 |
+
low = live_stats["range_low"]
|
| 445 |
open_price = _first_market_number(item.get("openingPrice"), item.get("open"), item.get("o"))
|
| 446 |
|
| 447 |
payload = {
|
|
|
|
| 462 |
"open": open_price,
|
| 463 |
"high": high,
|
| 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": {
|
|
|
|
| 472 |
"instrument_lookup": reference.get("lookup_mode"),
|
| 473 |
"master_symbol_name": reference.get("master_symbol_name"),
|
| 474 |
"master_trading_symbol": reference.get("master_trading_symbol"),
|
| 475 |
+
"reference_data": "backend/data/nifty50_1m.parquet + backend/data/nifty50_1d.parquet",
|
| 476 |
},
|
| 477 |
}
|
| 478 |
|
|
|
|
| 483 |
}
|
| 484 |
return payload
|
| 485 |
|
| 486 |
+
def _load_nifty50_reference_stats(self, now_ist: datetime, last_traded_price: float | None) -> dict[str, Any]:
|
| 487 |
+
today = now_ist.date()
|
| 488 |
+
market_open = time(9, 15) <= now_ist.time() < time(15, 30)
|
| 489 |
+
|
| 490 |
+
daily = pd.read_parquet(NIFTY_1D_PATH, columns=["date", "open", "high", "low", "close"]).copy()
|
| 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 |
+
previous_close = _to_float(previous_daily.iloc[-1]["close"])
|
| 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 |
+
minute = pd.read_parquet(NIFTY_1M_PATH, columns=["date", "open", "high", "low", "close"]).copy()
|
| 506 |
+
minute["date"] = _normalize_frame_dates(minute)
|
| 507 |
+
minute = minute.dropna(subset=["date"]).sort_values("date")
|
| 508 |
+
today_minute = minute[minute["date"].dt.date == today]
|
| 509 |
+
|
| 510 |
+
if not today_minute.empty:
|
| 511 |
+
today_open = _to_float(today_minute.iloc[0]["open"]) or today_open
|
| 512 |
+
minute_high = pd.to_numeric(today_minute["high"], errors="coerce").max()
|
| 513 |
+
minute_low = pd.to_numeric(today_minute["low"], errors="coerce").min()
|
| 514 |
+
today_high = _to_float(minute_high) or today_high
|
| 515 |
+
today_low = _to_float(minute_low) or today_low
|
| 516 |
+
today_close = _to_float(today_minute.iloc[-1]["close"]) or today_close
|
| 517 |
+
|
| 518 |
+
if market_open:
|
| 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 = today_high
|
| 525 |
+
range_low = today_low
|
| 526 |
+
return_base = previous_close
|
| 527 |
+
return_basis = "previous_close"
|
| 528 |
+
if last_traded_price is None:
|
| 529 |
+
last_traded_price = today_close
|
| 530 |
+
|
| 531 |
+
return {
|
| 532 |
+
"previous_close": previous_close,
|
| 533 |
+
"return_base": return_base,
|
| 534 |
+
"return_basis": return_basis,
|
| 535 |
+
"range_high": range_high,
|
| 536 |
+
"range_low": range_low,
|
| 537 |
+
"market_open": market_open,
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
def _ensure_authenticated_locked(self) -> None:
|
| 541 |
if not self.edit_token or not self.edit_sid or not self.base_url:
|
| 542 |
raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
|