Add live broker positions to portfolio API

This commit is contained in:
Thigazhezhilan J 2026-04-06 11:31:29 +05:30
parent c41f6f2411
commit 565de64459
3 changed files with 142 additions and 26 deletions

View File

@ -22,9 +22,11 @@ from app.services.groww_service import (
fetch_funds as fetch_groww_funds,
fetch_holdings as fetch_groww_holdings,
fetch_ltp as fetch_groww_ltp,
fetch_positions as fetch_groww_positions,
fetch_profile as fetch_groww_profile,
generate_access_token,
normalize_holding as normalize_groww_holding,
normalize_position as normalize_groww_position,
)
from app.services.groww_storage import get_session as get_groww_session
from app.services.live_equity_service import capture_live_equity_snapshot, get_live_equity_curve
@ -35,7 +37,9 @@ from app.services.zerodha_service import (
exchange_request_token,
fetch_funds as fetch_zerodha_funds,
fetch_holdings as fetch_zerodha_holdings,
fetch_positions as fetch_zerodha_positions,
normalize_holding as normalize_zerodha_holding,
normalize_position as normalize_zerodha_position,
)
from app.services.zerodha_storage import (
clear_session as clear_zerodha_session,
@ -181,36 +185,53 @@ def _fetch_normalized_groww_holdings(access_token: str) -> list[dict]:
items = fetch_groww_holdings(access_token)
holdings: list[dict] = []
for item in items:
normalized = normalize_groww_holding(item)
tradingsymbol = _groww_holding_tradingsymbol(normalized)
exchange = _groww_holding_exchange(normalized)
segment = _groww_holding_segment(normalized)
if tradingsymbol and not normalized.get("last_price"):
try:
ltp_data = fetch_groww_ltp(
access_token,
exchange=exchange,
segment=segment,
trading_symbol=tradingsymbol,
)
normalized["last_price"] = _first_number(
ltp_data.get("ltp"),
ltp_data.get("last_price"),
ltp_data.get("price"),
normalized.get("last_price"),
default=0.0,
)
normalized["close_price"] = normalized["last_price"]
normalized["holding_value"] = normalized.get("effective_quantity", 0) * normalized["last_price"]
normalized["display_pnl"] = normalized.get("effective_quantity", 0) * (
normalized["last_price"] - normalized.get("average_price", 0)
)
except GrowwApiError:
pass
normalized = _normalize_enrich_groww_item(access_token, item, normalize_groww_holding)
holdings.append(normalized)
return holdings
def _normalize_enrich_groww_item(access_token: str, item: dict, normalizer) -> dict:
normalized = normalizer(item)
tradingsymbol = _groww_holding_tradingsymbol(normalized)
exchange = _groww_holding_exchange(normalized)
segment = _groww_holding_segment(normalized)
if tradingsymbol and not normalized.get("last_price"):
try:
ltp_data = fetch_groww_ltp(
access_token,
exchange=exchange,
segment=segment,
trading_symbol=tradingsymbol,
)
normalized["last_price"] = _first_number(
ltp_data.get("ltp"),
ltp_data.get("last_price"),
ltp_data.get("price"),
normalized.get("last_price"),
default=0.0,
)
normalized["close_price"] = normalized["last_price"]
normalized["holding_value"] = normalized.get("effective_quantity", 0) * normalized["last_price"]
normalized["display_pnl"] = _first_number(
normalized.get("display_pnl"),
normalized.get("effective_quantity", 0)
* (normalized["last_price"] - normalized.get("average_price", 0)),
default=0.0,
)
except GrowwApiError:
pass
return normalized
def _fetch_normalized_groww_positions(access_token: str) -> list[dict]:
items = fetch_groww_positions(access_token)
positions: list[dict] = []
for item in items:
normalized = _normalize_enrich_groww_item(access_token, item, normalize_groww_position)
positions.append(normalized)
return positions
def _normalize_groww_funds(data: dict | None) -> dict:
payload = data if isinstance(data, dict) else {}
available = payload.get("available") if isinstance(payload.get("available"), dict) else {}
@ -603,6 +624,33 @@ async def broker_holdings(request: Request):
raise HTTPException(status_code=400, detail=f"Unsupported broker: {broker_name}")
@router.get("/positions")
async def broker_positions(request: Request):
user = _require_user(request)
_entry, broker_name = _resolve_connected_broker(user["id"])
if broker_name == "ZERODHA":
session = get_zerodha_session(user["id"])
if not session:
raise HTTPException(status_code=400, detail="Zerodha is not connected")
try:
data = fetch_zerodha_positions(session["api_key"], session["access_token"])
except KiteApiError as exc:
_raise_zerodha_error(user["id"], exc)
return {"broker": broker_name, "positions": [normalize_zerodha_position(item) for item in data]}
if broker_name == "GROWW":
session = get_groww_session(user["id"])
if not session or not session.get("access_token"):
raise HTTPException(status_code=400, detail="Groww is not connected")
try:
positions = _fetch_normalized_groww_positions(session["access_token"])
except GrowwApiError as exc:
_raise_groww_error(user["id"], exc)
return {"broker": broker_name, "positions": positions}
raise HTTPException(status_code=400, detail=f"Unsupported broker: {broker_name}")
@router.get("/funds")
async def broker_funds(request: Request):
user = _require_user(request)

View File

@ -358,3 +358,9 @@ def normalize_holding(item: dict | None) -> dict:
entry["display_pnl"] = quantity * (last_price - average_price)
entry["holding_value"] = quantity * last_price
return entry
def normalize_position(item: dict | None) -> dict:
entry = normalize_holding(item)
entry["product"] = _first_text(entry.get("product"), entry.get("product_type"), default="CNC").upper()
return entry

View File

@ -157,6 +157,19 @@ def fetch_holdings(api_key: str, access_token: str) -> list:
return response.get("data", [])
def fetch_positions(api_key: str, access_token: str) -> list:
url = f"{KITE_API_BASE}/portfolio/positions"
response = _request("GET", url, headers=_auth_headers(api_key, access_token))
data = response.get("data", {})
if isinstance(data, dict):
net_positions = data.get("net")
if isinstance(net_positions, list):
return net_positions
if isinstance(data, list):
return data
return []
def fetch_funds(api_key: str, access_token: str) -> dict:
url = f"{KITE_API_BASE}/user/margins"
response = _request("GET", url, headers=_auth_headers(api_key, access_token))
@ -260,6 +273,55 @@ def fetch_order_history(api_key: str, access_token: str, order_id: str) -> list:
return response.get("data", [])
def position_quantity(item: dict | None) -> float:
entry = item or {}
return _first_float(
entry.get("net_quantity"),
entry.get("quantity"),
entry.get("qty"),
default=0.0,
)
def position_average_price(item: dict | None) -> float:
entry = item or {}
return _first_float(
entry.get("average_price"),
entry.get("avg_price"),
entry.get("buy_price"),
default=0.0,
)
def position_last_price(item: dict | None) -> float:
entry = item or {}
return _first_float(
entry.get("last_price"),
entry.get("close_price"),
entry.get("average_price"),
entry.get("avg_price"),
default=0.0,
)
def normalize_position(item: dict | None) -> dict:
entry = dict(item or {})
quantity = position_quantity(entry)
average_price = position_average_price(entry)
last_price = position_last_price(entry)
pnl = _first_float(entry.get("pnl"), default=quantity * (last_price - average_price))
entry["settled_quantity"] = quantity
entry["t1_quantity"] = 0.0
entry["effective_quantity"] = quantity
entry["quantity"] = quantity
entry["average_price"] = average_price
entry["last_price"] = last_price
entry["close_price"] = last_price
entry["display_pnl"] = pnl
entry["holding_value"] = quantity * last_price
return entry
def cancel_order(
api_key: str,
access_token: str,