New_Dhan_UI / backend /utils /dhan_api.py
kishan-1721
Initial deployment to Hugging Face Space
123bf24
"""
Dhan API integration utilities
"""
import requests
import pandas as pd
from dhanhq import dhanhq
from backend.utils.data_loader import safe_json_to_df
def fetch_holdings(access_token: str) -> pd.DataFrame:
"""
Fetch holdings from Dhan API
Args:
access_token: Client access token
Returns:
DataFrame with holdings data
"""
url = "https://api.dhan.co/v2/holdings"
headers = {"Content-Type": "application/json", "access-token": access_token}
try:
resp = requests.get(url, headers=headers, timeout=8)
except Exception as e:
print(f"Error fetching holdings: {e}")
return pd.DataFrame()
columns_holding = [
"exchange",
"tradingSymbol",
"securityId",
"isin",
"totalQty",
"dpQty",
"t1Qty",
"mtf_t1_qty",
"mtf_qty",
"availableQty",
"collateralQty",
"avgCostPrice",
"lastTradedPrice",
]
df = safe_json_to_df(resp, columns_holding)
# Ensure numeric columns exist
for c in ["mtf_qty", "availableQty", "avgCostPrice", "lastTradedPrice"]:
if c not in df.columns:
df[c] = 0
df["mtf_qty"] = pd.to_numeric(df["mtf_qty"], errors="coerce").fillna(0)
df["availableQty"] = pd.to_numeric(df["availableQty"], errors="coerce").fillna(0)
df["avgCostPrice"] = pd.to_numeric(df["avgCostPrice"], errors="coerce").fillna(0)
df["lastTradedPrice"] = pd.to_numeric(df["lastTradedPrice"], errors="coerce").fillna(0)
return df
def fetch_positions(access_token: str) -> pd.DataFrame:
"""
Fetch positions from Dhan API
Args:
access_token: Client access token
Returns:
DataFrame with positions data
"""
url = "https://api.dhan.co/v2/positions"
headers = {"Content-Type": "application/json", "access-token": access_token}
try:
resp = requests.get(url, headers=headers, timeout=8)
except Exception as e:
print(f"Error fetching positions: {e}")
return pd.DataFrame()
columns_position = [
"dhanClientId",
"tradingSymbol",
"securityId",
"positionType",
"exchangeSegment",
"productType",
"buyAvg",
"costPrice",
"buyQty",
"sellAvg",
"sellQty",
"netQty",
"realizedProfit",
"unrealizedProfit",
"rbiReferenceRate",
"multiplier",
"carryForwardBuyQty",
"carryForwardSellQty",
"carryForwardBuyValue",
"carryForwardSellValue",
"dayBuyQty",
"daySellQty",
"dayBuyValue",
"daySellValue",
"drvExpiryDate",
"drvOptionType",
"drvStrikePrice",
"crossCurrency",
]
df = safe_json_to_df(resp, columns_position)
# Ensure numeric columns exist
for c in ["netQty", "costPrice", "unrealizedProfit"]:
if c not in df.columns:
df[c] = 0
df["netQty"] = pd.to_numeric(df["netQty"], errors="coerce").fillna(0)
df["costPrice"] = pd.to_numeric(df["costPrice"], errors="coerce").fillna(0)
df["unrealizedProfit"] = pd.to_numeric(df["unrealizedProfit"], errors="coerce").fillna(0)
return df
def process_client_data(client_row: pd.Series) -> pd.DataFrame:
"""
Process client data by combining holdings and positions
Args:
client_row: Series containing client details
Returns:
DataFrame with combined holdings and positions
"""
access_token = client_row.get("access_token")
account_holder = client_row.get("Client Name") or client_row.get("client_id")
holding_df = fetch_holdings(access_token)
position_df = fetch_positions(access_token)
# Normalize column names and compute P&L
if not holding_df.empty:
holding_df["productType"] = holding_df["mtf_qty"].apply(lambda x: "MTF" if x > 0 else "CASH")
holding_df["positionType"] = holding_df["availableQty"].apply(lambda x: "BUY" if x > 0 else "SELL")
holding_df["exchangeSegment"] = holding_df["availableQty"].apply(lambda x: "NSE_EQ" if x > 0 else "NONE")
holding_df["netQty"] = holding_df["availableQty"]
holding_df["costPrice"] = holding_df["avgCostPrice"]
holding_df["P & L"] = (holding_df["lastTradedPrice"] - holding_df["avgCostPrice"]) * holding_df["netQty"]
# Positions may already have netQty / costPrice / unrealizedProfit
if not position_df.empty:
position_df = position_df.rename(columns={"unrealizedProfit": "P & L"})
columns = ["tradingSymbol", "positionType", "exchangeSegment", "productType", "costPrice", "netQty", "P & L"]
if position_df.empty and holding_df.empty:
result = pd.DataFrame(columns=columns)
else:
parts = []
if not position_df.empty:
parts.append(position_df.reindex(columns=columns, fill_value=0))
if not holding_df.empty:
parts.append(holding_df.reindex(columns=columns, fill_value=0))
result = pd.concat(parts, ignore_index=True)
result = result[result["netQty"] != 0].reset_index(drop=True)
result['costPrice'] = round(result['costPrice'], 2)
# Attach account holder for traceability
if not result.empty:
result["Account_Holder"] = account_holder
return result
def add_total_row(df: pd.DataFrame, label: str = "TOTAL") -> pd.DataFrame:
"""
Add a total row to DataFrame with summed numeric columns
Args:
df: Input DataFrame
label: Label for the total row
Returns:
DataFrame with total row appended
"""
if df.empty:
return df
total_row = {col: "" for col in df.columns}
first_col = list(df.columns)[0]
total_row[first_col] = label
if "P & L" in df.columns:
total_row["P & L"] = df["P & L"].sum()
if "My Investment" in df.columns:
total_row["My Investment"] = df["My Investment"].sum()
if "Total Investment" in df.columns:
total_row["Total Investment"] = df["Total Investment"].sum()
if "Profit %" in df.columns:
if "My Investment" in df.columns and df["My Investment"].sum() != 0:
weighted_profit = (df["P & L"].sum() / df["My Investment"].sum()) * 100
total_row["Profit %"] = round(weighted_profit, 2)
else:
total_row["Profit %"] = 0
return pd.concat([df, pd.DataFrame([total_row])], ignore_index=True)
def get_fund_limits(client_id: str, access_token: str) -> dict:
"""
Get fund limits for a client using dhanhq SDK
Args:
client_id: Client ID
access_token: Access token
Returns:
Dictionary with fund limit information
"""
try:
dhan = dhanhq(client_id, access_token)
fund_info = dhan.get_fund_limits().get("data", {})
return {
"availabelBalance": fund_info.get("availabelBalance", 0),
"utilizedAmount": fund_info.get("utilizedAmount", 0)
}
except Exception as e:
print(f"Error fetching fund limits for {client_id}: {e}")
return {"availabelBalance": 0, "utilizedAmount": 0}