Modern financial analysis is rapidly moving toward automation and agentic workflows. Integrating large language models (LLMs) with real-time financial data unlocks not just powerful insights but also entirely new ways of interacting with portfolio data.
This post walks through a practical, autonomous solution using Google ADK, Zerodha’s Kite MCP protocol, and an LLM for actionable portfolio analytics. The full workflow and code are available on GitHub.
Why This Stack?
- Google ADK: Enables LLM agents to interact with live tools, APIs, and event streams in a repeatable, testable way.
- Zerodha MCP (Model Control Protocol): Provides a secure, real-time API to portfolio holdings using Server-Sent Events (SSE).
- LLMs (Gemini/GPT-4o): Analyze portfolio data, highlight concentration risk, and offer actionable recommendations.
Architecture Overview
The workflow has three main steps:
- User authenticates with Zerodha using an OAuth browser flow.
- The agent retrieves live holdings via the MCP
get_holdings
tool. - The LLM agent analyzes the raw data for risk and performance insights.
All API keys and connection details are managed through environment variables for security and reproducibility.
Key Code Snippets
1. Environment and Dependency Setup
import os
from dotenv import load_dotenv
# Load API keys and config from .env
load_dotenv('.env')
os.environ["GOOGLE_API_KEY"] = os.environ["GOOGLE_API_KEY"]
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False"
2. ADK Agent and Toolset Initialization
from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, SseServerParams
MCP_SSE_URL = os.environ.get("MCP_SSE_URL", "https://mcp.kite.trade/sse")
toolset = MCPToolset(
connection_params=SseServerParams(url=MCP_SSE_URL, headers={})
)
root_agent = LlmAgent(
model='gemini-2.0-flash',
name='zerodha_portfolio_assistant',
instruction=(
"You are an expert Zerodha portfolio assistant. "
"Use the 'login' tool to authenticate, and the 'get_holdings' tool to fetch stock holdings. "
"When given portfolio data, analyze for concentration risk and best/worst performers."
),
tools=[toolset]
)
3. Orchestrating the Workflow
from google.adk.sessions import InMemorySessionService
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.runners import Runner
from google.genai import types
import asyncio
async def run_workflow():
session_service = InMemorySessionService()
artifacts_service = InMemoryArtifactService()
session = await session_service.create_session(
state={}, app_name='zerodha_portfolio_app', user_id='user1'
)
runner = Runner(
app_name='zerodha_portfolio_app',
agent=root_agent,
artifact_service=artifacts_service,
session_service=session_service,
)
# 1. Login Step
login_query = "Authenticate and provide the login URL for Zerodha."
content = types.Content(role='user', parts=[types.Part(text=login_query)])
login_url = None
async for event in runner.run_async(session_id=session.id, user_id=session.user_id, new_message=content):
if event.is_final_response():
import re
match = re.search(r'(https?://[^\s)]+)', getattr(event.content.parts[0], "text", ""))
if match:
login_url = match.group(1)
if not login_url:
print("No login URL found. Exiting.")
return
print(f"Open this URL in your browser to authenticate:\n{login_url}")
import webbrowser; webbrowser.open(login_url)
input("Press Enter after completing login...")
# 2. Fetch Holdings
holdings_query = "Show my current stock holdings."
content = types.Content(role='user', parts=[types.Part(text=holdings_query)])
holdings_raw = None
async for event in runner.run_async(session_id=session.id, user_id=session.user_id, new_message=content):
if event.is_final_response():
holdings_raw = getattr(event.content.parts[0], "text", None)
if not holdings_raw:
print("No holdings data found.")
return
# 3. Analysis
analysis_prompt = f"""
You are a senior portfolio analyst.
Given only the raw stock holdings listed below, do not invent or assume any other holdings.
1. **Concentration Risk**: Identify if a significant percentage of the total portfolio is allocated to a single stock or sector. Quantify the largest exposures, explain why this matters, and suggest specific diversification improvements.
2. **Performance Standouts**: Clearly identify the best and worst performing stocks in the portfolio (by absolute and percentage P&L), and give actionable recommendations.
Raw holdings:
{holdings_raw}
Use only the provided data.
"""
content = types.Content(role='user', parts=[types.Part(text=analysis_prompt)])
async for event in runner.run_async(session_id=session.id, user_id=session.user_id, new_message=content):
if event.is_final_response():
print("\n=== Portfolio Analysis Report ===\n")
print(getattr(event.content.parts[0], "text", ""))
asyncio.run(run_workflow())
Security and Environment Configuration
All API keys and MCP endpoints are managed via environment variables or a .env
file.
Never hardcode sensitive information in code.
Example .env
file:
GOOGLE_API_KEY=your_google_gemini_api_key
MCP_SSE_URL=https://mcp.kite.trade/sse
What This Enables
- Reproducible automation: Agents can authenticate, retrieve, and analyze portfolios with minimal human input.
- Extensibility: Easily add more tools (orders, margins, etc.) or more advanced analytic prompts.
- Separation of concerns: Business logic, security, and agent workflow are all clearly separated.
Repository
Full working code and documentation:
https://github.com/navveenb/agentic-ai-worfklows/tree/main/google-adk-zerodha
This workflow is for educational and portfolio analysis purposes only. Not investment advice.