Add live broker positions to portfolio API
This commit is contained in:
parent
c41f6f2411
commit
565de64459
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user