Spaces:
Running
Running
Upload 2 files
Browse files- 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
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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":
|
| 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=
|
| 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()
|