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

Upload 42 files

Browse files
__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
@@ -450,6 +450,23 @@ def kotak_account() -> dict:
450
  raise HTTPException(status_code=401, detail=str(exc)) from exc
451
  except KotakNeoError as exc:
452
  raise HTTPException(status_code=502, detail=str(exc)) from exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
 
455
  @app.get("/cron/keepalive")
 
450
  raise HTTPException(status_code=401, detail=str(exc)) from exc
451
  except KotakNeoError as exc:
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:
458
+ snapshot = kotak_neo_manager.fetch_account_snapshot()
459
+ return {
460
+ "activity_log": snapshot.get("activity_log", {}),
461
+ "trade_history": snapshot.get("trade_history", []),
462
+ "order_book": snapshot.get("order_book", []),
463
+ }
464
+ except KotakNeoConfigError as exc:
465
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
466
+ except KotakNeoSessionRequired as exc:
467
+ raise HTTPException(status_code=401, detail=str(exc)) from exc
468
+ except KotakNeoError as exc:
469
+ raise HTTPException(status_code=502, detail=str(exc)) from exc
470
 
471
 
472
  @app.get("/cron/keepalive")
kotak_neo.py CHANGED
@@ -4,6 +4,7 @@ import os
4
  import threading
5
  from concurrent.futures import ThreadPoolExecutor, as_completed
6
  from datetime import datetime, timezone
 
7
  from typing import Any
8
  from urllib.parse import quote
9
 
@@ -18,6 +19,8 @@ TOTP_LOGIN_PATH = "login/1.0/tradeApiLogin"
18
  TOTP_VALIDATE_PATH = "login/1.0/tradeApiValidate"
19
  DEFAULT_TIMEOUT_SECONDS = 20
20
  ACCOUNT_TIMEOUT_SECONDS = 7
 
 
21
 
22
 
23
  class KotakNeoError(Exception):
@@ -125,8 +128,29 @@ class KotakNeoManager:
125
  self.neo_fin_key = os.getenv("KOTAK_NEO_FIN_KEY", "neotradeapi")
126
 
127
  self._lock = threading.RLock()
 
 
 
 
128
  self._clear_session_locked()
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  def _clear_session_locked(self) -> None:
131
  self.view_token: str | None = None
132
  self.sid: str | None = None
@@ -281,6 +305,12 @@ class KotakNeoManager:
281
 
282
  normalized_holdings = [self._normalize_holding(item, quote_map) for item in holdings]
283
  normalized_positions = [self._normalize_position(item, quote_map) for item in positions]
 
 
 
 
 
 
284
 
285
  holdings_market_value = sum(item["market_value"] or 0.0 for item in normalized_holdings)
286
  holdings_cost = sum(item["cost_value"] or 0.0 for item in normalized_holdings)
@@ -317,15 +347,20 @@ class KotakNeoManager:
317
  "live_pnl": holdings_pnl + positions_pnl,
318
  "open_positions": sum(1 for item in normalized_positions if item["net_quantity"]),
319
  "holdings_count": len(normalized_holdings),
320
- "orders_count": len(orders),
321
- "trades_count": len(trades),
322
  },
323
  "limits_summary": limits_summary,
324
  "limits_raw": limits_raw,
325
  "holdings": normalized_holdings,
326
  "positions": normalized_positions,
327
- "trade_history": trades[:50],
328
- "order_book": orders[:50],
 
 
 
 
 
329
  "quotes": list(quote_map.values()),
330
  }
331
 
@@ -701,5 +736,123 @@ class KotakNeoManager:
701
  "updated_at": _first_text(item.get("hsUpTm"), item.get("exTm"), item.get("flDtTm")),
702
  }
703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
 
705
  kotak_neo_manager = KotakNeoManager()
 
4
  import threading
5
  from concurrent.futures import ThreadPoolExecutor, as_completed
6
  from datetime import datetime, timezone
7
+ from pathlib import Path
8
  from typing import Any
9
  from urllib.parse import quote
10
 
 
19
  TOTP_VALIDATE_PATH = "login/1.0/tradeApiValidate"
20
  DEFAULT_TIMEOUT_SECONDS = 20
21
  ACCOUNT_TIMEOUT_SECONDS = 7
22
+ DATA_DIR = Path(__file__).resolve().parent / "data"
23
+ KOTAK_ACTIVITY_LOG_PATH = DATA_DIR / "kotak_activity_log.txt"
24
 
25
 
26
  class KotakNeoError(Exception):
 
128
  self.neo_fin_key = os.getenv("KOTAK_NEO_FIN_KEY", "neotradeapi")
129
 
130
  self._lock = threading.RLock()
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
 
137
+ def _load_existing_activity_keys(self) -> None:
138
+ if not self.activity_log_path.exists():
139
+ return
140
+ try:
141
+ for line in self.activity_log_path.read_text(encoding="utf-8").splitlines():
142
+ if not line.strip():
143
+ continue
144
+ try:
145
+ payload = json.loads(line)
146
+ except json.JSONDecodeError:
147
+ continue
148
+ key = str(payload.get("activity_key") or "").strip()
149
+ if key:
150
+ self._seen_activity_keys.add(key)
151
+ except Exception:
152
+ pass
153
+
154
  def _clear_session_locked(self) -> None:
155
  self.view_token: str | None = None
156
  self.sid: str | None = None
 
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"])
313
+ merged_orders = self._merge_activity(normalized_orders, journal["orders"])
314
 
315
  holdings_market_value = sum(item["market_value"] or 0.0 for item in normalized_holdings)
316
  holdings_cost = sum(item["cost_value"] or 0.0 for item in normalized_holdings)
 
347
  "live_pnl": holdings_pnl + positions_pnl,
348
  "open_positions": sum(1 for item in normalized_positions if item["net_quantity"]),
349
  "holdings_count": len(normalized_holdings),
350
+ "orders_count": len(merged_orders),
351
+ "trades_count": len(merged_trades),
352
  },
353
  "limits_summary": limits_summary,
354
  "limits_raw": limits_raw,
355
  "holdings": normalized_holdings,
356
  "positions": normalized_positions,
357
+ "trade_history": merged_trades[:100],
358
+ "order_book": merged_orders[:100],
359
+ "activity_log": {
360
+ "path": str(self.activity_log_path),
361
+ "trades_count": len(journal["trades"]),
362
+ "orders_count": len(journal["orders"]),
363
+ },
364
  "quotes": list(quote_map.values()),
365
  }
366
 
 
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",
742
+ "activity_key": self._trade_key(item),
743
+ "order_no": _first_text(item.get("nOrdNo")),
744
+ "trade_id": _first_text(item.get("flId")),
745
+ "exchange_order_id": _first_text(item.get("exOrdId")),
746
+ "symbol": _first_text(item.get("sym"), item.get("trdSym")),
747
+ "trading_symbol": _first_text(item.get("trdSym"), item.get("sym")),
748
+ "exchange_segment": _first_text(item.get("exSeg")),
749
+ "transaction_type": _first_text(item.get("trnsTp")),
750
+ "product": _first_text(item.get("prod")),
751
+ "quantity": _first_number(item.get("fldQty"), item.get("qty")),
752
+ "price": _first_number(item.get("avgPrc"), item.get("prc")),
753
+ "average_price": _first_number(item.get("avgPrc")),
754
+ "status": _first_text(item.get("rptTp"), item.get("ordSt"), item.get("stat")),
755
+ "trade_time": _first_text(item.get("flDtTm"), item.get("exTm"), item.get("flTm"), item.get("flDt")),
756
+ "raw": item,
757
+ }
758
+
759
+ def _normalize_order(self, item: dict[str, Any]) -> dict[str, Any]:
760
+ return {
761
+ "activity_type": "order",
762
+ "activity_key": self._order_key(item),
763
+ "order_no": _first_text(item.get("nOrdNo")),
764
+ "request_id": _first_text(item.get("reqId"), item.get("nReqId")),
765
+ "exchange_order_id": _first_text(item.get("exOrdId")),
766
+ "symbol": _first_text(item.get("sym"), item.get("trdSym")),
767
+ "trading_symbol": _first_text(item.get("trdSym"), item.get("sym")),
768
+ "exchange_segment": _first_text(item.get("exSeg")),
769
+ "transaction_type": _first_text(item.get("trnsTp")),
770
+ "product": _first_text(item.get("prod")),
771
+ "quantity": _first_number(item.get("qty")),
772
+ "filled_quantity": _first_number(item.get("fldQty")),
773
+ "unfilled_size": _first_number(item.get("unFldSz")),
774
+ "price": _first_number(item.get("prc")),
775
+ "trigger_price": _first_number(item.get("trgPrc")),
776
+ "order_type": _first_text(item.get("prcTp")),
777
+ "status": _first_text(item.get("ordSt"), item.get("stat")),
778
+ "order_time": _first_text(item.get("ordDtTm"), item.get("exCfmTm"), item.get("hsUpTm")),
779
+ "rejection_reason": _first_text(item.get("rejRsn")),
780
+ "raw": item,
781
+ }
782
+
783
+ def _trade_key(self, item: dict[str, Any]) -> str:
784
+ return "|".join(
785
+ [
786
+ "trade",
787
+ str(_first_text(item.get("nOrdNo")) or ""),
788
+ str(_first_text(item.get("flId")) or ""),
789
+ str(_first_text(item.get("flDtTm"), item.get("exTm"), item.get("flTm")) or ""),
790
+ ]
791
+ )
792
+
793
+ def _order_key(self, item: dict[str, Any]) -> str:
794
+ return "|".join(
795
+ [
796
+ "order",
797
+ str(_first_text(item.get("nOrdNo")) or ""),
798
+ str(_first_text(item.get("ordSt"), item.get("stat")) or ""),
799
+ str(_first_text(item.get("ordDtTm"), item.get("exCfmTm"), item.get("hsUpTm")) or ""),
800
+ ]
801
+ )
802
+
803
+ def _append_activity_entries(self, trades: list[dict[str, Any]], orders: list[dict[str, Any]]) -> None:
804
+ lines: list[str] = []
805
+ for entry in [*trades, *orders]:
806
+ key = str(entry.get("activity_key") or "").strip()
807
+ if not key or key in self._seen_activity_keys:
808
+ continue
809
+ payload = {
810
+ "activity_key": key,
811
+ "activity_type": entry.get("activity_type"),
812
+ "captured_at": _utc_now_iso(),
813
+ **entry,
814
+ }
815
+ lines.append(json.dumps(payload, ensure_ascii=True))
816
+ self._seen_activity_keys.add(key)
817
+ if not lines:
818
+ return
819
+ with self.activity_log_path.open("a", encoding="utf-8") as handle:
820
+ handle.write("\n".join(lines) + "\n")
821
+
822
+ def _read_activity_journal(self) -> dict[str, list[dict[str, Any]]]:
823
+ trades: list[dict[str, Any]] = []
824
+ orders: list[dict[str, Any]] = []
825
+ if not self.activity_log_path.exists():
826
+ return {"trades": trades, "orders": orders}
827
+ for line in self.activity_log_path.read_text(encoding="utf-8").splitlines():
828
+ if not line.strip():
829
+ continue
830
+ try:
831
+ item = json.loads(line)
832
+ except json.JSONDecodeError:
833
+ continue
834
+ if item.get("activity_type") == "trade":
835
+ trades.append(item)
836
+ elif item.get("activity_type") == "order":
837
+ orders.append(item)
838
+ trades.sort(key=lambda item: str(item.get("trade_time") or item.get("captured_at") or ""), reverse=True)
839
+ orders.sort(key=lambda item: str(item.get("order_time") or item.get("captured_at") or ""), reverse=True)
840
+ return {"trades": trades, "orders": orders}
841
+
842
+ def _merge_activity(
843
+ self,
844
+ live_entries: list[dict[str, Any]],
845
+ journal_entries: list[dict[str, Any]],
846
+ ) -> list[dict[str, Any]]:
847
+ merged: list[dict[str, Any]] = []
848
+ seen: set[str] = set()
849
+ for entry in [*live_entries, *journal_entries]:
850
+ key = str(entry.get("activity_key") or "").strip()
851
+ if not key or key in seen:
852
+ continue
853
+ merged.append(entry)
854
+ seen.add(key)
855
+ return merged
856
+
857
 
858
  kotak_neo_manager = KotakNeoManager()