Jitendra12421 commited on
Commit
d220c08
·
verified ·
1 Parent(s): 4e35faa

Upload 42 files

Browse files
__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
@@ -2,6 +2,7 @@ from __future__ import annotations
2
 
3
  import os
4
  import threading
 
5
  from concurrent.futures import ThreadPoolExecutor, as_completed
6
  from datetime import datetime, timezone
7
  from pathlib import Path
@@ -131,6 +132,7 @@ class KotakNeoManager:
131
  self.activity_log_path = KOTAK_ACTIVITY_LOG_PATH
132
  self.activity_log_path.parent.mkdir(parents=True, exist_ok=True)
133
  self._seen_activity_keys: set[str] = set()
 
134
  self._load_existing_activity_keys()
135
  self._clear_session_locked()
136
 
@@ -291,11 +293,19 @@ class KotakNeoManager:
291
  trades = sorted(_extract_items(results["trades"]), key=_sort_key, reverse=True)
292
  orders = sorted(_extract_items(results["orders"]), key=_sort_key, reverse=True)
293
 
 
 
 
294
  quotes = self._safe_account_call(
295
  "quotes",
296
  lambda: self._fetch_quotes_with_context(
297
  context,
298
- self._instrument_tokens_for_quotes(holdings, positions),
 
 
 
 
 
299
  timeout=ACCOUNT_TIMEOUT_SECONDS,
300
  )
301
  ,
@@ -305,8 +315,6 @@ class KotakNeoManager:
305
 
306
  normalized_holdings = [self._normalize_holding(item, quote_map) for item in holdings]
307
  normalized_positions = [self._normalize_position(item, quote_map) for item in positions]
308
- normalized_trades = [self._normalize_trade(item) for item in trades]
309
- normalized_orders = [self._normalize_order(item) for item in orders]
310
  self._append_activity_entries(normalized_trades, normalized_orders)
311
  journal = self._read_activity_journal()
312
  merged_trades = self._merge_activity(normalized_trades, journal["trades"])
@@ -337,6 +345,10 @@ class KotakNeoManager:
337
  return {
338
  "status": self.status(),
339
  "as_of": _utc_now_iso(),
 
 
 
 
340
  "summary": {
341
  "available_cash": available_cash,
342
  "current_capital": current_capital,
@@ -559,6 +571,8 @@ class KotakNeoManager:
559
  ]
560
  if any(marker in text for marker in empty_markers):
561
  return True
 
 
562
  if label in {"holdings", "positions", "trades", "orders"} and "424" in text:
563
  return True
564
  return False
@@ -594,8 +608,10 @@ class KotakNeoManager:
594
 
595
  def _instrument_tokens_for_quotes(
596
  self,
 
597
  holdings: list[dict[str, Any]],
598
  positions: list[dict[str, Any]],
 
599
  ) -> list[dict[str, str]]:
600
  unique: dict[tuple[str, str], dict[str, str]] = {}
601
 
@@ -616,9 +632,84 @@ class KotakNeoManager:
616
  "exchange_segment": exchange,
617
  "instrument_token": token,
618
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
  return list(unique.values())
621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  def _build_quote_map(self, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
623
  items = _extract_items(payload)
624
  quote_map: dict[str, dict[str, Any]] = {}
@@ -736,6 +827,7 @@ class KotakNeoManager:
736
  "updated_at": _first_text(item.get("hsUpTm"), item.get("exTm"), item.get("flDtTm")),
737
  }
738
 
 
739
  def _normalize_trade(self, item: dict[str, Any]) -> dict[str, Any]:
740
  return {
741
  "activity_type": "trade",
 
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
8
  from pathlib import Path
 
132
  self.activity_log_path = KOTAK_ACTIVITY_LOG_PATH
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
 
 
293
  trades = sorted(_extract_items(results["trades"]), key=_sort_key, reverse=True)
294
  orders = sorted(_extract_items(results["orders"]), key=_sort_key, reverse=True)
295
 
296
+ normalized_trades = [self._normalize_trade(item) for item in trades]
297
+ normalized_orders = [self._normalize_order(item) for item in orders]
298
+
299
  quotes = self._safe_account_call(
300
  "quotes",
301
  lambda: self._fetch_quotes_with_context(
302
  context,
303
+ self._instrument_tokens_for_quotes(
304
+ context,
305
+ holdings,
306
+ positions,
307
+ normalized_trades,
308
+ ),
309
  timeout=ACCOUNT_TIMEOUT_SECONDS,
310
  )
311
  ,
 
315
 
316
  normalized_holdings = [self._normalize_holding(item, quote_map) for item in holdings]
317
  normalized_positions = [self._normalize_position(item, quote_map) for item in positions]
 
 
318
  self._append_activity_entries(normalized_trades, normalized_orders)
319
  journal = self._read_activity_journal()
320
  merged_trades = self._merge_activity(normalized_trades, journal["trades"])
 
345
  return {
346
  "status": self.status(),
347
  "as_of": _utc_now_iso(),
348
+ "neo_behavior": {
349
+ "holdings_note": "Kotak Neo shows CNC delivery buys in Positions on trade day and in Holdings/T1 from the next trading day.",
350
+ "trade_history_note": "Kotak Neo trade history availability is limited by Neo's own order and portfolio tracker behavior.",
351
+ },
352
  "summary": {
353
  "available_cash": available_cash,
354
  "current_capital": current_capital,
 
571
  ]
572
  if any(marker in text for marker in empty_markers):
573
  return True
574
+ if label in {"positions", "trades", "orders"} and text.strip() == "kotak neo rejected the request.":
575
+ return True
576
  if label in {"holdings", "positions", "trades", "orders"} and "424" in text:
577
  return True
578
  return False
 
608
 
609
  def _instrument_tokens_for_quotes(
610
  self,
611
+ context: dict[str, str],
612
  holdings: list[dict[str, Any]],
613
  positions: list[dict[str, Any]],
614
+ trades: list[dict[str, Any]] | None = None,
615
  ) -> list[dict[str, str]]:
616
  unique: dict[tuple[str, str], dict[str, str]] = {}
617
 
 
632
  "exchange_segment": exchange,
633
  "instrument_token": token,
634
  }
635
+ elif exchange:
636
+ resolved = self._resolve_symbol_token(
637
+ context,
638
+ exchange_segment=exchange,
639
+ symbol=_first_text(item.get("trdSym"), item.get("sym")),
640
+ )
641
+ if resolved:
642
+ unique[(exchange, resolved)] = {
643
+ "exchange_segment": exchange,
644
+ "instrument_token": resolved,
645
+ }
646
+
647
+ for item in trades or []:
648
+ exchange = _first_text(item.get("exchange_segment"), item.get("exSeg"))
649
+ if not exchange:
650
+ continue
651
+ resolved = self._resolve_symbol_token(
652
+ context,
653
+ exchange_segment=exchange,
654
+ symbol=_first_text(item.get("trading_symbol"), item.get("trdSym"), item.get("symbol"), item.get("sym")),
655
+ )
656
+ if resolved:
657
+ unique[(exchange, resolved)] = {
658
+ "exchange_segment": exchange,
659
+ "instrument_token": resolved,
660
+ }
661
 
662
  return list(unique.values())
663
 
664
+ def _resolve_symbol_token(
665
+ self,
666
+ context: dict[str, str],
667
+ *,
668
+ exchange_segment: str,
669
+ symbol: str | None,
670
+ ) -> str | None:
671
+ symbol = str(symbol or "").strip()
672
+ if not symbol:
673
+ return None
674
+ candidates = self._load_scrip_candidates(context, exchange_segment)
675
+ symbol_upper = symbol.upper()
676
+ base_symbol_upper = symbol_upper.split("-")[0]
677
+ for item in candidates:
678
+ trading_symbol = str(item.get("pTrdSymbol") or "").upper()
679
+ symbol_name = str(item.get("pSymbolName") or "").upper()
680
+ token = str(item.get("pSymbol") or "").strip()
681
+ if not token:
682
+ continue
683
+ if trading_symbol == symbol_upper or symbol_name == base_symbol_upper:
684
+ return token
685
+ return None
686
+
687
+ def _load_scrip_candidates(self, context: dict[str, str], exchange_segment: str) -> list[dict[str, str]]:
688
+ key = str(exchange_segment).lower()
689
+ if key in self._scrip_cache:
690
+ return self._scrip_cache[key]
691
+
692
+ response = requests.get(
693
+ f"{context['base_url'].rstrip('/')}/script-details/1.0/masterscrip/file-paths",
694
+ headers={
695
+ "Authorization": context["consumer_key"],
696
+ "Accept": "application/json",
697
+ },
698
+ timeout=ACCOUNT_TIMEOUT_SECONDS,
699
+ )
700
+ data = self._decode_response(response)
701
+ file_paths = ((data.get("data") or {}).get("filesPaths") or []) if isinstance(data, dict) else []
702
+ csv_url = next((path for path in file_paths if key in str(path).lower()), None)
703
+ if not csv_url:
704
+ self._scrip_cache[key] = []
705
+ return []
706
+
707
+ csv_response = requests.get(csv_url, timeout=ACCOUNT_TIMEOUT_SECONDS)
708
+ csv_response.raise_for_status()
709
+ rows = list(DictReader(csv_response.text.splitlines()))
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]] = {}
 
827
  "updated_at": _first_text(item.get("hsUpTm"), item.get("exTm"), item.get("flDtTm")),
828
  }
829
 
830
+
831
  def _normalize_trade(self, item: dict[str, Any]) -> dict[str, Any]:
832
  return {
833
  "activity_type": "trade",