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_funds as fetch_groww_funds,
fetch_holdings as fetch_groww_holdings, fetch_holdings as fetch_groww_holdings,
fetch_ltp as fetch_groww_ltp, fetch_ltp as fetch_groww_ltp,
fetch_positions as fetch_groww_positions,
fetch_profile as fetch_groww_profile, fetch_profile as fetch_groww_profile,
generate_access_token, generate_access_token,
normalize_holding as normalize_groww_holding, 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.groww_storage import get_session as get_groww_session
from app.services.live_equity_service import capture_live_equity_snapshot, get_live_equity_curve 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, exchange_request_token,
fetch_funds as fetch_zerodha_funds, fetch_funds as fetch_zerodha_funds,
fetch_holdings as fetch_zerodha_holdings, fetch_holdings as fetch_zerodha_holdings,
fetch_positions as fetch_zerodha_positions,
normalize_holding as normalize_zerodha_holding, normalize_holding as normalize_zerodha_holding,
normalize_position as normalize_zerodha_position,
) )
from app.services.zerodha_storage import ( from app.services.zerodha_storage import (
clear_session as clear_zerodha_session, clear_session as clear_zerodha_session,
@ -181,7 +185,13 @@ def _fetch_normalized_groww_holdings(access_token: str) -> list[dict]:
items = fetch_groww_holdings(access_token) items = fetch_groww_holdings(access_token)
holdings: list[dict] = [] holdings: list[dict] = []
for item in items: for item in items:
normalized = normalize_groww_holding(item) 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) tradingsymbol = _groww_holding_tradingsymbol(normalized)
exchange = _groww_holding_exchange(normalized) exchange = _groww_holding_exchange(normalized)
segment = _groww_holding_segment(normalized) segment = _groww_holding_segment(normalized)
@ -202,13 +212,24 @@ def _fetch_normalized_groww_holdings(access_token: str) -> list[dict]:
) )
normalized["close_price"] = normalized["last_price"] normalized["close_price"] = normalized["last_price"]
normalized["holding_value"] = normalized.get("effective_quantity", 0) * normalized["last_price"] normalized["holding_value"] = normalized.get("effective_quantity", 0) * normalized["last_price"]
normalized["display_pnl"] = normalized.get("effective_quantity", 0) * ( normalized["display_pnl"] = _first_number(
normalized["last_price"] - normalized.get("average_price", 0) normalized.get("display_pnl"),
normalized.get("effective_quantity", 0)
* (normalized["last_price"] - normalized.get("average_price", 0)),
default=0.0,
) )
except GrowwApiError: except GrowwApiError:
pass pass
holdings.append(normalized) return normalized
return holdings
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: def _normalize_groww_funds(data: dict | None) -> dict:
@ -603,6 +624,33 @@ async def broker_holdings(request: Request):
raise HTTPException(status_code=400, detail=f"Unsupported broker: {broker_name}") 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") @router.get("/funds")
async def broker_funds(request: Request): async def broker_funds(request: Request):
user = _require_user(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["display_pnl"] = quantity * (last_price - average_price)
entry["holding_value"] = quantity * last_price entry["holding_value"] = quantity * last_price
return entry 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", []) 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: def fetch_funds(api_key: str, access_token: str) -> dict:
url = f"{KITE_API_BASE}/user/margins" url = f"{KITE_API_BASE}/user/margins"
response = _request("GET", url, headers=_auth_headers(api_key, access_token)) 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", []) 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( def cancel_order(
api_key: str, api_key: str,
access_token: str, access_token: str,