Upload 42 files
Browse files- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/kotak_neo.cpython-311.pyc +0 -0
- app.py +12 -0
- kotak_neo.py +91 -0
__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/quote/nifty50")
|
| 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:
|
kotak_neo.py
CHANGED
|
@@ -6,6 +6,7 @@ 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 typing import Any
|
| 10 |
from urllib.parse import quote
|
| 11 |
|
|
@@ -133,6 +134,7 @@ 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 +378,67 @@ 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.")
|
|
@@ -710,6 +773,34 @@ class KotakNeoManager:
|
|
| 710 |
self._scrip_cache[key] = rows
|
| 711 |
return rows
|
| 712 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
def _build_quote_map(self, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
| 714 |
items = _extract_items(payload)
|
| 715 |
quote_map: dict[str, dict[str, Any]] = {}
|
|
|
|
| 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 |
|
|
|
|
| 134 |
self.activity_log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 135 |
self._seen_activity_keys: set[str] = set()
|
| 136 |
self._scrip_cache: dict[str, list[dict[str, str]]] = {}
|
| 137 |
+
self._quote_cache: dict[str, dict[str, Any]] = {}
|
| 138 |
self._load_existing_activity_keys()
|
| 139 |
self._clear_session_locked()
|
| 140 |
|
|
|
|
| 378 |
"quotes": list(quote_map.values()),
|
| 379 |
}
|
| 380 |
|
| 381 |
+
def fetch_nifty50_quote(self, *, force_refresh: bool = False) -> dict[str, Any]:
|
| 382 |
+
cache_key = "nifty50_quote"
|
| 383 |
+
with self._lock:
|
| 384 |
+
if not force_refresh:
|
| 385 |
+
cached = self._quote_cache.get(cache_key)
|
| 386 |
+
if cached and (monotonic() - float(cached.get("stored_at_monotonic") or 0.0)) < 1.0:
|
| 387 |
+
return dict(cached["payload"])
|
| 388 |
+
context = self._context_locked()
|
| 389 |
+
|
| 390 |
+
reference = self._resolve_nifty50_reference(context)
|
| 391 |
+
quote_payload = self._fetch_quotes_with_context(
|
| 392 |
+
context,
|
| 393 |
+
[
|
| 394 |
+
{
|
| 395 |
+
"exchange_segment": "nse_cm",
|
| 396 |
+
"instrument_token": str(reference["quote_instrument_token"]),
|
| 397 |
+
}
|
| 398 |
+
],
|
| 399 |
+
timeout=ACCOUNT_TIMEOUT_SECONDS,
|
| 400 |
+
)
|
| 401 |
+
items = _extract_items(quote_payload)
|
| 402 |
+
if not items:
|
| 403 |
+
raise KotakNeoError("Kotak Neo did not return a NIFTY 50 quote.")
|
| 404 |
+
|
| 405 |
+
item = items[0]
|
| 406 |
+
payload = {
|
| 407 |
+
"symbol": "NIFTY 50",
|
| 408 |
+
"exchange_segment": "nse_cm",
|
| 409 |
+
"instrument_token": _first_text(
|
| 410 |
+
item.get("instrument_token"),
|
| 411 |
+
item.get("instrumentToken"),
|
| 412 |
+
item.get("tk"),
|
| 413 |
+
reference.get("master_instrument_token"),
|
| 414 |
+
reference["quote_instrument_token"],
|
| 415 |
+
),
|
| 416 |
+
"display_name": _first_text(item.get("trading_symbol"), item.get("ts"), item.get("name"), "NIFTY 50"),
|
| 417 |
+
"last_traded_price": _first_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv")),
|
| 418 |
+
"close": _first_number(item.get("close"), item.get("c"), item.get("ic")),
|
| 419 |
+
"change": _first_number(item.get("change"), item.get("cng")),
|
| 420 |
+
"change_pct": _first_number(item.get("net_change_percentage"), item.get("nc")),
|
| 421 |
+
"open": _first_number(item.get("openingPrice"), item.get("open"), item.get("o")),
|
| 422 |
+
"high": _first_number(item.get("highPrice"), item.get("high"), item.get("h")),
|
| 423 |
+
"low": _first_number(item.get("lowPrice"), item.get("low"), item.get("l")),
|
| 424 |
+
"exchange_feed_time": _first_text(item.get("tvalue"), item.get("updRecvTm"), item.get("hsUpTm")),
|
| 425 |
+
"as_of": _utc_now_iso(),
|
| 426 |
+
"source": {
|
| 427 |
+
"quote_api": QUOTE_PATH_TEMPLATE,
|
| 428 |
+
"master_scrip_verified": bool(reference.get("master_record_found")),
|
| 429 |
+
"instrument_lookup": reference.get("lookup_mode"),
|
| 430 |
+
"master_symbol_name": reference.get("master_symbol_name"),
|
| 431 |
+
"master_trading_symbol": reference.get("master_trading_symbol"),
|
| 432 |
+
},
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
with self._lock:
|
| 436 |
+
self._quote_cache[cache_key] = {
|
| 437 |
+
"stored_at_monotonic": monotonic(),
|
| 438 |
+
"payload": payload,
|
| 439 |
+
}
|
| 440 |
+
return payload
|
| 441 |
+
|
| 442 |
def _ensure_authenticated_locked(self) -> None:
|
| 443 |
if not self.edit_token or not self.edit_sid or not self.base_url:
|
| 444 |
raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
|
|
|
|
| 773 |
self._scrip_cache[key] = rows
|
| 774 |
return rows
|
| 775 |
|
| 776 |
+
def _resolve_nifty50_reference(self, context: dict[str, str]) -> dict[str, Any]:
|
| 777 |
+
candidates = self._load_scrip_candidates(context, "nse_cm")
|
| 778 |
+
match = None
|
| 779 |
+
aliases = {
|
| 780 |
+
"NIFTY 50",
|
| 781 |
+
"NIFTY50",
|
| 782 |
+
"NIFTY 50 INDEX",
|
| 783 |
+
"NIFTY",
|
| 784 |
+
}
|
| 785 |
+
for item in candidates:
|
| 786 |
+
candidate_values = {
|
| 787 |
+
str(item.get("pSymbolName") or "").strip().upper(),
|
| 788 |
+
str(item.get("pTrdSymbol") or "").strip().upper(),
|
| 789 |
+
str(item.get("pSymbol") or "").strip().upper(),
|
| 790 |
+
}
|
| 791 |
+
if aliases & candidate_values:
|
| 792 |
+
match = item
|
| 793 |
+
break
|
| 794 |
+
|
| 795 |
+
return {
|
| 796 |
+
"quote_instrument_token": "Nifty 50",
|
| 797 |
+
"lookup_mode": "index-name-direct" if match is None else "master-scrip-verified-index-name-direct",
|
| 798 |
+
"master_record_found": match is not None,
|
| 799 |
+
"master_instrument_token": _first_text(match.get("pSymbol")) if match else None,
|
| 800 |
+
"master_symbol_name": _first_text(match.get("pSymbolName")) if match else None,
|
| 801 |
+
"master_trading_symbol": _first_text(match.get("pTrdSymbol")) if match else None,
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
def _build_quote_map(self, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
| 805 |
items = _extract_items(payload)
|
| 806 |
quote_map: dict[str, dict[str, Any]] = {}
|