Jitendra12421 commited on
Commit
83edf2f
·
verified ·
1 Parent(s): 14589e4

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 ADDED
Binary file (31 kB). View file
 
app.py CHANGED
@@ -7,9 +7,10 @@ from datetime import date, datetime, time, timedelta
7
  import sys
8
  from pathlib import Path
9
 
10
- from fastapi import BackgroundTasks, Query
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from fastapi import FastAPI
 
13
 
14
  sys.path.insert(0, str(Path(__file__).resolve().parent))
15
  from nifty_backend.runtime import (
@@ -31,9 +32,15 @@ from nifty_backend.runtime import (
31
  seconds_until_next_ist_run,
32
  warm_dashboard_payload_cache,
33
  )
 
 
 
 
 
 
34
 
35
 
36
- app = FastAPI(title="NIFTY 50 Forecaster Backend")
37
  app.add_middleware(
38
  CORSMiddleware,
39
  allow_origins=["*"],
@@ -49,6 +56,10 @@ tplus1_refresh_lock = threading.Lock()
49
  MARKET_OPEN = time(9, 15)
50
  FIRST5_READY = time(9, 20)
51
  MARKET_CLOSE = time(15, 30)
 
 
 
 
52
 
53
 
54
  def refresh_market_close_data_if_due() -> dict:
@@ -412,6 +423,33 @@ def root() -> dict[str, str]:
412
  @app.get("/dashboard")
413
  def dashboard() -> dict:
414
  return attach_market_state(dashboard_payload())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
 
417
  @app.get("/cron/keepalive")
 
7
  import sys
8
  from pathlib import Path
9
 
10
+ from fastapi import BackgroundTasks, HTTPException, Query
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi import FastAPI
13
+ from pydantic import BaseModel
14
 
15
  sys.path.insert(0, str(Path(__file__).resolve().parent))
16
  from nifty_backend.runtime import (
 
32
  seconds_until_next_ist_run,
33
  warm_dashboard_payload_cache,
34
  )
35
+ from kotak_neo import (
36
+ KotakNeoConfigError,
37
+ KotakNeoError,
38
+ KotakNeoSessionRequired,
39
+ kotak_neo_manager,
40
+ )
41
 
42
 
43
+ app = FastAPI(title="NIFTY 50 Forecaster Backend")
44
  app.add_middleware(
45
  CORSMiddleware,
46
  allow_origins=["*"],
 
56
  MARKET_OPEN = time(9, 15)
57
  FIRST5_READY = time(9, 20)
58
  MARKET_CLOSE = time(15, 30)
59
+
60
+
61
+ class TotpRequest(BaseModel):
62
+ totp: str
63
 
64
 
65
  def refresh_market_close_data_if_due() -> dict:
 
423
  @app.get("/dashboard")
424
  def dashboard() -> dict:
425
  return attach_market_state(dashboard_payload())
426
+
427
+
428
+ @app.get("/kotak/status")
429
+ def kotak_status() -> dict:
430
+ return kotak_neo_manager.status()
431
+
432
+
433
+ @app.post("/kotak/auth/totp")
434
+ def kotak_auth_totp(payload: TotpRequest) -> dict:
435
+ try:
436
+ return kotak_neo_manager.authenticate_with_totp(payload.totp)
437
+ except KotakNeoConfigError as exc:
438
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
439
+ except KotakNeoError as exc:
440
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
441
+
442
+
443
+ @app.get("/kotak/account")
444
+ def kotak_account() -> dict:
445
+ try:
446
+ return kotak_neo_manager.fetch_account_snapshot()
447
+ except KotakNeoConfigError as exc:
448
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
449
+ except KotakNeoSessionRequired as exc:
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")
kotak_neo.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import threading
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+ from urllib.parse import quote
8
+
9
+ import requests
10
+
11
+
12
+ SESSION_BASE_URL = "https://mis.kotaksecurities.com"
13
+ QUOTE_PATH_TEMPLATE = "script-details/1.0/quotes/neosymbol/{neo_symbols}/{quote_type}"
14
+ TOTP_LOGIN_PATH = "login/1.0/tradeApiLogin"
15
+ TOTP_VALIDATE_PATH = "login/1.0/tradeApiValidate"
16
+ DEFAULT_TIMEOUT_SECONDS = 20
17
+
18
+
19
+ class KotakNeoError(Exception):
20
+ pass
21
+
22
+
23
+ class KotakNeoConfigError(KotakNeoError):
24
+ pass
25
+
26
+
27
+ class KotakNeoSessionRequired(KotakNeoError):
28
+ pass
29
+
30
+
31
+ def _utc_now_iso() -> str:
32
+ return datetime.now(timezone.utc).isoformat()
33
+
34
+
35
+ def _to_float(value: Any) -> float | None:
36
+ if value in (None, "", "--", "NA", "na", "-", "null"):
37
+ return None
38
+ try:
39
+ return float(value)
40
+ except (TypeError, ValueError):
41
+ return None
42
+
43
+
44
+ def _first_number(*values: Any) -> float | None:
45
+ for value in values:
46
+ parsed = _to_float(value)
47
+ if parsed is not None:
48
+ return parsed
49
+ return None
50
+
51
+
52
+ def _first_text(*values: Any) -> str | None:
53
+ for value in values:
54
+ if value not in (None, "", "--", "NA", "na", "-"):
55
+ return str(value)
56
+ return None
57
+
58
+
59
+ def _sum_numbers(*values: Any) -> float | None:
60
+ numbers: list[float] = []
61
+ for value in values:
62
+ parsed = _to_float(value)
63
+ if parsed is not None:
64
+ numbers.append(parsed)
65
+ return sum(numbers) if numbers else None
66
+
67
+
68
+ def _extract_items(payload: Any) -> list[dict[str, Any]]:
69
+ if isinstance(payload, list):
70
+ return [item for item in payload if isinstance(item, dict)]
71
+ if not isinstance(payload, dict):
72
+ return []
73
+
74
+ data = payload.get("data")
75
+ if isinstance(data, list):
76
+ return [item for item in data if isinstance(item, dict)]
77
+ if isinstance(data, dict):
78
+ nested = data.get("data")
79
+ if isinstance(nested, list):
80
+ return [item for item in nested if isinstance(item, dict)]
81
+ return [data]
82
+ return []
83
+
84
+
85
+ def _sort_key(item: dict[str, Any]) -> str:
86
+ return str(
87
+ _first_text(
88
+ item.get("updRecvTm"),
89
+ item.get("hsUpTm"),
90
+ item.get("flDtTm"),
91
+ item.get("exTm"),
92
+ item.get("ordDtTm"),
93
+ item.get("TimeStamp"),
94
+ item.get("flDt"),
95
+ )
96
+ or ""
97
+ )
98
+
99
+
100
+ class KotakNeoManager:
101
+ def __init__(self) -> None:
102
+ self.consumer_key = os.getenv("KOTAK_CONSUMER_KEY")
103
+ self.mobile_number = os.getenv("KOTAK_MOBILE_NUMBER")
104
+ self.ucc = os.getenv("KOTAK_UCC")
105
+ self.mpin = os.getenv("KOTAK_MPIN")
106
+ self.neo_fin_key = os.getenv("KOTAK_NEO_FIN_KEY", "neotradeapi")
107
+
108
+ self._lock = threading.RLock()
109
+ self._clear_session_locked()
110
+
111
+ def _clear_session_locked(self) -> None:
112
+ self.view_token: str | None = None
113
+ self.sid: str | None = None
114
+ self.edit_token: str | None = None
115
+ self.edit_sid: str | None = None
116
+ self.edit_rid: str | None = None
117
+ self.server_id: str | None = None
118
+ self.data_center: str | None = None
119
+ self.base_url: str | None = None
120
+ self.authenticated_at: str | None = None
121
+
122
+ def _configured(self) -> bool:
123
+ return all([self.consumer_key, self.mobile_number, self.ucc, self.mpin])
124
+
125
+ def status(self) -> dict[str, Any]:
126
+ configured = self._configured()
127
+ with self._lock:
128
+ authenticated = bool(self.edit_token and self.edit_sid and self.base_url)
129
+ return {
130
+ "available": configured,
131
+ "configured": configured,
132
+ "authenticated": authenticated,
133
+ "needs_totp": configured and not authenticated,
134
+ "last_authenticated_at": self.authenticated_at,
135
+ "reason": None if configured else "Kotak Neo environment variables are incomplete.",
136
+ }
137
+
138
+ def authenticate_with_totp(self, totp: str) -> dict[str, Any]:
139
+ if not self._configured():
140
+ raise KotakNeoConfigError("Kotak Neo environment variables are incomplete.")
141
+ if not str(totp).strip():
142
+ raise KotakNeoError("A TOTP code is required.")
143
+
144
+ with self._lock:
145
+ login_response = self._post_session_api(
146
+ TOTP_LOGIN_PATH,
147
+ headers={
148
+ "Authorization": self.consumer_key,
149
+ "neo-fin-key": self.neo_fin_key,
150
+ "Content-Type": "application/json",
151
+ "Accept": "application/json",
152
+ },
153
+ payload={
154
+ "mobileNumber": self.mobile_number,
155
+ "ucc": self.ucc,
156
+ "totp": str(totp).strip(),
157
+ },
158
+ )
159
+ login_data = (login_response.get("data") or {}) if isinstance(login_response, dict) else {}
160
+ self.view_token = login_data.get("token")
161
+ self.sid = login_data.get("sid")
162
+
163
+ if not self.view_token or not self.sid:
164
+ self._clear_session_locked()
165
+ raise KotakNeoError("Kotak Neo did not return a valid pre-auth session.")
166
+
167
+ validate_response = self._post_session_api(
168
+ TOTP_VALIDATE_PATH,
169
+ headers={
170
+ "Authorization": self.consumer_key,
171
+ "sid": self.sid,
172
+ "Auth": self.view_token,
173
+ "neo-fin-key": self.neo_fin_key,
174
+ "Content-Type": "application/json",
175
+ "Accept": "application/json",
176
+ },
177
+ payload={"mpin": self.mpin},
178
+ )
179
+ validate_data = (validate_response.get("data") or {}) if isinstance(validate_response, dict) else {}
180
+
181
+ self.edit_token = validate_data.get("token")
182
+ self.edit_sid = validate_data.get("sid")
183
+ self.edit_rid = validate_data.get("rid")
184
+ self.server_id = validate_data.get("hsServerId")
185
+ self.data_center = validate_data.get("dataCenter")
186
+ self.base_url = str(validate_data.get("baseUrl") or "").rstrip("/")
187
+ self.authenticated_at = _utc_now_iso()
188
+
189
+ if not self.edit_token or not self.edit_sid or not self.base_url:
190
+ self._clear_session_locked()
191
+ raise KotakNeoError("Kotak Neo did not return a usable trading session.")
192
+
193
+ return self.status()
194
+
195
+ def fetch_account_snapshot(self) -> dict[str, Any]:
196
+ if not self._configured():
197
+ raise KotakNeoConfigError("Kotak Neo environment variables are incomplete.")
198
+
199
+ with self._lock:
200
+ self._ensure_authenticated_locked()
201
+
202
+ holdings_raw = self._request_trading_api_locked("portfolio/v1/holdings")
203
+ positions_raw = self._request_trading_api_locked("quick/user/positions")
204
+ trades_raw = self._request_trading_api_locked("quick/user/trades")
205
+ orders_raw = self._request_trading_api_locked("quick/user/orders")
206
+ limits_raw = self._request_trading_api_locked("quick/user/limits")
207
+
208
+ holdings = _extract_items(holdings_raw)
209
+ positions = _extract_items(positions_raw)
210
+ trades = sorted(_extract_items(trades_raw), key=_sort_key, reverse=True)
211
+ orders = sorted(_extract_items(orders_raw), key=_sort_key, reverse=True)
212
+
213
+ quotes = self._fetch_quotes_locked(self._instrument_tokens_for_quotes(holdings, positions))
214
+ quote_map = self._build_quote_map(quotes)
215
+
216
+ normalized_holdings = [self._normalize_holding(item, quote_map) for item in holdings]
217
+ normalized_positions = [self._normalize_position(item, quote_map) for item in positions]
218
+
219
+ holdings_market_value = sum(item["market_value"] or 0.0 for item in normalized_holdings)
220
+ holdings_cost = sum(item["cost_value"] or 0.0 for item in normalized_holdings)
221
+ holdings_pnl = sum(item["pnl"] or 0.0 for item in normalized_holdings)
222
+ positions_pnl = sum(item["pnl"] or 0.0 for item in normalized_positions)
223
+
224
+ limits_summary = {
225
+ "net": _first_number(limits_raw.get("Net")) if isinstance(limits_raw, dict) else None,
226
+ "margin_used": _first_number(limits_raw.get("MarginUsed")) if isinstance(limits_raw, dict) else None,
227
+ "collateral_value": _first_number(limits_raw.get("CollateralValue")) if isinstance(limits_raw, dict) else None,
228
+ "cash_unrealized_mtm": _first_number(limits_raw.get("CashUnRlsMtomPrsnt")) if isinstance(limits_raw, dict) else None,
229
+ "cash_realized_mtm": _first_number(limits_raw.get("CashRlsMtomPrsnt")) if isinstance(limits_raw, dict) else None,
230
+ }
231
+
232
+ available_cash = None
233
+ if limits_summary["net"] is not None and limits_summary["margin_used"] is not None:
234
+ available_cash = limits_summary["net"] - limits_summary["margin_used"]
235
+
236
+ current_capital = None
237
+ if available_cash is not None:
238
+ current_capital = available_cash + holdings_market_value
239
+
240
+ return {
241
+ "status": self.status(),
242
+ "as_of": _utc_now_iso(),
243
+ "summary": {
244
+ "available_cash": available_cash,
245
+ "current_capital": current_capital,
246
+ "holdings_market_value": holdings_market_value,
247
+ "holdings_cost_value": holdings_cost,
248
+ "holdings_pnl": holdings_pnl,
249
+ "positions_pnl": positions_pnl,
250
+ "live_pnl": holdings_pnl + positions_pnl,
251
+ "open_positions": sum(1 for item in normalized_positions if item["net_quantity"]),
252
+ "holdings_count": len(normalized_holdings),
253
+ "orders_count": len(orders),
254
+ "trades_count": len(trades),
255
+ },
256
+ "limits_summary": limits_summary,
257
+ "limits_raw": limits_raw,
258
+ "holdings": normalized_holdings,
259
+ "positions": normalized_positions,
260
+ "trade_history": trades[:50],
261
+ "order_book": orders[:50],
262
+ "quotes": list(quote_map.values()),
263
+ }
264
+
265
+ def _ensure_authenticated_locked(self) -> None:
266
+ if not self.edit_token or not self.edit_sid or not self.base_url:
267
+ raise KotakNeoSessionRequired("Kotak Neo session is not authenticated.")
268
+
269
+ def _post_session_api(self, path: str, headers: dict[str, str], payload: dict[str, Any]) -> dict[str, Any]:
270
+ response = requests.post(
271
+ f"{SESSION_BASE_URL.rstrip('/')}/{path.lstrip('/')}",
272
+ headers=headers,
273
+ json=payload,
274
+ timeout=DEFAULT_TIMEOUT_SECONDS,
275
+ )
276
+ data = self._decode_response(response)
277
+ self._raise_for_error(response, data, session_sensitive=False)
278
+ return data
279
+
280
+ def _request_trading_api_locked(self, path: str) -> dict[str, Any]:
281
+ self._ensure_authenticated_locked()
282
+ response = requests.get(
283
+ f"{self.base_url.rstrip('/')}/{path.lstrip('/')}",
284
+ headers={
285
+ "Sid": self.edit_sid or "",
286
+ "Auth": self.edit_token or "",
287
+ "Accept": "application/json",
288
+ },
289
+ params={"sId": self.server_id or ""},
290
+ timeout=DEFAULT_TIMEOUT_SECONDS,
291
+ )
292
+ data = self._decode_response(response)
293
+ self._raise_for_error(response, data, session_sensitive=True)
294
+ return data
295
+
296
+ def _fetch_quotes_locked(self, instrument_tokens: list[dict[str, str]]) -> dict[str, Any]:
297
+ if not instrument_tokens:
298
+ return {"data": []}
299
+
300
+ neo_symbols = ",".join(
301
+ f"{item['exchange_segment']}|{item['instrument_token']}" for item in instrument_tokens
302
+ )
303
+ encoded_symbols = quote(neo_symbols, safe="")
304
+ response = requests.get(
305
+ f"{self.base_url.rstrip('/')}/{QUOTE_PATH_TEMPLATE.format(neo_symbols=encoded_symbols, quote_type='all')}",
306
+ headers={
307
+ "Authorization": self.consumer_key or "",
308
+ "Content-Type": "application/x-www-form-urlencoded",
309
+ "Accept": "application/json",
310
+ },
311
+ timeout=DEFAULT_TIMEOUT_SECONDS,
312
+ )
313
+ data = self._decode_response(response)
314
+ self._raise_for_error(response, data, session_sensitive=False)
315
+ return data
316
+
317
+ def _decode_response(self, response: requests.Response) -> dict[str, Any]:
318
+ try:
319
+ parsed = response.json()
320
+ except ValueError as exc:
321
+ raise KotakNeoError(
322
+ f"Kotak Neo returned a non-JSON response with HTTP {response.status_code}."
323
+ ) from exc
324
+ if isinstance(parsed, dict):
325
+ return parsed
326
+ return {"data": parsed}
327
+
328
+ def _raise_for_error(
329
+ self,
330
+ response: requests.Response,
331
+ data: dict[str, Any],
332
+ *,
333
+ session_sensitive: bool,
334
+ ) -> None:
335
+ stat = str(data.get("stat") or "").strip().lower()
336
+ st_code = _first_number(data.get("stCode"))
337
+ message = _first_text(
338
+ data.get("message"),
339
+ data.get("error"),
340
+ data.get("Error"),
341
+ data.get("emsg"),
342
+ )
343
+
344
+ if response.status_code == 403 or stat == "not_ok":
345
+ if session_sensitive:
346
+ self._clear_session_locked()
347
+ raise KotakNeoSessionRequired(message or "Kotak Neo session expired.")
348
+ raise KotakNeoError(message or "Kotak Neo rejected the request.")
349
+
350
+ if response.status_code >= 400 or (st_code is not None and st_code >= 400):
351
+ raise KotakNeoError(message or f"Kotak Neo request failed with HTTP {response.status_code}.")
352
+
353
+ def _instrument_tokens_for_quotes(
354
+ self,
355
+ holdings: list[dict[str, Any]],
356
+ positions: list[dict[str, Any]],
357
+ ) -> list[dict[str, str]]:
358
+ unique: dict[tuple[str, str], dict[str, str]] = {}
359
+
360
+ for item in holdings:
361
+ token = _first_text(item.get("instrumentToken"), item.get("exchangeIdentifier"), item.get("tok"))
362
+ exchange = _first_text(item.get("exchangeSegment"), item.get("exSeg"))
363
+ if token and exchange:
364
+ unique[(exchange, token)] = {
365
+ "exchange_segment": exchange,
366
+ "instrument_token": token,
367
+ }
368
+
369
+ for item in positions:
370
+ token = _first_text(item.get("tok"), item.get("instrumentToken"), item.get("exchangeIdentifier"))
371
+ exchange = _first_text(item.get("exSeg"), item.get("exchangeSegment"))
372
+ if token and exchange:
373
+ unique[(exchange, token)] = {
374
+ "exchange_segment": exchange,
375
+ "instrument_token": token,
376
+ }
377
+
378
+ return list(unique.values())
379
+
380
+ def _build_quote_map(self, payload: dict[str, Any]) -> dict[str, dict[str, Any]]:
381
+ items = _extract_items(payload)
382
+ quote_map: dict[str, dict[str, Any]] = {}
383
+ for item in items:
384
+ token = _first_text(item.get("instrument_token"), item.get("instrumentToken"), item.get("tk"))
385
+ exchange = _first_text(item.get("exchange_segment"), item.get("exchangeSegment"), item.get("e"))
386
+ if not token or not exchange:
387
+ continue
388
+ key = f"{exchange}|{token}"
389
+ quote_map[key] = {
390
+ "instrument_token": token,
391
+ "exchange_segment": exchange,
392
+ "trading_symbol": _first_text(item.get("trading_symbol"), item.get("ts"), item.get("symbol")),
393
+ "last_traded_price": _first_number(item.get("last_traded_price"), item.get("ltp"), item.get("iv")),
394
+ "close": _first_number(item.get("close"), item.get("c"), item.get("ic")),
395
+ "change": _first_number(item.get("change"), item.get("cng")),
396
+ "change_pct": _first_number(item.get("net_change_percentage"), item.get("nc")),
397
+ }
398
+ return quote_map
399
+
400
+ def _normalize_holding(
401
+ self,
402
+ item: dict[str, Any],
403
+ quote_map: dict[str, dict[str, Any]],
404
+ ) -> dict[str, Any]:
405
+ token = _first_text(item.get("instrumentToken"), item.get("exchangeIdentifier"), item.get("tok"))
406
+ exchange = _first_text(item.get("exchangeSegment"), item.get("exSeg"))
407
+ quote = quote_map.get(f"{exchange}|{token}", {}) if token and exchange else {}
408
+
409
+ quantity = _first_number(item.get("quantity"), item.get("sellableQuantity"))
410
+ average_price = _first_number(item.get("averagePrice"), item.get("avgPrc"))
411
+ holding_cost = _first_number(item.get("holdingCost"))
412
+ market_value = _first_number(item.get("mktValue"))
413
+ ltp = _first_number(quote.get("last_traded_price"))
414
+
415
+ if market_value is None and quantity is not None and ltp is not None:
416
+ market_value = quantity * ltp
417
+ if holding_cost is None and quantity is not None and average_price is not None:
418
+ holding_cost = quantity * average_price
419
+
420
+ pnl = None
421
+ pnl_pct = None
422
+ if market_value is not None and holding_cost is not None:
423
+ pnl = market_value - holding_cost
424
+ if holding_cost:
425
+ pnl_pct = pnl / holding_cost
426
+
427
+ return {
428
+ "symbol": _first_text(item.get("displaySymbol"), item.get("symbol"), item.get("trdSym")),
429
+ "trading_symbol": _first_text(item.get("symbol"), item.get("displaySymbol"), item.get("trdSym")),
430
+ "exchange_segment": exchange,
431
+ "instrument_token": token,
432
+ "quantity": quantity,
433
+ "sellable_quantity": _first_number(item.get("sellableQuantity")),
434
+ "average_price": average_price,
435
+ "last_traded_price": ltp,
436
+ "market_value": market_value,
437
+ "cost_value": holding_cost,
438
+ "pnl": pnl,
439
+ "pnl_pct": pnl_pct,
440
+ }
441
+
442
+ def _normalize_position(
443
+ self,
444
+ item: dict[str, Any],
445
+ quote_map: dict[str, dict[str, Any]],
446
+ ) -> dict[str, Any]:
447
+ token = _first_text(item.get("tok"), item.get("instrumentToken"), item.get("exchangeIdentifier"))
448
+ exchange = _first_text(item.get("exSeg"), item.get("exchangeSegment"))
449
+ quote = quote_map.get(f"{exchange}|{token}", {}) if token and exchange else {}
450
+
451
+ multiplier = _first_number(item.get("multiplier")) or 1.0
452
+ buy_qty = _first_number(
453
+ item.get("buyQty"),
454
+ _sum_numbers(item.get("cfBuyQty"), item.get("flBuyQty")),
455
+ )
456
+ sell_qty = _first_number(
457
+ item.get("sellQty"),
458
+ _sum_numbers(item.get("cfSellQty"), item.get("flSellQty")),
459
+ )
460
+ qty = _first_number(item.get("netQty"), item.get("qty"))
461
+
462
+ if qty is None and buy_qty is not None and sell_qty is not None:
463
+ qty = buy_qty - sell_qty
464
+ elif qty is None and buy_qty is not None and sell_qty is None:
465
+ qty = buy_qty
466
+ elif qty is None and sell_qty is not None:
467
+ qty = -sell_qty
468
+
469
+ average_price = _first_number(item.get("avgPrc"), item.get("averagePrice"))
470
+ ltp = _first_number(quote.get("last_traded_price"))
471
+
472
+ pnl = _first_number(
473
+ item.get("pnl"),
474
+ item.get("mtm"),
475
+ item.get("urmtom"),
476
+ item.get("unRealizedMtom"),
477
+ )
478
+ if pnl is None and qty is not None and average_price is not None and ltp is not None:
479
+ pnl = (ltp - average_price) * qty * multiplier
480
+
481
+ return {
482
+ "order_no": _first_text(item.get("nOrdNo")),
483
+ "symbol": _first_text(item.get("sym"), item.get("trdSym")),
484
+ "trading_symbol": _first_text(item.get("trdSym"), item.get("sym")),
485
+ "exchange_segment": exchange,
486
+ "instrument_token": token,
487
+ "product": _first_text(item.get("prod")),
488
+ "transaction_type": _first_text(item.get("trnsTp")),
489
+ "net_quantity": qty,
490
+ "average_price": average_price,
491
+ "last_traded_price": ltp,
492
+ "pnl": pnl,
493
+ "multiplier": multiplier,
494
+ "updated_at": _first_text(item.get("hsUpTm"), item.get("exTm"), item.get("flDtTm")),
495
+ }
496
+
497
+
498
+ kotak_neo_manager = KotakNeoManager()
scripts/__pycache__/retrain_opening_model.cpython-311.pyc ADDED
Binary file (11.2 kB). View file