Jitendra12421 commited on
Commit
50a604a
·
verified ·
1 Parent(s): 07cf176

Upload 2 files

Browse files
Files changed (1) hide show
  1. kotak_neo.py +218 -5
kotak_neo.py CHANGED
@@ -412,11 +412,15 @@ class KotakNeoManager:
412
  journal = self._read_activity_journal()
413
  merged_trades = self._merge_activity(normalized_trades, journal["trades"])
414
  merged_orders = self._merge_activity(normalized_orders, journal["orders"])
 
 
415
 
416
  holdings_market_value = sum(item["market_value"] or 0.0 for item in normalized_holdings)
417
  holdings_cost = sum(item["cost_value"] or 0.0 for item in normalized_holdings)
418
  holdings_pnl = sum(item["pnl"] or 0.0 for item in normalized_holdings)
419
- positions_pnl = sum(item["pnl"] or 0.0 for item in normalized_positions)
 
 
420
 
421
  limits_raw = results["limits"]
422
  limits_summary = {
@@ -449,8 +453,11 @@ class KotakNeoManager:
449
  "holdings_cost_value": holdings_cost,
450
  "holdings_pnl": holdings_pnl,
451
  "positions_pnl": positions_pnl,
 
 
452
  "live_pnl": holdings_pnl + positions_pnl,
453
- "open_positions": sum(1 for item in normalized_positions if item["net_quantity"]),
 
454
  "holdings_count": len(normalized_holdings),
455
  "orders_count": len(merged_orders),
456
  "trades_count": len(merged_trades),
@@ -458,7 +465,7 @@ class KotakNeoManager:
458
  "limits_summary": limits_summary,
459
  "limits_raw": limits_raw,
460
  "holdings": normalized_holdings,
461
- "positions": normalized_positions,
462
  "trade_history": merged_trades[:100],
463
  "order_book": merged_orders[:100],
464
  "activity_log": {
@@ -694,6 +701,7 @@ class KotakNeoManager:
694
  *,
695
  timeout: int,
696
  ) -> dict[str, Any]:
 
697
  response = requests.get(
698
  f"{context['base_url'].rstrip('/')}/{path.lstrip('/')}",
699
  headers={
@@ -701,7 +709,7 @@ class KotakNeoManager:
701
  "Auth": context["edit_token"],
702
  "Accept": "application/json",
703
  },
704
- params={"sId": context["server_id"]},
705
  timeout=timeout,
706
  )
707
  data = self._decode_response(response)
@@ -738,7 +746,7 @@ class KotakNeoManager:
738
  "Accept": "application/json",
739
  "Content-Type": content_type,
740
  }
741
- query_params = {"sId": context["server_id"]}
742
  url = f"{context['base_url'].rstrip('/')}/{path.lstrip('/')}"
743
 
744
  if content_type == "application/x-www-form-urlencoded":
@@ -1253,5 +1261,210 @@ class KotakNeoManager:
1253
  seen.add(key)
1254
  return merged
1255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
 
1257
  kotak_neo_manager = KotakNeoManager()
 
412
  journal = self._read_activity_journal()
413
  merged_trades = self._merge_activity(normalized_trades, journal["trades"])
414
  merged_orders = self._merge_activity(normalized_orders, journal["orders"])
415
+ derived_positions = self._derive_positions_from_trades(merged_trades, quote_map)
416
+ effective_positions = normalized_positions if self._positions_are_meaningful(normalized_positions) else derived_positions
417
 
418
  holdings_market_value = sum(item["market_value"] or 0.0 for item in normalized_holdings)
419
  holdings_cost = sum(item["cost_value"] or 0.0 for item in normalized_holdings)
420
  holdings_pnl = sum(item["pnl"] or 0.0 for item in normalized_holdings)
421
+ positions_pnl = sum(item["pnl"] or 0.0 for item in effective_positions)
422
+ positions_realized_pnl = sum(item.get("realized_pnl") or 0.0 for item in effective_positions)
423
+ positions_unrealized_pnl = sum(item.get("unrealized_pnl") or 0.0 for item in effective_positions)
424
 
425
  limits_raw = results["limits"]
426
  limits_summary = {
 
453
  "holdings_cost_value": holdings_cost,
454
  "holdings_pnl": holdings_pnl,
455
  "positions_pnl": positions_pnl,
456
+ "positions_realized_pnl": positions_realized_pnl,
457
+ "positions_unrealized_pnl": positions_unrealized_pnl,
458
  "live_pnl": holdings_pnl + positions_pnl,
459
+ "positions_count": len(effective_positions),
460
+ "open_positions": sum(1 for item in effective_positions if item["net_quantity"]),
461
  "holdings_count": len(normalized_holdings),
462
  "orders_count": len(merged_orders),
463
  "trades_count": len(merged_trades),
 
465
  "limits_summary": limits_summary,
466
  "limits_raw": limits_raw,
467
  "holdings": normalized_holdings,
468
+ "positions": effective_positions,
469
  "trade_history": merged_trades[:100],
470
  "order_book": merged_orders[:100],
471
  "activity_log": {
 
701
  *,
702
  timeout: int,
703
  ) -> dict[str, Any]:
704
+ query_params = {"sId": context["server_id"]} if context.get("server_id") else None
705
  response = requests.get(
706
  f"{context['base_url'].rstrip('/')}/{path.lstrip('/')}",
707
  headers={
 
709
  "Auth": context["edit_token"],
710
  "Accept": "application/json",
711
  },
712
+ params=query_params,
713
  timeout=timeout,
714
  )
715
  data = self._decode_response(response)
 
746
  "Accept": "application/json",
747
  "Content-Type": content_type,
748
  }
749
+ query_params = {"sId": context["server_id"]} if context.get("server_id") else None
750
  url = f"{context['base_url'].rstrip('/')}/{path.lstrip('/')}"
751
 
752
  if content_type == "application/x-www-form-urlencoded":
 
1261
  seen.add(key)
1262
  return merged
1263
 
1264
+ def _positions_are_meaningful(self, positions: list[dict[str, Any]]) -> bool:
1265
+ for item in positions:
1266
+ if abs(_first_number(item.get("net_quantity")) or 0.0) > 0:
1267
+ return True
1268
+ if abs(_first_number(item.get("pnl")) or 0.0) > 0:
1269
+ return True
1270
+ if abs(_first_number(item.get("average_price")) or 0.0) > 0:
1271
+ return True
1272
+ if abs(_first_number(item.get("last_traded_price")) or 0.0) > 0:
1273
+ return True
1274
+ return False
1275
+
1276
+ def _activity_moment(self, item: dict[str, Any]) -> datetime | None:
1277
+ for field in ("trade_time", "captured_at", "order_time", "updated_at"):
1278
+ value = item.get(field)
1279
+ if not value:
1280
+ continue
1281
+ parsed = pd.to_datetime(value, errors="coerce")
1282
+ if pd.isna(parsed):
1283
+ continue
1284
+ if getattr(parsed, "tzinfo", None) is None:
1285
+ try:
1286
+ parsed = parsed.tz_localize(IST)
1287
+ except Exception:
1288
+ continue
1289
+ else:
1290
+ try:
1291
+ parsed = parsed.tz_convert(IST)
1292
+ except Exception:
1293
+ continue
1294
+ return parsed.to_pydatetime()
1295
+ return None
1296
+
1297
+ def _activity_day(self, item: dict[str, Any]) -> date | None:
1298
+ moment = self._activity_moment(item)
1299
+ return moment.date() if moment else None
1300
+
1301
+ def _trade_side_sign(self, transaction_type: Any) -> int:
1302
+ side = str(transaction_type or "").strip().upper()
1303
+ if side in {"B", "BUY"}:
1304
+ return 1
1305
+ if side in {"S", "SELL"}:
1306
+ return -1
1307
+ return 0
1308
+
1309
+ def _quote_for_symbol(
1310
+ self,
1311
+ quote_map: dict[str, dict[str, Any]],
1312
+ *,
1313
+ exchange_segment: str | None,
1314
+ symbol: str | None,
1315
+ ) -> dict[str, Any]:
1316
+ symbol_text = str(symbol or "").strip()
1317
+ if not symbol_text:
1318
+ return {}
1319
+ exchange_text = str(exchange_segment or "").strip()
1320
+ symbol_upper = symbol_text.upper()
1321
+ base_symbol_upper = symbol_upper.split("-")[0]
1322
+
1323
+ def matches(quote: dict[str, Any]) -> bool:
1324
+ quote_symbol = str(quote.get("trading_symbol") or "").strip().upper()
1325
+ if not quote_symbol:
1326
+ return False
1327
+ return quote_symbol == symbol_upper or quote_symbol == base_symbol_upper
1328
+
1329
+ if exchange_text:
1330
+ for quote in quote_map.values():
1331
+ if str(quote.get("exchange_segment") or "").strip() != exchange_text:
1332
+ continue
1333
+ if matches(quote):
1334
+ return quote
1335
+
1336
+ for quote in quote_map.values():
1337
+ if matches(quote):
1338
+ return quote
1339
+ return {}
1340
+
1341
+ def _derive_positions_from_trades(
1342
+ self,
1343
+ trades: list[dict[str, Any]],
1344
+ quote_map: dict[str, dict[str, Any]],
1345
+ ) -> list[dict[str, Any]]:
1346
+ today = datetime.now(IST).date()
1347
+ buckets: dict[tuple[str, str], dict[str, Any]] = {}
1348
+
1349
+ ordered_trades = sorted(
1350
+ trades,
1351
+ key=lambda item: self._activity_moment(item) or datetime.min.replace(tzinfo=IST),
1352
+ )
1353
+
1354
+ for trade in ordered_trades:
1355
+ trade_day = self._activity_day(trade)
1356
+ if trade_day is not None and trade_day != today:
1357
+ continue
1358
+
1359
+ side = self._trade_side_sign(trade.get("transaction_type"))
1360
+ quantity = abs(_first_number(trade.get("quantity")) or 0.0)
1361
+ price = _first_number(trade.get("price"), trade.get("average_price"))
1362
+ symbol = _first_text(trade.get("trading_symbol"), trade.get("symbol"))
1363
+ exchange = _first_text(trade.get("exchange_segment"))
1364
+ if not side or quantity <= 0 or price is None or not symbol:
1365
+ continue
1366
+
1367
+ key = (exchange or "", symbol.upper())
1368
+ bucket = buckets.setdefault(
1369
+ key,
1370
+ {
1371
+ "symbol": _first_text(trade.get("symbol"), trade.get("trading_symbol")),
1372
+ "trading_symbol": symbol,
1373
+ "exchange_segment": exchange,
1374
+ "instrument_token": None,
1375
+ "product": _first_text(trade.get("product")),
1376
+ "transaction_type": None,
1377
+ "net_quantity": 0.0,
1378
+ "average_price": None,
1379
+ "last_traded_price": None,
1380
+ "pnl": 0.0,
1381
+ "realized_pnl": 0.0,
1382
+ "unrealized_pnl": 0.0,
1383
+ "multiplier": 1.0,
1384
+ "updated_at": None,
1385
+ "trade_count": 0,
1386
+ "_open_lots": [],
1387
+ "_last_fill_price": None,
1388
+ "_last_moment": None,
1389
+ },
1390
+ )
1391
+
1392
+ bucket["trade_count"] += 1
1393
+ bucket["transaction_type"] = trade.get("transaction_type") or bucket["transaction_type"]
1394
+ bucket["_last_fill_price"] = price
1395
+ moment = self._activity_moment(trade)
1396
+ if moment and (bucket["_last_moment"] is None or moment > bucket["_last_moment"]):
1397
+ bucket["_last_moment"] = moment
1398
+ bucket["updated_at"] = moment.isoformat()
1399
+
1400
+ remaining = quantity
1401
+ incoming_sign = side
1402
+ open_lots: list[dict[str, float]] = bucket["_open_lots"]
1403
+
1404
+ while remaining > 0 and open_lots and int(open_lots[0]["sign"]) != incoming_sign:
1405
+ lot = open_lots[0]
1406
+ matched = min(float(lot["remaining"]), remaining)
1407
+ bucket["realized_pnl"] += (price - float(lot["price"])) * matched * int(lot["sign"])
1408
+ lot["remaining"] = float(lot["remaining"]) - matched
1409
+ remaining -= matched
1410
+ if lot["remaining"] <= 1e-12:
1411
+ open_lots.pop(0)
1412
+
1413
+ if remaining > 0:
1414
+ open_lots.append({
1415
+ "sign": float(incoming_sign),
1416
+ "remaining": float(remaining),
1417
+ "price": float(price),
1418
+ })
1419
+
1420
+ derived_positions: list[dict[str, Any]] = []
1421
+ for bucket in buckets.values():
1422
+ open_lots = bucket.pop("_open_lots")
1423
+ open_quantity = sum(float(lot["sign"]) * float(lot["remaining"]) for lot in open_lots)
1424
+ open_abs_quantity = sum(float(lot["remaining"]) for lot in open_lots)
1425
+ average_price = None
1426
+ if open_abs_quantity > 0:
1427
+ average_price = sum(float(lot["remaining"]) * float(lot["price"]) for lot in open_lots) / open_abs_quantity
1428
+
1429
+ quote = self._quote_for_symbol(
1430
+ quote_map,
1431
+ exchange_segment=bucket["exchange_segment"],
1432
+ symbol=bucket["trading_symbol"],
1433
+ )
1434
+ ltp = _first_number(quote.get("last_traded_price"))
1435
+ if ltp is None:
1436
+ ltp = _first_number(bucket["_last_fill_price"])
1437
+
1438
+ unrealized_pnl = 0.0
1439
+ if open_quantity and average_price is not None and ltp is not None:
1440
+ unrealized_pnl = (ltp - average_price) * open_quantity
1441
+
1442
+ total_pnl = bucket["realized_pnl"] + unrealized_pnl
1443
+ if abs(open_quantity) <= 1e-12 and abs(total_pnl) <= 1e-12:
1444
+ continue
1445
+
1446
+ derived_positions.append(
1447
+ {
1448
+ "order_no": None,
1449
+ "symbol": bucket["symbol"],
1450
+ "trading_symbol": bucket["trading_symbol"],
1451
+ "exchange_segment": bucket["exchange_segment"],
1452
+ "instrument_token": _first_text(bucket["instrument_token"], quote.get("instrument_token")),
1453
+ "product": bucket["product"],
1454
+ "transaction_type": bucket["transaction_type"],
1455
+ "net_quantity": open_quantity,
1456
+ "average_price": average_price,
1457
+ "last_traded_price": ltp,
1458
+ "pnl": total_pnl,
1459
+ "realized_pnl": bucket["realized_pnl"],
1460
+ "unrealized_pnl": unrealized_pnl,
1461
+ "multiplier": bucket["multiplier"],
1462
+ "updated_at": bucket["updated_at"],
1463
+ }
1464
+ )
1465
+
1466
+ derived_positions.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
1467
+ return derived_positions
1468
+
1469
 
1470
  kotak_neo_manager = KotakNeoManager()