diff --git a/.env.example b/.env.example
index 8f2bc4c..19d35ee 100644
--- a/.env.example
+++ b/.env.example
@@ -23,13 +23,25 @@ SERVER_DEBUG=false
# ===== CLIENT CONFIGURATION =====
+# LLM Configuration
+# -------------------
+# Choose the LLM provider to use (default: openai) - options: openai, groq
+LLM_PROVIDER=openai
+
# OpenAI Configuration
# -------------------
-# OpenAI API key for the AGNO agent (required for client)
+# OpenAI API key for the AGNO agent (required if using OpenAI)
OPENAI_API_KEY=your_openai_api_key_here
# OpenAI model to use (default: gpt-4o)
OPENAI_MODEL=gpt-4o
+# GROQ Configuration
+# -------------------
+# GROQ API key (required if using GROQ)
+GROQ_API_KEY=your_groq_api_key_here
+# GROQ model to use (default: llama-3.1-70b-versatile)
+GROQ_MODEL=llama-3.3-70b-versatile
+
# Client MCP Connection
# --------------------
# MCP server host (default: localhost)
diff --git a/README.md b/README.md
index d60d9a6..4a6099a 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,15 @@
+[](https://mseep.ai/app/marketcalls-openalgo-mcp)
+
# OpenAlgo MCP - AI Trading Assistant
-An AI-powered trading assistant platform for OpenAlgo, leveraging Machine Conversation Protocol (MCP) and Large Language Models to provide intelligent trading capabilities.
+An AI-powered trading assistant platform for OpenAlgo, leveraging Machine Conversation Protocol (MCP) and Large Language Models to provide intelligent trading capabilities through a modern web interface.
## Overview
OpenAlgo MCP integrates the powerful OpenAlgo trading platform with advanced AI capabilities through:
1. An MCP server that exposes OpenAlgo API functions as tools for AI interaction
-2. An intelligent client application providing a conversational interface for trading
+2. A web-based client application providing a conversational interface for trading
+3. Modern UI with real-time updates via WebSockets
This bridge between OpenAlgo's trading capabilities and AI allows for a natural language interface to complex trading operations, making algorithmic trading more accessible to users of all technical backgrounds.
@@ -33,18 +36,31 @@ This bridge between OpenAlgo's trading capabilities and AI allows for a natural
- Guided assistance for complex trading operations
- Real-time data presentation in human-readable formats
+### Modern Web Interface
+
+- Responsive design with light/dark mode support
+- Real-time WebSocket communication
+- Markdown rendering for better readability
+- Quick action buttons for common operations
+- Connection status monitoring
+
## Project Structure
```
openalgo-mcp/
-├── .env # Common environment configuration
+├── .env # Environment configuration
├── .env.example # Example configuration template
-├── requirements.txt # Common dependencies for both client and server
+├── requirements.txt # Project dependencies
├── LICENSE # MIT License
├── server/ # MCP Server implementation
-│ ├── server.py # OpenAlgo MCP server code
-└── client/ # Client implementation
- ├── trading_agent.py # AI assistant client code
+│ └── server.py # FastMCP server exposing OpenAlgo API
+└── client/ # Web Client implementation
+ ├── app.py # FastAPI web application
+ ├── templates/ # HTML templates
+ │ └── index.html # Main UI template
+ └── static/ # Static assets
+ ├── script.js # Client-side JavaScript
+ └── style.css # CSS styles
```
## Installation Guide
@@ -59,7 +75,7 @@ openalgo-mcp/
```bash
git clone https://github.com/marketcalls/openalgo-mcp.git
-cd openalgo-mcp/mcpserver
+cd openalgo-mcp
```
### Step 2: Set Up Environment
@@ -67,7 +83,10 @@ cd openalgo-mcp/mcpserver
```bash
# Create and activate virtual environment
python -m venv venv
-source venv/bin/activate # On Windows: venv\Scripts\activate
+# On Windows:
+venv\Scripts\activate
+# On Linux/Mac:
+# source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
@@ -80,9 +99,15 @@ pip install -r requirements.txt
cp .env.example .env
# Edit the .env file with your API keys and settings
-# vim .env or use any text editor
+# Use your preferred text editor
```
+Required environment variables:
+- `OPENALGO_API_KEY`: Your OpenAlgo API key
+- `OPENALGO_API_HOST`: OpenAlgo API host (default: http://127.0.0.1:5000)
+- `OPENAI_API_KEY`: OpenAI API key for the AI assistant
+- `OPENAI_MODEL`: OpenAI model to use (default: gpt-4o)
+
## Usage
### Starting the MCP Server
@@ -98,17 +123,18 @@ The server supports the following options:
- `--port`: Server port (default: 8001)
- `--mode`: Server transport mode - 'stdio' or 'sse' (default: sse)
-### Starting the Trading Assistant Client
+### Starting the Web UI Client
```bash
cd client
-python trading_agent.py
+python app.py
```
-The client supports these options:
-- `--host`: MCP server host (default: from .env)
-- `--port`: MCP server port (default: from .env)
-- `--model`: OpenAI model to use (default: from .env)
+This will start the web interface on http://localhost:8000 by default.
+
+You can then access the trading assistant through your web browser.
+
+The client application will automatically connect to the MCP server as configured in the .env file.
## Configuration
@@ -159,18 +185,21 @@ The OpenAlgo MCP implementation provides comprehensive API coverage including:
The implementation uses FastMCP with SSE (Server-Sent Events) transport for real-time communication and includes proper error handling, logging, and parameter validation.
-## Server Implementation Details
+## Technical Implementation
+
+### Server Implementation
The OpenAlgo MCP Server is built using the FastMCP library and exposes OpenAlgo trading functionality through a comprehensive set of tools. It uses Server-Sent Events (SSE) as the primary transport mechanism for real-time communication.
-### Server Architecture
+#### Server Architecture
- **Framework**: Uses FastMCP with Starlette for the web server
- **Transport**: Server-Sent Events (SSE) for real-time bidirectional communication
- **API Client**: Wraps the OpenAlgo API with appropriate error handling and logging
- **Configuration**: Uses environment variables with command-line override capabilities
+- **Health Endpoint**: Supports /health endpoint for client health checks
-### Available API Tools
+#### Available API Tools
The server exposes over 15 trading-related tools, including:
@@ -180,23 +209,29 @@ The server exposes over 15 trading-related tools, including:
- **Account Information**: get_funds, get_holdings, get_position_book, get_order_book, get_trade_book
- **Symbol Information**: get_symbol_metadata, get_all_tickers
-## Client Implementation Details
+### Web Client Implementation
-The Trading Assistant client provides a user-friendly interface to interact with the OpenAlgo platform through natural language. It uses OpenAI's language models to interpret user commands and invoke the appropriate trading functions.
+The Trading Assistant web client provides a user-friendly interface to interact with the OpenAlgo platform through natural language. It uses OpenAI's language models to interpret user commands and invoke the appropriate trading functions.
-### Client Architecture
+#### Client Architecture
-- **Framework**: Uses Agno agent framework with OpenAI Chat models
-- **UI**: Rich console interface with custom styling for an enhanced terminal experience
+- **Framework**: FastAPI web server with WebSocket support
+- **MCP Client**: Uses MCP's SSE client for communicating with the server
+- **LLM Integration**: Uses the Agno agent framework with OpenAI Chat models
+- **UI**: Modern web interface built with HTML, CSS, and JavaScript
- **Symbol Helper**: Built-in utilities for correct symbol formatting across exchanges
- **Error Handling**: Comprehensive exception handling with user-friendly feedback
-### Trading Assistant Capabilities
+#### Web UI Features
-- **Natural Language Interface**: Understands trading terminology and concepts
-- **Symbol Format Assistance**: Helps construct proper symbol formats for equities, futures, and options
-- **Data Presentation**: Formats market data in clean, readable formats
-- **Contextual Awareness**: Maintains conversation history to provide contextual responses
+- **Real-time Chat**: WebSocket-based real-time communication
+- **Theme Support**: Light and dark mode themes
+- **Quick Actions**: Pre-defined buttons for common operations
+- **Markdown Rendering**: Format AI responses with proper markdown styling
+- **Connection Status**: Visual indicators for server connection state
+- **Responsive Design**: Works on desktop and mobile devices
+- **Auto-scrolling**: Messages automatically scroll into view
+- **Message History**: Maintains conversation context for better interactions
## Troubleshooting Guide
diff --git a/client/app.py b/client/app.py
new file mode 100644
index 0000000..d7a8ab1
--- /dev/null
+++ b/client/app.py
@@ -0,0 +1,589 @@
+import os
+import asyncio
+import logging
+import uvicorn
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException
+from fastapi.responses import HTMLResponse, JSONResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+from pathlib import Path
+from mcp import ClientSession
+from mcp.client.sse import sse_client
+from agno.models.openai import OpenAIChat
+from agno.models.groq import Groq
+from agno.agent import Agent
+from agno.tools.mcp import MCPTools
+from typing import Optional, Dict, List
+from contextlib import AsyncExitStack
+from pydantic import BaseModel
+from dotenv import load_dotenv
+import json
+from fastapi.middleware.cors import CORSMiddleware
+
+# Configure logging with a custom filter to reduce noise
+class SilentFilter(logging.Filter):
+ def filter(self, record):
+ # Filter out specific noisy log messages
+ silenced_messages = [
+ "HTTP Request:",
+ "HTTP Response:",
+ "Connection pool is full",
+ "Starting new HTTPS connection"
+ ]
+ return not any(msg in record.getMessage() for msg in silenced_messages)
+
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+# Add the filter to reduce noise
+for handler in logging.getLogger().handlers:
+ handler.addFilter(SilentFilter())
+
+# Find and load the .env file
+parent_dir = Path(__file__).resolve().parent
+env_path = parent_dir / ".env"
+if env_path.exists():
+ load_dotenv(dotenv_path=env_path)
+ logger.info(f"Loaded environment from {env_path}")
+else:
+ # Try parent directory
+ parent_env = parent_dir.parent / ".env"
+ if parent_env.exists():
+ load_dotenv(dotenv_path=parent_env)
+ logger.info(f"Loaded environment from {parent_env}")
+ else:
+ load_dotenv()
+ logger.info("Loaded environment from default location")
+
+# Create FastAPI app
+app = FastAPI(title="OpenAlgo Trading Assistant")
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # For production, specify the domains you want to allow
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Set up templates and static files
+templates = Jinja2Templates(directory=str(parent_dir / "templates"))
+static_dir = parent_dir / "static"
+os.makedirs(parent_dir / "templates", exist_ok=True)
+os.makedirs(static_dir, exist_ok=True)
+app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
+
+# Configuration
+MCP_HOST = os.environ.get("MCP_HOST", "localhost")
+MCP_PORT = int(os.environ.get("MCP_PORT", "8001"))
+MCP_URL = f"http://{MCP_HOST}:{MCP_PORT}/sse"
+
+# LLM Provider settings
+LLM_PROVIDER = os.environ.get("LLM_PROVIDER", "openai").lower()
+OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o")
+GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama-3.1-70b-versatile")
+
+# Store active connections and resources
+active_connections: Dict[str, WebSocket] = {}
+mcp_clients: Dict[str, "MCPClient"] = {}
+agents: Dict[str, Agent] = {}
+chat_histories: Dict[str, "ChatHistory"] = {}
+
+class SymbolHelper:
+ """Helper class for OpenAlgo symbol format assistance"""
+
+ @staticmethod
+ def format_equity(symbol, exchange="NSE"):
+ """Format equity symbol"""
+ return symbol.upper()
+
+ @staticmethod
+ def format_future(base_symbol, expiry_year, expiry_month, expiry_date=None):
+ """Format futures symbol - Example: BANKNIFTY24APR24FUT"""
+ month_map = {
+ 1: "JAN", 2: "FEB", 3: "MAR", 4: "APR", 5: "MAY", 6: "JUN",
+ 7: "JUL", 8: "AUG", 9: "SEP", 10: "OCT", 11: "NOV", 12: "DEC"
+ }
+
+ if isinstance(expiry_month, int):
+ month_str = month_map[expiry_month]
+ else:
+ month_str = expiry_month.upper()
+
+ if expiry_date:
+ date_part = f"{expiry_date}"
+ else:
+ date_part = ""
+
+ if isinstance(expiry_year, int) and expiry_year > 2000:
+ year = str(expiry_year)[2:]
+ else:
+ year = str(expiry_year)
+
+ return f"{base_symbol.upper()}{year}{month_str}{date_part}FUT"
+
+ @staticmethod
+ def format_option(base_symbol, expiry_date, expiry_month, expiry_year, strike_price, option_type):
+ """Format options symbol - Example: NIFTY28MAR2420800CE"""
+ month_map = {
+ 1: "JAN", 2: "FEB", 3: "MAR", 4: "APR", 5: "MAY", 6: "JUN",
+ 7: "JUL", 8: "AUG", 9: "SEP", 10: "OCT", 11: "NOV", 12: "DEC"
+ }
+
+ if isinstance(expiry_month, int):
+ month_str = month_map[expiry_month]
+ else:
+ month_str = expiry_month.upper()
+
+ if isinstance(expiry_year, int) and expiry_year > 2000:
+ year = str(expiry_year)[2:]
+ else:
+ year = str(expiry_year)
+
+ opt_type = "CE" if option_type.upper() in ["C", "CALL", "CE"] else "PE"
+
+ if isinstance(strike_price, (int, float)):
+ if strike_price == int(strike_price):
+ strike_str = str(int(strike_price))
+ else:
+ strike_str = str(strike_price)
+ else:
+ strike_str = strike_price
+
+ return f"{base_symbol.upper()}{expiry_date}{month_str}{year}{strike_str}{opt_type}"
+
+class MCPClient:
+ """Enhanced MCP Client with better error handling"""
+
+ def __init__(self):
+ self.session: Optional[ClientSession] = None
+ self._streams_context = None
+ self._session_context = None
+ self._connected = False
+
+ async def connect_to_sse_server(self, server_url: str) -> bool:
+ """Connect to an MCP server running with SSE transport"""
+ logger.info(f"Attempting to connect to MCP server at {server_url}")
+ try:
+ # Clean up any existing connections
+ await self.cleanup()
+
+ # Create new connection
+ self._streams_context = sse_client(url=server_url)
+ streams = await self._streams_context.__aenter__()
+ logger.info("Successfully created SSE client streams")
+
+ self._session_context = ClientSession(*streams)
+ self.session = await self._session_context.__aenter__()
+ logger.info("Successfully created client session")
+
+ # Initialize session
+ await self.session.initialize()
+ logger.info("Successfully initialized MCP session")
+
+ # Verify connection by listing tools
+ response = await self.session.list_tools()
+ logger.info(f"Successfully connected - found {len(response.tools)} tools")
+ self._connected = True
+ return True
+
+ except Exception as e:
+ logger.error(f"Error connecting to MCP server: {str(e)}")
+ await self.cleanup()
+ raise
+
+ async def cleanup(self):
+ """Properly clean up the session and streams"""
+ try:
+ if self._session_context:
+ await self._session_context.__aexit__(None, None, None)
+ self._session_context = None
+ if self._streams_context:
+ await self._streams_context.__aexit__(None, None, None)
+ self._streams_context = None
+ self.session = None
+ self._connected = False
+ except Exception as e:
+ logger.error(f"Error during cleanup: {str(e)}")
+
+ async def disconnect(self):
+ """Disconnect from the MCP server"""
+ await self.cleanup()
+
+ @property
+ def is_connected(self) -> bool:
+ return self._connected and self.session is not None
+
+class Message(BaseModel):
+ role: str
+ content: str
+
+class ChatHistory(BaseModel):
+ messages: List[Message] = []
+
+async def get_mcp_client(client_id: str) -> MCPClient:
+ """Get or create an MCP client for the connection"""
+ if client_id not in mcp_clients:
+ mcp_client = MCPClient()
+ try:
+ await mcp_client.connect_to_sse_server(MCP_URL)
+ mcp_clients[client_id] = mcp_client
+
+ # Initialize the agent
+ mcp_tools = MCPTools(session=mcp_client.session)
+ await mcp_tools.initialize()
+
+ # Choose model based on provider
+ if LLM_PROVIDER == "groq":
+ model = Groq(id=GROQ_MODEL, timeout=60.0)
+ else:
+ model = OpenAIChat(id=OPENAI_MODEL)
+
+ agent = Agent(
+ instructions="""
+You are an OpenAlgo Trading Assistant, helping users manage their trading accounts, orders, portfolio, and positions using OpenAlgo API tools provided over MCP.
+
+# Responsibilities:
+- Assist with order placement, modification, and cancellation
+- Provide insights on portfolio holdings, positions, and orders
+- Track order status, market quotes, and market depth
+- Help with getting historical data and symbol information
+- Assist with retrieving funds and managing positions
+- Guide users on correct OpenAlgo symbol formats for different instruments
+
+# OpenAlgo Symbol Format Guidelines:
+## Exchange Codes:
+- NSE: National Stock Exchange equities
+- BSE: Bombay Stock Exchange equities
+- NFO: NSE Futures and Options
+- BFO: BSE Futures and Options
+
+## Equity Symbol Format:
+Simply use the base symbol, e.g., "INFY", "SBIN", "TATAMOTORS"
+
+## Future Symbol Format:
+[Base Symbol][Expiration Date]FUT
+Examples: BANKNIFTY24APR24FUT, USDINR10MAY24FUT
+
+## Options Symbol Format:
+[Base Symbol][Expiration Date][Strike Price][Option Type]
+Examples: NIFTY28MAR2420800CE, VEDL25APR24292.5CE
+
+# Parameter Guidelines:
+- symbol: Trading symbol following OpenAlgo format
+- exchange: Exchange code (NSE, BSE, NFO, etc.)
+- price_type: "MARKET", "LIMIT", "SL" (stop-loss), "SL-M" (stop-loss market)
+- product: "MIS" (intraday), "CNC" (delivery), "NRML" (normal)
+- action: "BUY" or "SELL"
+- quantity: Number of shares/contracts to trade
+- strategy: Usually "Python" (default)
+
+# Updated Agent Instructions with Better Formatting
+# Add this to your client/app.py when creating the agent
+
+# Important Instructions:
+- Respond in human-like conversational, friendly, and professional tone in concise manner.
+- ALWAYS format responses in clean, readable markdown format
+- Use tables for structured data like portfolio, funds, orders, and quotes
+- Present numerical values with proper formatting and currency symbols
+- Use clear headings and sections to organize information
+- Make responses visually appealing and easy to scan
+
+# Response Formatting Guidelines:
+
+## For Funds Information:
+Format funds data as a clean table with proper alignment:
+
+```markdown
+## 💰 Account Funds Summary
+
+| **Category** | **Amount (₹)** |
+|--------------|----------------|
+| Available Cash | 808.18 |
+| Collateral | 0.00 |
+| M2M Realized | -24.60 |
+| M2M Unrealized | 0.00 |
+| Utilized Debits | 115.22 |
+
+**Key Insights:**
+- ✅ Available for trading: **₹808.18**
+- 📊 Total utilized: **₹115.22**
+- 📈 Realized P&L: **₹-24.60**
+```
+
+## For Portfolio/Holdings:
+Present holdings in a structured table format:
+
+```markdown
+## 📈 Portfolio Holdings
+
+| **Symbol** | **Exchange** | **Qty** | **Product** | **P&L (₹)** | **P&L %** |
+|------------|--------------|---------|-------------|-------------|-----------|
+| TATASTEEL | NSE | 1 | CNC | 14.00 | 9.79% |
+| CANBANK | NSE | 5 | CNC | 39.00 | 7.61% |
+
+### Portfolio Summary:
+- **Total Holding Value:** ₹715.00
+- **Total Investment:** ₹662.00
+- **Total P&L:** ₹53.61 **(8.09%)**
+- **Number of Holdings:** 2
+```
+
+## For Market Quotes:
+Format quotes with clear price information:
+
+```markdown
+## 📊 NIFTY Market Quote
+
+### Current Price Information:
+- **Last Traded Price (LTP):** ₹24,752.45
+- **Previous Close:** ₹24,826.20
+- **Change:** -₹73.75 **(-0.30%)**
+
+### Market Status:
+- 🔴 **Currently Closed** - No live updates for open, high, low, ask, bid, or volume
+- ⏰ **Next Session:** Regular trading hours
+
+*For detailed market depth and live data, please check during market hours.*
+```
+
+## For Orders:
+Present order information in tables:
+
+```markdown
+## 📋 Order Book
+
+| **Order ID** | **Symbol** | **Action** | **Qty** | **Price** | **Status** | **Time** |
+|--------------|------------|------------|---------|-----------|------------|----------|
+| 12345 | RELIANCE | BUY | 10 | 2,450.00 | COMPLETE | 09:30 AM |
+| 12346 | TCS | SELL | 5 | 3,890.00 | PENDING | 10:15 AM |
+
+### Order Summary:
+- **Total Orders:** 2
+- **Completed:** 1
+- **Pending:** 1
+```
+
+## General Formatting Rules:
+1. Use emoji icons (💰📈📊📋) to make sections visually appealing
+2. Bold important numbers and percentages
+3. Use proper currency symbols (₹ for INR)
+4. Color-code positive/negative values contextually
+5. Include summary sections with key insights
+6. Use consistent table formatting with clear headers
+7. Add explanatory text when data might be confusing
+
+## For Empty or Error Responses:
+When API returns no data or errors:
+
+```markdown
+## ⚠️ Information Not Available
+
+The requested data is currently unavailable. This could be due to:
+- Market is closed
+- No positions/orders exist
+- API connectivity issues
+
+Please try again during market hours or contact support if the issue persists.
+
+# Limitations:
+You are not a financial advisor and should not provide investment advice. Your role is to ensure secure, efficient, and compliant account management.
+""",
+ model=model,
+ add_history_to_messages=True,
+ num_history_responses=10,
+ tools=[mcp_tools],
+ show_tool_calls=False,
+ markdown=True,
+ read_tool_call_history=True,
+ read_chat_history=True,
+ tool_call_limit=10,
+ telemetry=False,
+ add_datetime_to_instructions=True
+ )
+
+ agents[client_id] = agent
+ chat_histories[client_id] = ChatHistory()
+
+ except Exception as e:
+ logger.error(f"Error setting up MCP client: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to connect to MCP server: {str(e)}")
+
+ return mcp_clients[client_id]
+
+@app.get("/", response_class=HTMLResponse)
+async def get_homepage(request: Request):
+ """Serve the main chat interface"""
+ return templates.TemplateResponse("index.html", {"request": request})
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint"""
+ return {"status": "ok", "mcp_server": MCP_URL}
+
+@app.get("/api/status")
+async def get_status():
+ """Get MCP server connection status"""
+ try:
+ # Create a temporary client to check connection
+ temp_client = MCPClient()
+ connected = False
+ try:
+ connected = await temp_client.connect_to_sse_server(MCP_URL)
+ finally:
+ await temp_client.disconnect()
+
+ return {
+ "status": "connected" if connected else "disconnected",
+ "mcp_server": MCP_URL
+ }
+ except Exception as e:
+ logger.error(f"Error checking MCP server status: {str(e)}")
+ return {
+ "status": "error",
+ "message": str(e),
+ "mcp_server": MCP_URL
+ }
+
+@app.websocket("/ws/{client_id}")
+async def websocket_endpoint(websocket: WebSocket, client_id: str):
+ """WebSocket endpoint for chat communication"""
+ await websocket.accept()
+ active_connections[client_id] = websocket
+
+ try:
+ # Send a welcome message
+ await websocket.send_json({
+ "role": "assistant",
+ "content": "Welcome to OpenAlgo Trading Assistant! I'm here to help you manage your trading account, orders, portfolio, and positions. How can I help you today?"
+ })
+
+ # Get or create MCP client
+ mcp_client = await get_mcp_client(client_id)
+
+ while True:
+ try:
+ data = await websocket.receive_text()
+ message = json.loads(data)
+ user_query = message.get("content", "").strip()
+
+ if not user_query:
+ continue
+
+ # Add user message to chat history
+ chat_histories[client_id].messages.append(Message(role="user", content=user_query))
+
+ # Send processing message
+ await websocket.send_json({
+ "role": "system",
+ "content": "Processing your request..."
+ })
+
+ # Get agent response
+ agent = agents[client_id]
+ full_response = ""
+ has_streamed_content = False
+
+ # Run the agent and stream the response
+ try:
+ result = await agent.arun(user_query, stream=True)
+
+ # Check if we get streaming responses
+ async for response in result:
+ if response.content:
+ has_streamed_content = True
+ full_response += response.content
+ # Send partial response
+ await websocket.send_json({
+ "role": "assistant",
+ "content": response.content,
+ "partial": True
+ })
+
+ # Only send complete response if we didn't get any streaming content
+ if not has_streamed_content and full_response:
+ await websocket.send_json({
+ "role": "assistant",
+ "content": full_response,
+ "partial": False
+ })
+ elif has_streamed_content:
+ # Send a signal that streaming is complete (frontend will ignore this)
+ await websocket.send_json({
+ "role": "assistant",
+ "content": "",
+ "partial": False,
+ "streaming_complete": True
+ })
+
+ except Exception as agent_error:
+ logger.error(f"Agent error: {str(agent_error)}")
+ full_response = f"I encountered an error while processing your request: {str(agent_error)}"
+ # Send error as complete message (not streaming)
+ await websocket.send_json({
+ "role": "assistant",
+ "content": full_response,
+ "partial": False
+ })
+
+ # Add assistant message to chat history (use full response)
+ if full_response:
+ chat_histories[client_id].messages.append(Message(role="assistant", content=full_response))
+
+ except json.JSONDecodeError:
+ logger.error(f"Invalid JSON received from client {client_id}")
+ await websocket.send_json({
+ "role": "system",
+ "content": "Error: Invalid message format."
+ })
+ except Exception as e:
+ logger.error(f"Error processing message from {client_id}: {str(e)}")
+ await websocket.send_json({
+ "role": "system",
+ "content": f"Error: {str(e)}"
+ })
+
+ except WebSocketDisconnect:
+ logger.info(f"Client {client_id} disconnected")
+ except Exception as e:
+ logger.error(f"WebSocket error for client {client_id}: {str(e)}")
+ finally:
+ # Clean up resources
+ active_connections.pop(client_id, None)
+
+ if client_id in mcp_clients:
+ try:
+ await mcp_clients[client_id].disconnect()
+ except Exception as e:
+ logger.error(f"Error disconnecting MCP client: {str(e)}")
+ mcp_clients.pop(client_id, None)
+
+ if client_id in agents:
+ agents.pop(client_id, None)
+
+ if client_id in chat_histories:
+ chat_histories.pop(client_id, None)
+
+@app.on_event("shutdown")
+async def shutdown_event():
+ """Clean up resources on shutdown"""
+ logger.info("Shutting down - cleaning up resources")
+ for client_id, mcp_client in list(mcp_clients.items()):
+ try:
+ await mcp_client.disconnect()
+ except Exception as e:
+ logger.error(f"Error disconnecting client {client_id}: {str(e)}")
+
+if __name__ == "__main__":
+ logger.info(f"Starting OpenAlgo Trading Assistant on port 8000")
+ logger.info(f"MCP Server URL: {MCP_URL}")
+ logger.info(f"LLM Provider: {LLM_PROVIDER}")
+
+ uvicorn.run(
+ "app:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info"
+ )
\ No newline at end of file
diff --git a/client/requirements.txt b/client/requirements.txt
deleted file mode 100644
index bab5108..0000000
--- a/client/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-agno>=0.4.0
-mcp>=1.4.0
-python-dotenv>=1.0.0
-rich>=13.4.2
diff --git a/client/static/script.js b/client/static/script.js
new file mode 100644
index 0000000..015f7bc
--- /dev/null
+++ b/client/static/script.js
@@ -0,0 +1,883 @@
+// Enhanced OpenAlgo Trading Assistant JavaScript - Streaming Fix
+
+class TradingAssistant {
+ constructor() {
+ this.socket = null;
+ this.clientId = null;
+ this.isConnected = false;
+ this.chatHistory = [];
+ this.currentTheme = 'synthwave';
+ this.reconnectAttempts = 0;
+ this.maxReconnectAttempts = 5;
+ this.messageQueue = [];
+ this.isTyping = false;
+ this.currentAssistantMessageId = null; // Track current streaming message
+ this.messageIdCounter = 0; // Counter for unique message IDs
+ this.isStreaming = false; // Track if we're currently streaming
+
+ this.init();
+ }
+
+ init() {
+ // Generate unique client ID
+ this.clientId = uuid.v4();
+
+ // Initialize event listeners
+ this.setupEventListeners();
+
+ // Initialize UI components
+ this.initializeUI();
+
+ // Check server status
+ this.checkServerStatus();
+
+ // Auto-connect after a delay
+ setTimeout(() => {
+ this.connectWebSocket();
+ }, 1000);
+
+ // Set up periodic status checks
+ setInterval(() => {
+ if (!this.isConnected) {
+ this.checkServerStatus();
+ }
+ }, 30000);
+ }
+
+ setupEventListeners() {
+ // Message form
+ document.getElementById('message-form').addEventListener('submit', (e) => this.sendMessage(e));
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
+
+ // Input handling
+ const userInput = document.getElementById('user-input');
+ userInput.addEventListener('input', (e) => this.handleInputChange(e));
+ userInput.addEventListener('keydown', (e) => this.handleKeyDown(e));
+
+ // Connection toggle
+ document.getElementById('connect-btn').addEventListener('click', () => this.toggleConnection());
+
+ // Window events
+ window.addEventListener('beforeunload', () => this.cleanup());
+ window.addEventListener('online', () => this.handleOnline());
+ window.addEventListener('offline', () => this.handleOffline());
+ }
+
+ initializeUI() {
+ // Initialize Lucide icons
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+
+ // Set welcome message timestamp
+ const welcomeTime = document.getElementById('welcome-time');
+ if (welcomeTime) {
+ welcomeTime.textContent = new Date().toLocaleTimeString();
+ }
+
+ // Initialize theme
+ this.setTheme(this.currentTheme);
+
+ // Add sample data
+ setTimeout(() => {
+ this.addSampleChatHistory();
+ this.updateMarketOverview();
+ }, 2000);
+ }
+
+ async checkServerStatus() {
+ try {
+ const response = await fetch('/api/status');
+ const data = await response.json();
+
+ const serverUrl = document.getElementById('server-url');
+ if (serverUrl) {
+ serverUrl.textContent = data.mcp_server;
+ }
+
+ if (data.status === 'connected') {
+ this.updateConnectionStatus('ready');
+ } else {
+ this.updateConnectionStatus('disconnected');
+ if (data.status === 'error') {
+ this.showNotification(`Server error: ${data.message}`, 'error');
+ }
+ }
+ } catch (error) {
+ console.error('Error checking server status:', error);
+ this.updateConnectionStatus('disconnected');
+ this.showNotification('Could not connect to the server', 'error');
+ }
+ }
+
+ toggleConnection() {
+ if (this.isConnected) {
+ this.disconnectWebSocket();
+ } else {
+ this.connectWebSocket();
+ }
+ }
+
+ connectWebSocket() {
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
+ return;
+ }
+
+ this.updateConnectionStatus('connecting');
+
+ const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
+ const wsUrl = `${protocol}${window.location.host}/ws/${this.clientId}`;
+
+ try {
+ this.socket = new WebSocket(wsUrl);
+
+ this.socket.onopen = () => {
+ console.log('WebSocket connected');
+ this.isConnected = true;
+ this.reconnectAttempts = 0;
+ this.updateConnectionStatus('connected');
+ this.showNotification('Connected to trading assistant', 'success');
+ this.processMessageQueue();
+ };
+
+ this.socket.onmessage = (event) => {
+ this.handleWebSocketMessage(event);
+ };
+
+ this.socket.onclose = (event) => {
+ console.log('WebSocket disconnected', event);
+ this.isConnected = false;
+ this.updateConnectionStatus('disconnected');
+
+ // Attempt reconnection if not intentionally closed
+ if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.attemptReconnection();
+ }
+ };
+
+ this.socket.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ this.updateConnectionStatus('error');
+ this.showNotification('Connection error occurred', 'error');
+ };
+
+ } catch (error) {
+ console.error('Failed to create WebSocket connection:', error);
+ this.updateConnectionStatus('error');
+ this.showNotification('Failed to establish connection', 'error');
+ }
+ }
+
+ disconnectWebSocket() {
+ if (this.socket) {
+ this.socket.close(1000, 'User initiated disconnect');
+ this.socket = null;
+ }
+ this.isConnected = false;
+ this.updateConnectionStatus('disconnected');
+ }
+
+ attemptReconnection() {
+ this.reconnectAttempts++;
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
+
+ this.showNotification(`Reconnecting in ${delay/1000} seconds... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'warning');
+
+ setTimeout(() => {
+ if (!this.isConnected) {
+ this.connectWebSocket();
+ }
+ }, delay);
+ }
+
+ handleWebSocketMessage(event) {
+ try {
+ const message = JSON.parse(event.data);
+ console.log('Received message:', message); // Debug log
+
+ if (message.role === 'system') {
+ // Only show system messages that are not "Processing your request..."
+ if (message.content !== 'Processing your request...') {
+ this.showNotification(message.content, 'info');
+ }
+ return; // Don't add system messages to chat
+ }
+
+ if (message.role === 'assistant') {
+ this.removeTypingIndicator();
+
+ if (message.partial === true) {
+ // This is a streaming partial response
+ if (!this.isStreaming) {
+ // First streaming chunk - create new message
+ this.isStreaming = true;
+ this.addMessage('assistant', message.content);
+ } else {
+ // Subsequent streaming chunks - append to existing message
+ this.appendToCurrentAssistantMessage(message.content);
+ }
+ } else if (message.partial === false) {
+ // This is the end of streaming or a complete non-streaming message
+ if (this.isStreaming) {
+ // End of streaming - reset streaming state but don't add new message
+ console.log('Streaming complete, resetting state');
+ this.isStreaming = false;
+ this.currentAssistantMessageId = null;
+ } else if (message.content && message.content.trim()) {
+ // This is a complete non-streaming message with content
+ this.addMessage('assistant', message.content);
+ }
+ // Ignore empty final messages
+ }
+ }
+ } catch (error) {
+ console.error('Error parsing WebSocket message:', error);
+ }
+ }
+
+ updateConnectionStatus(status) {
+ const indicator = document.getElementById('connection-indicator');
+ const statusText = document.getElementById('connection-status');
+ const connectBtn = document.getElementById('connect-btn');
+
+ if (!indicator || !statusText || !connectBtn) return;
+
+ // Remove all status classes
+ indicator.className = 'w-3 h-3 rounded-full';
+
+ switch (status) {
+ case 'connected':
+ indicator.classList.add('status-connected');
+ statusText.textContent = 'Connected';
+ connectBtn.innerHTML = 'Disconnect';
+ connectBtn.className = 'btn btn-sm btn-error floating-action';
+ break;
+
+ case 'connecting':
+ indicator.classList.add('status-connecting');
+ statusText.textContent = 'Connecting...';
+ connectBtn.innerHTML = 'Connecting';
+ connectBtn.className = 'btn btn-sm btn-warning floating-action';
+ break;
+
+ case 'ready':
+ indicator.classList.add('status-connecting');
+ statusText.textContent = 'Ready';
+ connectBtn.innerHTML = 'Connect';
+ connectBtn.className = 'btn btn-sm btn-primary floating-action';
+ break;
+
+ case 'error':
+ indicator.classList.add('status-disconnected');
+ statusText.textContent = 'Error';
+ connectBtn.innerHTML = 'Retry';
+ connectBtn.className = 'btn btn-sm btn-warning floating-action';
+ break;
+
+ default:
+ indicator.classList.add('status-disconnected');
+ statusText.textContent = 'Disconnected';
+ connectBtn.innerHTML = 'Connect';
+ connectBtn.className = 'btn btn-sm btn-accent floating-action';
+ }
+
+ // Reinitialize icons
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
+ sendMessage(event) {
+ event.preventDefault();
+
+ const inputElement = document.getElementById('user-input');
+ const message = inputElement.value.trim();
+
+ if (!message) return;
+
+ if (!this.isConnected) {
+ this.showNotification('Please connect to the server first', 'warning');
+ this.messageQueue.push(message);
+ return;
+ }
+
+ // Reset streaming state when user sends new message
+ this.isStreaming = false;
+ this.currentAssistantMessageId = null;
+
+ // Add user message to chat
+ this.addMessage('user', message);
+
+ // Send message to server
+ try {
+ this.socket.send(JSON.stringify({
+ role: 'user',
+ content: message
+ }));
+ } catch (error) {
+ console.error('Error sending message:', error);
+ this.showNotification('Failed to send message', 'error');
+ return;
+ }
+
+ // Clear input and add typing indicator
+ inputElement.value = '';
+ this.resetTextareaHeight(inputElement);
+ this.addTypingIndicator();
+
+ // Store in history
+ this.chatHistory.push({
+ role: 'user',
+ content: message,
+ timestamp: new Date().toISOString()
+ });
+ }
+
+ processMessageQueue() {
+ while (this.messageQueue.length > 0 && this.isConnected) {
+ const message = this.messageQueue.shift();
+ document.getElementById('user-input').value = message;
+ this.sendMessage({ preventDefault: () => {} });
+
+ // Add small delay between queued messages
+ if (this.messageQueue.length > 0) {
+ setTimeout(() => this.processMessageQueue(), 1000);
+ break;
+ }
+ }
+ }
+
+ addMessage(role, content) {
+ this.removeTypingIndicator();
+
+ const messagesContainer = document.getElementById('chat-messages');
+ if (!messagesContainer) return;
+
+ const messageId = `message-${this.messageIdCounter++}`;
+ const chatDiv = document.createElement('div');
+ chatDiv.className = `chat ${role === 'user' ? 'chat-end' : 'chat-start'} message-enter`;
+ chatDiv.setAttribute('data-message-id', messageId);
+
+ const isUser = role === 'user';
+ const timestamp = new Date().toLocaleTimeString();
+
+ chatDiv.innerHTML = `
+
+
+
+ ${role === 'assistant' ? this.parseMarkdown(content) : this.escapeHtml(content)}
+
+ `;
+
+ messagesContainer.appendChild(chatDiv);
+ this.scrollToBottom();
+
+ // Set current assistant message ID for streaming
+ if (role === 'assistant') {
+ this.currentAssistantMessageId = messageId;
+ }
+
+ // Reinitialize icons
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+
+ // Store in history
+ this.chatHistory.push({
+ role,
+ content,
+ timestamp: new Date().toISOString()
+ });
+ }
+
+ addTypingIndicator() {
+ this.removeTypingIndicator();
+ this.isTyping = true;
+
+ const messagesContainer = document.getElementById('chat-messages');
+ if (!messagesContainer) return;
+
+ const typingDiv = document.createElement('div');
+ typingDiv.id = 'typing-indicator';
+ typingDiv.className = 'chat chat-start';
+
+ typingDiv.innerHTML = `
+
+
+ `;
+
+ messagesContainer.appendChild(typingDiv);
+ this.scrollToBottom();
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
+ removeTypingIndicator() {
+ const indicator = document.getElementById('typing-indicator');
+ if (indicator) {
+ indicator.remove();
+ this.isTyping = false;
+ }
+ }
+
+ appendToCurrentAssistantMessage(content) {
+ if (!this.currentAssistantMessageId) {
+ // If no current message, create a new one
+ this.addMessage('assistant', content);
+ return;
+ }
+
+ const messagesContainer = document.getElementById('chat-messages');
+ const currentMessage = messagesContainer.querySelector(`[data-message-id="${this.currentAssistantMessageId}"]`);
+
+ if (!currentMessage) {
+ // If message not found, create a new one
+ this.addMessage('assistant', content);
+ return;
+ }
+
+ const chatBubble = currentMessage.querySelector('.chat-bubble');
+ if (!chatBubble) return;
+
+ // Get current content and append new content
+ const currentContent = this.extractTextContent(chatBubble.innerHTML);
+ const updatedContent = currentContent + content;
+
+ // Update the bubble with the new content
+ chatBubble.innerHTML = this.parseMarkdown(updatedContent);
+
+ this.scrollToBottom();
+
+ // Update chat history
+ if (this.chatHistory.length > 0) {
+ const lastEntry = this.chatHistory[this.chatHistory.length - 1];
+ if (lastEntry.role === 'assistant') {
+ lastEntry.content += content;
+ }
+ }
+ }
+
+ scrollToBottom() {
+ const messagesContainer = document.getElementById('chat-messages');
+ if (messagesContainer) {
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+ }
+ }
+
+ parseMarkdown(content) {
+ if (typeof marked !== 'undefined') {
+ // Configure marked for better table rendering
+ marked.setOptions({
+ gfm: true, // GitHub Flavored Markdown
+ breaks: true,
+ tables: true,
+ headerIds: false
+ });
+
+ // Process table-specific content for better formatting
+ let processedContent = content;
+
+ // Fix malformed tables from GROQ responses
+ if (content.includes('|') && !content.includes('```')) {
+ const lines = content.split('\n');
+ const tableLines = [];
+ let inTable = false;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (line.startsWith('|') && line.endsWith('|')) {
+ if (!inTable) {
+ inTable = true;
+ // If this is first line of table and next line isn't a separator, add one
+ if (i + 1 < lines.length) {
+ const nextLine = lines[i + 1].trim();
+ if (!nextLine.includes('---') && !nextLine.includes('===')) {
+ // Count columns and create separator
+ const colCount = (line.match(/\|/g) || []).length - 1;
+ tableLines.push(line);
+ tableLines.push('|' + Array(colCount).fill(' --- |').join(''));
+ continue;
+ }
+ }
+ }
+ tableLines.push(line);
+ } else if (inTable && line.includes('|')) {
+ // Handle malformed table rows that don't start/end with |
+ tableLines.push('|' + line + (line.endsWith('|') ? '' : '|'));
+ } else if (line === '' && inTable) {
+ inTable = false;
+ } else {
+ tableLines.push(line);
+ }
+ }
+ processedContent = tableLines.join('\n');
+ }
+
+ return marked.parse(processedContent);
+ }
+ return this.escapeHtml(content).replace(/\n/g, '
');
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ extractTextContent(html) {
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ return div.textContent || div.innerText || '';
+ }
+
+ // UI Helper Methods
+ quickMessage(message) {
+ const input = document.getElementById('user-input');
+ if (input) {
+ input.value = message;
+ this.sendMessage({ preventDefault: () => {} });
+ }
+ }
+
+ startNewChat() {
+ this.clientId = uuid.v4();
+ this.currentAssistantMessageId = null;
+ this.messageIdCounter = 0;
+ this.isStreaming = false;
+ this.clearChat();
+ if (!this.isConnected) {
+ this.connectWebSocket();
+ }
+ this.showNotification('Started new chat session', 'success');
+ }
+
+ clearChat() {
+ const messagesContainer = document.getElementById('chat-messages');
+ if (!messagesContainer) return;
+
+ const timestamp = new Date().toLocaleTimeString();
+ messagesContainer.innerHTML = `
+
+
+
+
+ Chat cleared. Ready for new conversation! 🎉
+
+
+ `;
+
+ this.chatHistory = [];
+ this.currentAssistantMessageId = null;
+ this.messageIdCounter = 0;
+ this.isStreaming = false;
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
+ // Event Handlers
+ handleKeyboardShortcuts(event) {
+ // Ctrl/Cmd + K to focus on input
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
+ event.preventDefault();
+ const input = document.getElementById('user-input');
+ if (input) input.focus();
+ }
+
+ // Ctrl/Cmd + N for new chat
+ if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
+ event.preventDefault();
+ this.startNewChat();
+ }
+
+ // Ctrl/Cmd + L to clear chat
+ if ((event.ctrlKey || event.metaKey) && event.key === 'l') {
+ event.preventDefault();
+ this.clearChat();
+ }
+
+ // Escape to clear input
+ if (event.key === 'Escape') {
+ const input = document.getElementById('user-input');
+ if (input) {
+ input.value = '';
+ this.resetTextareaHeight(input);
+ }
+ }
+ }
+
+ handleKeyDown(event) {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ this.sendMessage(event);
+ }
+ }
+
+ handleInputChange(event) {
+ this.autoResizeTextarea(event.target);
+ }
+
+ autoResizeTextarea(textarea) {
+ textarea.style.height = 'auto';
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
+ }
+
+ resetTextareaHeight(textarea) {
+ textarea.style.height = 'auto';
+ }
+
+ handleOnline() {
+ this.showNotification('Connection restored', 'success');
+ if (!this.isConnected) {
+ this.connectWebSocket();
+ }
+ }
+
+ handleOffline() {
+ this.showNotification('Connection lost - you are offline', 'warning');
+ }
+
+ // Notification System
+ showNotification(message, type = 'info', duration = 3000) {
+ const toast = document.createElement('div');
+ toast.className = 'toast toast-top toast-end z-50';
+
+ const alertClass = {
+ 'success': 'alert-success',
+ 'error': 'alert-error',
+ 'warning': 'alert-warning',
+ 'info': 'alert-info'
+ }[type] || 'alert-info';
+
+ const iconName = {
+ 'success': 'check-circle',
+ 'error': 'x-circle',
+ 'warning': 'alert-triangle',
+ 'info': 'info'
+ }[type] || 'info';
+
+ toast.innerHTML = `
+
+
+ ${message}
+
+ `;
+
+ document.body.appendChild(toast);
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+
+ setTimeout(() => {
+ toast.remove();
+ }, duration);
+ }
+
+ // Theme Management
+ setTheme(theme) {
+ this.currentTheme = theme;
+ document.documentElement.setAttribute('data-theme', theme);
+ localStorage.setItem('trading-assistant-theme', theme);
+ }
+
+ toggleTheme() {
+ const themes = ['synthwave', 'cyberpunk', 'dark', 'night', 'forest', 'luxury', 'business', 'cupcake'];
+ const currentIndex = themes.indexOf(this.currentTheme);
+ const nextIndex = (currentIndex + 1) % themes.length;
+ const newTheme = themes[nextIndex];
+
+ this.setTheme(newTheme);
+ this.showNotification(`Switched to ${newTheme} theme`, 'success');
+ }
+
+ // Quick Actions
+ getPortfolio() {
+ this.quickMessage('Show my portfolio and holdings with performance metrics');
+ }
+
+ getFunds() {
+ this.quickMessage('Show my available funds and margin details');
+ }
+
+ getOrders() {
+ this.quickMessage('List all my orders with current status');
+ }
+
+ // Sample Data
+ addSampleChatHistory() {
+ const chatHistoryContainer = document.getElementById('chat-history');
+ if (!chatHistoryContainer) return;
+
+ const sampleChats = [
+ { title: 'Portfolio Analysis - Today', time: '2 mins ago' },
+ { title: 'NIFTY Options Strategy', time: '1 hour ago' },
+ { title: 'Fund Transfer Query', time: '3 hours ago' },
+ { title: 'Order Status Check', time: 'Yesterday' }
+ ];
+
+ sampleChats.forEach((chat, index) => {
+ const chatItem = document.createElement('div');
+ chatItem.className = 'btn btn-ghost btn-sm w-full text-left justify-start mb-1 hover:bg-primary/10';
+ chatItem.innerHTML = `
+
+
+ ${chat.title}
+ ${chat.time}
+
+ `;
+
+ chatItem.onclick = () => {
+ this.showNotification(`Loading ${chat.title}...`, 'info');
+ };
+
+ chatHistoryContainer.appendChild(chatItem);
+ });
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
+ updateMarketOverview() {
+ const marketOverview = document.getElementById('market-overview');
+ if (!marketOverview) return;
+
+ // Simulate real market data
+ const marketData = [
+ { name: 'NIFTY 50', value: '22,458.10', change: '+0.85%', positive: true },
+ { name: 'BANK NIFTY', value: '48,127.30', change: '-0.42%', positive: false },
+ { name: 'SENSEX', value: '74,239.15', change: '+1.12%', positive: true }
+ ];
+
+ marketOverview.innerHTML = '';
+
+ marketData.forEach(item => {
+ const marketItem = document.createElement('div');
+ marketItem.className = `market-ticker ${item.positive ? '' : 'negative'}`;
+ marketItem.innerHTML = `
+
+
+
${item.name}
+
${item.value}
+
+
+
+ `;
+
+ marketOverview.appendChild(marketItem);
+ });
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons();
+ }
+ }
+
+ // Cleanup
+ cleanup() {
+ if (this.socket) {
+ this.socket.close(1000, 'Page unload');
+ }
+ }
+}
+
+// Initialize the application
+document.addEventListener('DOMContentLoaded', () => {
+ window.tradingAssistant = new TradingAssistant();
+});
+
+// Global functions for backward compatibility and HTML onclick handlers
+function startNewChat() {
+ window.tradingAssistant?.startNewChat();
+}
+
+function clearChat() {
+ window.tradingAssistant?.clearChat();
+}
+
+function toggleConnection() {
+ window.tradingAssistant?.toggleConnection();
+}
+
+function quickMessage(message) {
+ window.tradingAssistant?.quickMessage(message);
+}
+
+function getPortfolio() {
+ window.tradingAssistant?.getPortfolio();
+}
+
+function getFunds() {
+ window.tradingAssistant?.getFunds();
+}
+
+function showQuickOrders() {
+ window.tradingAssistant?.showNotification('Quick orders panel coming soon!', 'info');
+}
+
+function placeQuickOrder() {
+ window.tradingAssistant?.showNotification('Quick order placement coming soon!', 'info');
+}
+
+function toggleTheme() {
+ window.tradingAssistant?.toggleTheme();
+}
+
+function showSettings() {
+ window.tradingAssistant?.showNotification('Settings panel coming soon!', 'info');
+}
+
+function showDashboard() {
+ window.tradingAssistant?.showNotification('Dashboard view coming soon!', 'info');
+}
+
+function showPortfolio() {
+ window.tradingAssistant?.quickMessage('Show detailed portfolio analysis with performance metrics');
+}
+
+function showOrders() {
+ window.tradingAssistant?.quickMessage('List all my orders with status and details');
+}
+
+function showAnalytics() {
+ window.tradingAssistant?.showNotification('Analytics dashboard coming soon!', 'info');
+}
+
+function voiceInput() {
+ window.tradingAssistant?.showNotification('Voice input coming soon!', 'info');
+}
\ No newline at end of file
diff --git a/client/static/style.css b/client/static/style.css
new file mode 100644
index 0000000..63cfaca
--- /dev/null
+++ b/client/static/style.css
@@ -0,0 +1,675 @@
+/* Modern Trading Assistant Styles */
+
+/* Custom CSS Variables for Dynamic Theming */
+:root {
+ --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ --gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ --gradient-success: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ --shadow-glow: 0 0 20px rgba(102, 126, 234, 0.3);
+ --border-glass: rgba(255, 255, 255, 0.2);
+ --bg-glass: rgba(255, 255, 255, 0.1);
+}
+
+/* Global Scrollbar Styling */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
+}
+
+*::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+*::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 3px;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+/* Enhanced Animation Classes */
+.fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+.slide-up {
+ animation: slideUp 0.3s ease-out;
+}
+
+.scale-in {
+ animation: scaleIn 0.3s ease-out;
+}
+
+.bounce-in {
+ animation: bounceIn 0.5s ease-out;
+}
+
+.glow-pulse {
+ animation: glowPulse 2s ease-in-out infinite;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes bounceIn {
+ 0% {
+ opacity: 0;
+ transform: scale(0.3);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 70% {
+ transform: scale(0.9);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes glowPulse {
+ 0%, 100% {
+ box-shadow: 0 0 5px rgba(102, 126, 234, 0.3);
+ }
+ 50% {
+ box-shadow: 0 0 20px rgba(102, 126, 234, 0.6);
+ }
+}
+
+/* Enhanced Glass Effect */
+.glass-enhanced {
+ backdrop-filter: blur(20px) saturate(180%);
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+/* Modern Card Styles */
+.modern-card {
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 16px;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.modern-card:hover {
+ transform: translateY(-5px) scale(1.02);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+ border-color: rgba(255, 255, 255, 0.4);
+}
+
+/* Enhanced Button Styles */
+.btn-gradient-primary {
+ background: var(--gradient-primary);
+ border: none;
+ color: white;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.btn-gradient-primary::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: left 0.5s;
+}
+
+.btn-gradient-primary:hover::before {
+ left: 100%;
+}
+
+.btn-gradient-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
+}
+
+/* Chat Message Enhancements */
+.chat-bubble {
+ position: relative;
+ overflow: hidden;
+}
+
+.chat-bubble::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
+ transition: left 0.5s;
+}
+
+.chat-bubble:hover::after {
+ left: 100%;
+}
+
+/* Enhanced Message Animation */
+.message-enter {
+ animation: messageEnter 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+@keyframes messageEnter {
+ 0% {
+ opacity: 0;
+ transform: translateX(-30px) scale(0.8);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
+/* Typing Indicator Enhancement */
+.typing-enhanced {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 12px 16px;
+ background: rgba(102, 126, 234, 0.1);
+ border-radius: 20px;
+ backdrop-filter: blur(10px);
+}
+
+.typing-dot {
+ width: 8px;
+ height: 8px;
+ background: currentColor;
+ border-radius: 50%;
+ animation: typingDot 1.4s infinite ease-in-out;
+}
+
+.typing-dot:nth-child(1) { animation-delay: -0.32s; }
+.typing-dot:nth-child(2) { animation-delay: -0.16s; }
+
+@keyframes typingDot {
+ 0%, 80%, 100% {
+ transform: scale(0);
+ opacity: 0.5;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+/* Status Indicator Enhancements */
+.status-connected {
+ background: radial-gradient(circle, #10b981, #059669);
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
+ animation: statusPulse 2s infinite;
+}
+
+.status-connecting {
+ background: radial-gradient(circle, #f59e0b, #d97706);
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
+ animation: statusPulse 1s infinite;
+}
+
+.status-disconnected {
+ background: radial-gradient(circle, #ef4444, #dc2626);
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
+ animation: statusPulse 2s infinite;
+}
+
+@keyframes statusPulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.7;
+ }
+}
+
+/* Portfolio Card Styles */
+.portfolio-metric {
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
+ border-radius: 12px;
+ padding: 16px;
+ transition: all 0.3s ease;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.portfolio-metric:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.profit-positive {
+ color: #10b981;
+ text-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
+}
+
+.profit-negative {
+ color: #ef4444;
+ text-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
+}
+
+/* Enhanced Form Styles */
+.form-modern {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ transition: all 0.3s ease;
+}
+
+.form-modern:focus {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(102, 126, 234, 0.5);
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+ outline: none;
+}
+
+/* Loading Spinner Enhancement */
+.loading-modern {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(102, 126, 234, 0.3);
+ border-top: 3px solid #667eea;
+ border-radius: 50%;
+ animation: modernSpin 1s linear infinite;
+}
+
+@keyframes modernSpin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Navbar Enhancements */
+.navbar-modern {
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.9), rgba(118, 75, 162, 0.9));
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* Sidebar Enhancements */
+.sidebar-modern {
+ background: linear-gradient(180deg, rgba(248, 249, 250, 0.95), rgba(248, 249, 250, 0.9));
+ backdrop-filter: blur(20px);
+ border-right: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+/* Quick Action Buttons */
+.quick-action {
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.quick-action:hover {
+ transform: translateY(-2px) scale(1.05);
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+/* Market Data Styling */
+.market-ticker {
+ background: linear-gradient(90deg, rgba(16, 185, 129, 0.1), rgba(5, 150, 105, 0.1));
+ border-left: 4px solid #10b981;
+ padding: 12px;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+.market-ticker.negative {
+ background: linear-gradient(90deg, rgba(239, 68, 68, 0.1), rgba(220, 38, 38, 0.1));
+ border-left-color: #ef4444;
+}
+
+/* Toast Notifications */
+.toast-modern {
+ backdrop-filter: blur(10px);
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ animation: toastSlide 0.3s ease-out;
+}
+
+@keyframes toastSlide {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+/* Responsive Enhancements */
+@media (max-width: 768px) {
+ .glass-enhanced {
+ backdrop-filter: blur(10px);
+ }
+
+ .modern-card {
+ border-radius: 12px;
+ }
+
+ .portfolio-metric {
+ padding: 12px;
+ }
+}
+
+/* Dark Mode Specific Styles */
+[data-theme="dark"] .modern-card,
+[data-theme="synthwave"] .modern-card,
+[data-theme="cyberpunk"] .modern-card {
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
+}
+
+/* High Contrast Mode */
+@media (prefers-contrast: high) {
+ .glass-enhanced {
+ background: rgba(0, 0, 0, 0.8);
+ border: 2px solid white;
+ }
+
+ .modern-card {
+ border: 2px solid currentColor;
+ }
+}
+
+/* Reduced Motion */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Print Styles */
+/* Enhanced Table Styles for Chat Messages - Add to client/static/style.css */
+
+/* Improved table formatting in chat bubbles */
+.chat-bubble table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 12px 0;
+ font-size: 0.875rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.chat-bubble th {
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.8), rgba(118, 75, 162, 0.8));
+ color: white;
+ font-weight: 600;
+ text-align: left;
+ padding: 12px 16px;
+ border: none;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.chat-bubble td {
+ padding: 10px 16px;
+ border: none;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ vertical-align: middle;
+ font-size: 0.875rem;
+}
+
+.chat-bubble tr:last-child td {
+ border-bottom: none;
+}
+
+.chat-bubble tr:nth-child(even) {
+ background-color: rgba(255, 255, 255, 0.02);
+}
+
+.chat-bubble tr:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+ transition: background-color 0.2s ease;
+}
+
+/* Responsive table handling */
+@media (max-width: 768px) {
+ .chat-bubble table {
+ font-size: 0.75rem;
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ .chat-bubble th,
+ .chat-bubble td {
+ padding: 8px 12px;
+ min-width: 80px;
+ }
+}
+
+/* Number formatting in tables */
+.chat-bubble td:has([data-type="currency"]),
+.chat-bubble td:has([data-type="number"]),
+.chat-bubble td[data-type="currency"],
+.chat-bubble td[data-type="number"] {
+ text-align: right;
+ font-family: 'Monaco', 'Consolas', monospace;
+ font-weight: 500;
+}
+
+/* Positive/Negative value styling */
+.chat-bubble .positive {
+ color: #10b981;
+ font-weight: 600;
+}
+
+.chat-bubble .negative {
+ color: #ef4444;
+ font-weight: 600;
+}
+
+.chat-bubble .neutral {
+ color: var(--text-secondary);
+}
+
+/* Status indicators */
+.chat-bubble .status-complete {
+ background: #10b981;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.chat-bubble .status-pending {
+ background: #f59e0b;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.chat-bubble .status-rejected {
+ background: #ef4444;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+/* Enhanced markdown headings in chat */
+.chat-bubble h1, .chat-bubble h2, .chat-bubble h3 {
+ color: var(--text-primary);
+ margin: 16px 0 8px 0;
+ font-weight: 700;
+}
+
+.chat-bubble h1 {
+ font-size: 1.25rem;
+ border-bottom: 2px solid rgba(102, 126, 234, 0.3);
+ padding-bottom: 4px;
+}
+
+.chat-bubble h2 {
+ font-size: 1.1rem;
+ color: #667eea;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.chat-bubble h3 {
+ font-size: 1rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+/* Code blocks in chat */
+.chat-bubble pre {
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 6px;
+ padding: 12px;
+ overflow-x: auto;
+ margin: 12px 0;
+ font-size: 0.8rem;
+}
+
+.chat-bubble code {
+ background: rgba(0, 0, 0, 0.2);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: 'Monaco', 'Consolas', monospace;
+ font-size: 0.85rem;
+ color: #a855f7;
+}
+
+.chat-bubble pre code {
+ background: transparent;
+ padding: 0;
+ color: #e5e7eb;
+}
+
+/* List styling in chat */
+.chat-bubble ul, .chat-bubble ol {
+ margin: 12px 0;
+ padding-left: 24px;
+}
+
+.chat-bubble li {
+ margin-bottom: 4px;
+ line-height: 1.5;
+}
+
+.chat-bubble ul li::marker {
+ color: #667eea;
+}
+
+/* Blockquote styling */
+.chat-bubble blockquote {
+ border-left: 4px solid #667eea;
+ padding-left: 16px;
+ margin: 12px 0;
+ font-style: italic;
+ color: var(--text-secondary);
+}
+
+/* Summary boxes */
+.chat-bubble .summary-box {
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
+ border: 1px solid rgba(102, 126, 234, 0.3);
+ border-radius: 8px;
+ padding: 16px;
+ margin: 16px 0;
+}
+
+.chat-bubble .summary-box h3 {
+ margin-top: 0;
+ color: #667eea;
+}
+
+/* Emoji support */
+.chat-bubble .emoji {
+ font-size: 1.2em;
+ margin-right: 4px;
+}
+
+/* Dark mode specific adjustments */
+[data-theme="dark"] .chat-bubble table,
+[data-theme="synthwave"] .chat-bubble table,
+[data-theme="cyberpunk"] .chat-bubble table {
+ background: rgba(0, 0, 0, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+[data-theme="dark"] .chat-bubble td,
+[data-theme="synthwave"] .chat-bubble td,
+[data-theme="cyberpunk"] .chat-bubble td {
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+/* Print-friendly table styles */
+@media print {
+ .chat-bubble table {
+ border: 1px solid #000;
+ background: white !important;
+ }
+
+ .chat-bubble th {
+ background: #f0f0f0 !important;
+ color: #000 !important;
+ border: 1px solid #000;
+ }
+
+ .chat-bubble td {
+ color: #000 !important;
+ border: 1px solid #000;
+ }
+}
\ No newline at end of file
diff --git a/client/templates/index.html b/client/templates/index.html
new file mode 100644
index 0000000..e80a1d7
--- /dev/null
+++ b/client/templates/index.html
@@ -0,0 +1,1379 @@
+
+
+
+
+
+ OpenAlgo Trading Assistant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Welcome to OpenAlgo Trading Assistant! 🚀
+
I'm here to help you with:
+
+ - 📊 Portfolio management and analysis
+ - 📈 Real-time market quotes and data
+ - 💰 Order placement and tracking
+ - 📋 Position monitoring and updates
+ - 💸 Fund information and margin details
+
+
How can I assist you today?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/trading_agent.py b/client/trading_agent.py
deleted file mode 100644
index 41ca005..0000000
--- a/client/trading_agent.py
+++ /dev/null
@@ -1,405 +0,0 @@
-import os
-import sys
-import asyncio
-import logging
-import argparse
-from pathlib import Path
-from agno.models.openai import OpenAIChat
-from agno.agent import Agent
-from agno.tools.mcp import MCPTools
-from mcp import ClientSession
-from rich.console import Console
-from rich.prompt import Prompt
-from rich.theme import Theme
-from typing import Optional
-from contextlib import AsyncExitStack
-from mcp.client.sse import sse_client
-from dotenv import load_dotenv
-
-# Silence all logging
-class SilentFilter(logging.Filter):
- def filter(self, record):
- return False
-
-# Configure root logger to be silent
-root_logger = logging.getLogger()
-root_logger.addFilter(SilentFilter())
-root_logger.setLevel(logging.CRITICAL)
-
-# Silence specific loggers
-for logger_name in ['agno', 'httpx', 'urllib3', 'asyncio']:
- logger = logging.getLogger(logger_name)
- logger.addFilter(SilentFilter())
- logger.setLevel(logging.CRITICAL)
- logger.propagate = False
-
-# Redirect stdout/stderr for the agno library
-class DevNull:
- def write(self, msg): pass
- def flush(self): pass
-
-sys.stderr = DevNull()
-
-# Define custom theme
-custom_theme = Theme({
- "info": "dim cyan",
- "warning": "yellow",
- "danger": "bold red",
- "success": "bold green",
- "query": "bold blue",
- "response": "bold green",
- "assistant": "bold magenta",
- "user": "bold magenta"
-})
-
-# Initialize rich console with custom theme
-console = Console(theme=custom_theme)
-
-# Find and load the common .env file from the parent directory
-parent_dir = Path(__file__).resolve().parent.parent
-env_path = parent_dir / ".env"
-if env_path.exists():
- load_dotenv(dotenv_path=env_path)
- print(f"Loaded environment from {env_path}")
-else:
- # Fall back to local .env file if common one doesn't exist
- load_dotenv()
- print("Loaded environment from local .env file")
-
-class MCPClient:
- def __init__(self):
- # Initialize session and client objects
- self.session: Optional[ClientSession] = None
- self.exit_stack = AsyncExitStack()
-
- async def connect_to_sse_server(self, server_url: str):
- """Connect to an MCP server running with SSE transport"""
- print(f"Attempting to connect to MCP server at {server_url}")
- try:
- # Store the context managers so they stay alive
- self._streams_context = sse_client(url=server_url)
- streams = await self._streams_context.__aenter__()
- print("Successfully created SSE client streams")
-
- self._session_context = ClientSession(*streams)
- self.session: ClientSession = await self._session_context.__aenter__()
- print("Successfully created client session")
-
- # Initialize with detailed error handling
- try:
- await self.session.initialize()
- print("Successfully initialized MCP session")
- except Exception as e:
- print(f"Error during session initialization: {str(e)}")
- if hasattr(e, '__dict__'):
- print(f"Error details: {e.__dict__}")
- raise
-
- # Try to verify connection by listing tools
- try:
- response = await self.session.list_tools()
- print(f"Successfully connected and retrieved tools from server")
- except Exception as e:
- print(f"Error listing tools: {str(e)}")
- if hasattr(e, '__dict__'):
- print(f"Error details: {e.__dict__}")
- raise
-
- except Exception as e:
- print(f"Error connecting to MCP server: {str(e)}")
- if hasattr(e, '__dict__'):
- print(f"Error details: {e.__dict__}")
- raise
-
- async def cleanup(self):
- """Properly clean up the session and streams"""
- if self._session_context:
- await self._session_context.__aexit__(None, None, None)
- if self._streams_context:
- await self._streams_context.__aexit__(None, None, None)
-
- async def disconnect(self):
- """Disconnect from the MCP server"""
- await self.cleanup()
-
-# Helper class for OpenAlgo symbol format assistance
-class SymbolHelper:
- @staticmethod
- def format_equity(symbol, exchange="NSE"):
- """Format equity symbol"""
- return symbol.upper()
-
- @staticmethod
- def format_future(base_symbol, expiry_year, expiry_month, expiry_date=None):
- """Format futures symbol
- Example: BANKNIFTY24APR24FUT
- """
- month_map = {
- 1: "JAN", 2: "FEB", 3: "MAR", 4: "APR", 5: "MAY", 6: "JUN",
- 7: "JUL", 8: "AUG", 9: "SEP", 10: "OCT", 11: "NOV", 12: "DEC"
- }
-
- # Handle month as string or int
- if isinstance(expiry_month, int):
- month_str = month_map[expiry_month]
- else:
- month_str = expiry_month.upper()
-
- # Format the date part
- if expiry_date:
- date_part = f"{expiry_date}"
- else:
- date_part = ""
-
- # Format the year (assuming 2-digit year format)
- if isinstance(expiry_year, int) and expiry_year > 2000:
- year = str(expiry_year)[2:]
- else:
- year = str(expiry_year)
-
- return f"{base_symbol.upper()}{year}{month_str}{date_part}FUT"
-
- @staticmethod
- def format_option(base_symbol, expiry_date, expiry_month, expiry_year, strike_price, option_type):
- """Format options symbol
- Example: NIFTY28MAR2420800CE
- """
- month_map = {
- 1: "JAN", 2: "FEB", 3: "MAR", 4: "APR", 5: "MAY", 6: "JUN",
- 7: "JUL", 8: "AUG", 9: "SEP", 10: "OCT", 11: "NOV", 12: "DEC"
- }
-
- # Handle month as string or int
- if isinstance(expiry_month, int):
- month_str = month_map[expiry_month]
- else:
- month_str = expiry_month.upper()
-
- # Format the year (assuming 2-digit year format)
- if isinstance(expiry_year, int) and expiry_year > 2000:
- year = str(expiry_year)[2:]
- else:
- year = str(expiry_year)
-
- # Format option type (call or put)
- opt_type = "CE" if option_type.upper() in ["C", "CALL", "CE"] else "PE"
-
- # Format strike price (remove decimal if it's a whole number)
- if isinstance(strike_price, (int, float)):
- if strike_price == int(strike_price):
- strike_str = str(int(strike_price))
- else:
- strike_str = str(strike_price)
- else:
- strike_str = strike_price
-
- return f"{base_symbol.upper()}{expiry_date}{month_str}{year}{strike_str}{opt_type}"
-
- @staticmethod
- def get_common_indices(exchange="NSE_INDEX"):
- """Get common index symbols"""
- if exchange.upper() == "NSE_INDEX":
- return ["NIFTY", "BANKNIFTY", "FINNIFTY", "NIFTYNXT50", "MIDCPNIFTY", "INDIAVIX"]
- elif exchange.upper() == "BSE_INDEX":
- return ["SENSEX", "BANKEX", "SENSEX50"]
- return []
-
-async def main():
- # Parse command line arguments
- parser = argparse.ArgumentParser(description='OpenAlgo Trading Assistant')
- parser.add_argument('--host', help='MCP server host (default: uses MCP_HOST from .env)')
- parser.add_argument('--port', type=int, help='MCP server port (default: uses MCP_PORT from .env)')
- parser.add_argument('--model', help='LLM model to use (default: uses OPENAI_MODEL from .env)')
- args = parser.parse_args()
-
- # Get MCP host and port from args or environment variables
- mcp_host = args.host or os.environ.get("MCP_HOST", "localhost")
- mcp_port = args.port or int(os.environ.get("MCP_PORT", "8001"))
- mcp_url = f"http://{mcp_host}:{mcp_port}/sse"
-
- # Get model from args or environment
- model_name = args.model or os.environ.get("OPENAI_MODEL", "gpt-4o")
-
- console.print(f"[info]Connecting to OpenAlgo MCP server at {mcp_url}...[/info]")
-
- mcp_client = MCPClient()
- # Add more detailed error handling during connection
- try:
- console.print(f"[info]Attempting connection to {mcp_url}...[/info]")
- await mcp_client.connect_to_sse_server(mcp_url)
- console.print(f"[success]Successfully connected to MCP server[/success]")
- except Exception as e:
- console.print(f"[danger]Error connecting to MCP server: {str(e)}[/danger]")
- console.print(f"[info]Make sure the server is running with 'python server/server.py'[/info]")
- return
-
- # List available tools with error handling
- try:
- console.print(f"[info]Retrieving available tools from server...[/info]")
- response = await mcp_client.session.list_tools()
- # ListToolsResult doesn't have a __len__ method, so we can't directly call len() on it
- console.print(f"[success]Successfully retrieved tools from server[/success]")
- except Exception as e:
- console.print(f"[danger]Error retrieving tools: {str(e)}[/danger]")
- return
-
- try:
- mcp_tools = MCPTools(session=mcp_client.session)
- await mcp_tools.initialize()
- console.print(f"[success]Successfully initialized MCP tools[/success]")
- except Exception as e:
- console.print(f"[danger]Error initializing MCP tools: {str(e)}[/danger]")
- return
-
- # Create the Agno agent with OpenAI model
- agent = Agent(
- instructions="""
-You are an OpenAlgo Trading Assistant, helping users manage their trading accounts, orders, portfolio, and positions using OpenAlgo API tools provided over MCP.
-
-# Important Instructions:
-- ALWAYS respond in plain text. NEVER use markdown formatting (no asterisks, hashes, or code blocks).
-- Respond in human-like conversational, friendly, and professional tone in concise manner.
-- When market data is requested, always present it in a clean, easy-to-read format.
-- For numerical values like prices and quantities, always display them with appropriate units.
-- Help users construct proper symbol formats based on OpenAlgo's standardized conventions.
-
-# Responsibilities:
-- Assist with order placement, modification, and cancellation
-- Provide insights on portfolio holdings, positions, and orders
-- Track order status, market quotes, and market depth
-- Help with getting historical data and symbol information
-- Assist with retrieving funds and managing positions
-- Guide users on correct OpenAlgo symbol formats for different instruments
-
-# Available Tools:
-
-## Order Management:
-- place_order: Place a new order with support for market, limit, stop-loss orders
-- modify_order: Modify an existing order's price, quantity, or other parameters
-- cancel_order: Cancel a specific order by ID
-- cancel_all_orders: Cancel all open orders
-- get_order_status: Check the status of a specific order
-- get_orders: List all orders
-
-## Advanced Order Types:
-- place_basket_order: Place multiple orders simultaneously
-- place_split_order: Split a large order into smaller chunks to reduce market impact
-- place_smart_order: Place an order that considers current position size
-
-## Market Data:
-- get_quote: Get current market quotes (bid, ask, last price) for a symbol
-- get_depth: Get detailed market depth (order book) for a symbol
-- get_history: Get historical price data for a symbol with various timeframes
-- get_intervals: Get available time intervals for historical data
-
-## Position & Portfolio Management:
-- get_open_position: Get details of an open position for a specific symbol
-- close_all_positions: Close all open positions across all symbols
-- get_position_book: Get all current positions
-- get_holdings: Get portfolio holdings information
-- get_trade_book: Get record of all executed trades
-
-## Account & Configuration:
-- get_funds: Get available funds and margin information
-- get_all_tickers: Get list of all available trading symbols
-- get_symbol_metadata: Get detailed information about a trading symbol
-
-# OpenAlgo Symbol Format Guidelines:
-
-## Exchange Codes:
-- NSE: National Stock Exchange equities
-- BSE: Bombay Stock Exchange equities
-- NFO: NSE Futures and Options
-- BFO: BSE Futures and Options
-- BCD: BSE Currency Derivatives
-- CDS: NSE Currency Derivatives
-- MCX: Multi Commodity Exchange
-- NSE_INDEX: NSE indices
-- BSE_INDEX: BSE indices
-
-## Equity Symbol Format:
-Simply use the base symbol, e.g., "INFY", "SBIN", "TATAMOTORS"
-
-## Future Symbol Format:
-[Base Symbol][Expiration Date]FUT
-Examples:
-- BANKNIFTY24APR24FUT (Bank Nifty futures expiring in April 2024)
-- USDINR10MAY24FUT (USDINR currency futures expiring in May 2024)
-
-## Options Symbol Format:
-[Base Symbol][Expiration Date][Strike Price][Option Type]
-Examples:
-- NIFTY28MAR2420800CE (Nifty call option with 20,800 strike expiring March 28, 2024)
-- VEDL25APR24292.5CE (Vedanta call option with 292.50 strike expiring April 25, 2024)
-
-## Common Index Symbols:
-- NSE_INDEX: NIFTY, BANKNIFTY, FINNIFTY, MIDCPNIFTY, INDIAVIX
-- BSE_INDEX: SENSEX, BANKEX, SENSEX50
-
-# Parameter Guidelines:
-- symbol: Trading symbol following OpenAlgo format
-- exchange: Exchange code (NSE, BSE, NFO, etc.)
-- price_type: "MARKET", "LIMIT", "SL" (stop-loss), "SL-M" (stop-loss market)
-- product: "MIS" (intraday), "CNC" (delivery), "NRML" (normal)
-- action: "BUY" or "SELL"
-- quantity: Number of shares/contracts to trade
-- strategy: Usually "Python" (default)
-
-# Limitations:
-You are not a financial advisor and should not provide investment advice. Your role is to ensure secure, efficient, and compliant account management.
-""",
- model=OpenAIChat(
- id=model_name
- ),
- add_history_to_messages=True,
- num_history_responses=10,
- tools=[mcp_tools],
- show_tool_calls=False,
- markdown=True,
- read_tool_call_history=True,
- read_chat_history=True,
- tool_call_limit=10,
- telemetry=False,
- add_datetime_to_instructions=True
- )
-
- # Welcome message
- console.print()
- console.print("[info]Welcome to OpenAlgo Trading Assistant! I'm here to help you manage your trading account, orders, portfolio, and positions. How can I help you today?[/info]", style="response")
-
- try:
- while True:
- # Add spacing before the prompt
- console.print()
- # Get user input with rich prompt
- user_query = Prompt.ask("[query]Enter your query:[/query] [dim](or 'quit' to exit)[/dim]")
-
- # Check if user wants to quit
- if user_query.lower() == 'quit':
- break
-
- # Add spacing before the prompt
- console.print()
- # Display user query
- console.print(f"[user]You:[/user] {user_query}")
- # Add spacing before the assistant's response
- console.print()
- console.print(f"[assistant]Assistant:[/assistant] ", end="")
-
- # Run the agent and stream the response
- result = await agent.arun(user_query, stream=True)
- async for response in result:
- if response.content:
- console.print(response.content, style="response", end="")
-
- console.print() # Add newline after the full response
- console.print() # Add extra spacing after the response
-
- except Exception as e:
- console.print(f"[danger]An error occurred: {str(e)}[/danger]")
- finally:
- # Disconnect from the MCP server
- await mcp_client.disconnect()
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/requirements.txt b/requirements.txt
index faf9966..20a14f4 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/server/requirements.txt b/server/requirements.txt
deleted file mode 100644
index 678f87b..0000000
Binary files a/server/requirements.txt and /dev/null differ
diff --git a/server/server.py b/server/server.py
index 9588096..5e3c954 100644
--- a/server/server.py
+++ b/server/server.py
@@ -5,23 +5,24 @@
import sys
from pathlib import Path
from starlette.applications import Starlette
-from starlette.routing import Mount
+from starlette.routing import Mount, Route
import uvicorn
import logging
import argparse
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
# Find and load the common .env file from the parent directory
parent_dir = Path(__file__).resolve().parent.parent
env_path = parent_dir / ".env"
if env_path.exists():
load_dotenv(dotenv_path=env_path)
- logging.info(f"Loaded environment from {env_path}")
+ logger.info(f"Loaded environment from {env_path}")
else:
load_dotenv() # Fall back to local .env file if common one doesn't exist
- logging.info("Loaded environment from local .env file")
+ logger.info("Loaded environment from local .env file")
# Parse command line arguments
parser = argparse.ArgumentParser(description='OpenAlgo MCP Server')
@@ -41,24 +42,13 @@
if not API_KEY:
raise ValueError("OPENALGO_API_KEY must be set either in .env file or via command line arguments")
-# Set up detailed logging for OpenAlgo API requests
-class APIDebugHandler(logging.Handler):
- def emit(self, record):
- if record.levelno >= logging.INFO:
- print(f"[{record.levelname}] {record.getMessage()}")
-
-logger = logging.getLogger()
-logger.addHandler(APIDebugHandler())
-
# Initialize OpenAlgo API client
-logging.info(f"Initializing OpenAlgo client with host: {API_HOST} and API key: {API_KEY[:5]}...{API_KEY[-5:]}")
+logger.info(f"Initializing OpenAlgo client with host: {API_HOST}")
try:
client = api(api_key=API_KEY, host=API_HOST)
- logging.info(f"Successfully initialized OpenAlgo client")
+ logger.info(f"Successfully initialized OpenAlgo client")
except Exception as e:
- logging.error(f"Error initializing OpenAlgo client: {str(e)}")
- logger = logging.getLogger(logger_name)
- logger.propagate = True
+ logger.error(f"Error initializing OpenAlgo client: {str(e)}")
raise
# Create an MCP server called "openalgo"
@@ -72,6 +62,326 @@ def emit(self, record):
- Accessing historical data
""")
+# Add this to your server/server.py file before the tool definitions
+
+import json
+from typing import Dict, List, Any
+
+class ResponseFormatter:
+ """Helper class to format API responses for better readability"""
+
+ @staticmethod
+ def format_funds(response: dict) -> str:
+ """Format funds response into readable markdown table"""
+ try:
+ if isinstance(response, str):
+ response = json.loads(response)
+
+ if response.get('status') != 'success':
+ return f"❌ **Error:** {response.get('message', 'Unable to fetch funds data')}"
+
+ data = response.get('data', {})
+
+ # Format the table
+ formatted = """## 💰 Account Funds Summary
+
+| **Category** | **Amount (₹)** |
+|--------------|----------------|"""
+
+ # Add each fund category
+ categories = [
+ ('Available Cash', data.get('availablecash', '0.00')),
+ ('Collateral', data.get('collateral', '0.00')),
+ ('M2M Realized', data.get('m2mrealized', '0.00')),
+ ('M2M Unrealized', data.get('m2munrealized', '0.00')),
+ ('Utilized Debits', data.get('utilizeddebits', '0.00'))
+ ]
+
+ for category, amount in categories:
+ formatted += f"\n| {category} | {amount} |"
+
+ # Add summary
+ available = float(data.get('availablecash', 0))
+ utilized = float(data.get('utilizeddebits', 0))
+ m2m_realized = float(data.get('m2mrealized', 0))
+
+ formatted += f"""
+
+### 📊 Key Insights:
+- ✅ **Available for trading:** ₹{available:,.2f}
+- 📈 **Total utilized:** ₹{utilized:,.2f}
+- 📊 **Realized P&L:** ₹{m2m_realized:,.2f}"""
+
+ if m2m_realized > 0:
+ formatted += " 🟢"
+ elif m2m_realized < 0:
+ formatted += " 🔴"
+
+ return formatted
+
+ except Exception as e:
+ return f"❌ **Error formatting funds data:** {str(e)}"
+
+ @staticmethod
+ def format_holdings(response: dict) -> str:
+ """Format holdings response into readable markdown table"""
+ try:
+ if isinstance(response, str):
+ response = json.loads(response)
+
+ if response.get('status') != 'success':
+ return f"❌ **Error:** {response.get('message', 'Unable to fetch holdings data')}"
+
+ data = response.get('data', [])
+
+ if not data:
+ return """## 📈 Portfolio Holdings
+
+**No holdings found in your portfolio.**
+
+Consider adding some positions to start building your portfolio! 🚀"""
+
+ # Format the table
+ formatted = """## 📈 Portfolio Holdings
+
+| **Symbol** | **Exchange** | **Qty** | **Product** | **P&L (₹)** | **P&L %** |
+|------------|--------------|---------|-------------|-------------|-----------|"""
+
+ total_investment = 0
+ total_current_value = 0
+
+ for holding in data:
+ symbol = holding.get('symbol', 'N/A')
+ exchange = holding.get('exchange', 'N/A')
+ qty = holding.get('quantity', 0)
+ product = holding.get('product', 'N/A')
+ pnl = float(holding.get('pnl', 0))
+ pnl_percent = float(holding.get('pnlpercent', 0))
+
+ # Add color coding for P&L
+ pnl_color = "🟢" if pnl > 0 else "🔴" if pnl < 0 else "⚪"
+
+ formatted += f"\n| {symbol} | {exchange} | {qty} | {product} | {pnl:+.2f} {pnl_color} | {pnl_percent:+.2f}% |"
+
+ # Calculate totals (if available)
+ if 'investment' in holding:
+ total_investment += float(holding.get('investment', 0))
+ if 'currentvalue' in holding:
+ total_current_value += float(holding.get('currentvalue', 0))
+
+ # Add summary if we have the data
+ if total_investment > 0:
+ total_pnl = total_current_value - total_investment
+ total_pnl_percent = (total_pnl / total_investment) * 100 if total_investment > 0 else 0
+
+ pnl_emoji = "🟢" if total_pnl > 0 else "🔴" if total_pnl < 0 else "⚪"
+
+ formatted += f"""
+
+### 📊 Portfolio Summary:
+- **Total Holdings:** {len(data)}
+- **Total Investment:** ₹{total_investment:,.2f}
+- **Current Value:** ₹{total_current_value:,.2f}
+- **Total P&L:** ₹{total_pnl:+,.2f} ({total_pnl_percent:+.2f}%) {pnl_emoji}"""
+
+ return formatted
+
+ except Exception as e:
+ return f"❌ **Error formatting holdings data:** {str(e)}"
+
+ @staticmethod
+ def format_quote(response: dict, symbol: str) -> str:
+ """Format quote response into readable format"""
+ try:
+ if isinstance(response, str):
+ response = json.loads(response)
+
+ if response.get('status') != 'success':
+ return f"❌ **Error:** {response.get('message', f'Unable to fetch quote for {symbol}')}"
+
+ data = response.get('data', {})
+
+ formatted = f"""## 📊 {symbol.upper()} Market Quote
+
+### Current Price Information:"""
+
+ # Main price info
+ ltp = data.get('ltp', 'N/A')
+ prev_close = data.get('close', 'N/A')
+
+ if ltp != 'N/A' and prev_close != 'N/A':
+ try:
+ change = float(ltp) - float(prev_close)
+ change_percent = (change / float(prev_close)) * 100
+ change_emoji = "🟢" if change > 0 else "🔴" if change < 0 else "⚪"
+
+ formatted += f"""
+- **Last Traded Price (LTP):** ₹{ltp}
+- **Previous Close:** ₹{prev_close}
+- **Change:** ₹{change:+.2f} ({change_percent:+.2f}%) {change_emoji}"""
+ except:
+ formatted += f"""
+- **Last Traded Price (LTP):** ₹{ltp}
+- **Previous Close:** ₹{prev_close}"""
+ else:
+ formatted += f"""
+- **Last Traded Price (LTP):** ₹{ltp}
+- **Previous Close:** ₹{prev_close}"""
+
+ # Additional data if available
+ if data.get('open') != 'N/A':
+ formatted += f"\n- **Open:** ₹{data.get('open')}"
+ if data.get('high') != 'N/A':
+ formatted += f"\n- **High:** ₹{data.get('high')}"
+ if data.get('low') != 'N/A':
+ formatted += f"\n- **Low:** ₹{data.get('low')}"
+
+ # Market status
+ formatted += """
+
+### Market Status:
+- ⏰ **Real-time data** (during market hours)
+- 📊 Use this data for informed trading decisions"""
+
+ return formatted
+
+ except Exception as e:
+ return f"❌ **Error formatting quote data:** {str(e)}"
+
+ @staticmethod
+ def format_orders(response: dict) -> str:
+ """Format orders response into readable markdown table"""
+ try:
+ if isinstance(response, str):
+ response = json.loads(response)
+
+ if response.get('status') != 'success':
+ return f"❌ **Error:** {response.get('message', 'Unable to fetch orders data')}"
+
+ data = response.get('data', [])
+
+ if not data:
+ return """## 📋 Order Book
+
+**No orders found.**
+
+Place your first order to start trading! 🚀"""
+
+ # Format the table
+ formatted = """## 📋 Order Book
+
+| **Order ID** | **Symbol** | **Action** | **Qty** | **Price** | **Status** | **Time** |
+|--------------|------------|------------|---------|-----------|------------|----------|"""
+
+ status_counts = {'complete': 0, 'pending': 0, 'rejected': 0, 'cancelled': 0}
+
+ for order in data:
+ order_id = str(order.get('orderid', 'N/A'))[:8] + '...' if len(str(order.get('orderid', ''))) > 8 else str(order.get('orderid', 'N/A'))
+ symbol = order.get('symbol', 'N/A')
+ action = order.get('action', 'N/A')
+ qty = order.get('quantity', 'N/A')
+ price = order.get('price', 'N/A')
+ status = order.get('status', 'N/A').lower()
+ order_time = order.get('time', 'N/A')
+
+ # Count statuses
+ if status in status_counts:
+ status_counts[status] += 1
+
+ # Format status with emoji
+ status_emoji = {
+ 'complete': '✅',
+ 'pending': '⏳',
+ 'rejected': '❌',
+ 'cancelled': '🚫'
+ }.get(status, '❓')
+
+ formatted += f"\n| {order_id} | {symbol} | {action} | {qty} | ₹{price} | {status_emoji} {status.title()} | {order_time} |"
+
+ # Add summary
+ total_orders = len(data)
+ formatted += f"""
+
+### 📊 Order Summary:
+- **Total Orders:** {total_orders}"""
+
+ for status, count in status_counts.items():
+ if count > 0:
+ status_emoji = {
+ 'complete': '✅',
+ 'pending': '⏳',
+ 'rejected': '❌',
+ 'cancelled': '🚫'
+ }.get(status, '❓')
+ formatted += f"\n- **{status.title()}:** {count} {status_emoji}"
+
+ return formatted
+
+ except Exception as e:
+ return f"❌ **Error formatting orders data:** {str(e)}"
+
+# Update the tool functions to use the formatter
+
+@mcp.tool()
+def get_funds() -> str:
+ """Get available funds and margin information."""
+ try:
+ logger.info("Getting funds information")
+ result = client.funds()
+ logger.info("Successfully retrieved funds information")
+
+ # Use the formatter for better output
+ return ResponseFormatter.format_funds(result)
+ except Exception as e:
+ logger.error(f"Error getting funds: {str(e)}")
+ return f"❌ **Error getting funds:** {str(e)}"
+
+@mcp.tool()
+def get_holdings() -> str:
+ """Get portfolio holdings."""
+ try:
+ logger.info("Getting holdings")
+ result = client.holdings()
+
+ # Use the formatter for better output
+ return ResponseFormatter.format_holdings(result)
+ except Exception as e:
+ logger.error(f"Error fetching holdings: {str(e)}")
+ return f"❌ **Error fetching holdings:** {str(e)}"
+
+@mcp.tool()
+def get_quote(symbol: str, exchange: str = "NSE") -> str:
+ """
+ Get market quotes for a symbol.
+
+ Args:
+ symbol: Trading symbol (e.g., SBIN, RELIANCE)
+ exchange: Exchange (NSE, BSE, etc.)
+ """
+ try:
+ logger.info(f"Getting quotes for {symbol.upper()} on {exchange.upper()}")
+ quote = client.quotes(symbol=symbol.upper(), exchange=exchange.upper())
+ logger.info(f"Successfully retrieved quotes for {symbol}")
+
+ # Use the formatter for better output
+ return ResponseFormatter.format_quote(quote, symbol)
+ except Exception as e:
+ logger.error(f"Error getting quotes for {symbol}: {str(e)}")
+ return f"❌ **Error getting quotes for {symbol}:** {str(e)}"
+
+@mcp.tool()
+def get_orders() -> str:
+ """Get all orders for the current strategy."""
+ try:
+ logger.info("Getting orders")
+ result = client.orderbook()
+
+ # Use the formatter for better output
+ return ResponseFormatter.format_orders(result)
+ except Exception as e:
+ logger.error(f"Error getting orders: {str(e)}")
+ return f"❌ **Error getting orders:** {str(e)}"
+
@mcp.tool()
def place_order(symbol: str, quantity: int, action: str, exchange: str = "NSE", price_type: str = "MARKET", product: str = "MIS", strategy: str = "Python", price: float = 0.0, trigger_price: float = 0.0, disclosed_quantity: int = 0) -> str:
"""
@@ -90,7 +400,7 @@ def place_order(symbol: str, quantity: int, action: str, exchange: str = "NSE",
disclosed_quantity: Disclosed quantity
"""
try:
- logging.info(f"Placing order: {action} {quantity} {symbol} on {exchange} as {price_type} for {product}")
+ logger.info(f"Placing order: {action} {quantity} {symbol} on {exchange} as {price_type} for {product}")
response = client.placeorder(
strategy=strategy,
symbol=symbol.upper(),
@@ -105,7 +415,7 @@ def place_order(symbol: str, quantity: int, action: str, exchange: str = "NSE",
)
return f"Order placed: {response}"
except Exception as e:
- logging.error(f"Error placing order: {str(e)}")
+ logger.error(f"Error placing order: {str(e)}")
return f"Error placing order: {str(e)}"
@mcp.tool()
@@ -118,60 +428,62 @@ def get_quote(symbol: str, exchange: str = "NSE") -> str:
exchange: Exchange (NSE, BSE, etc.)
"""
try:
- # Log the request parameters
- logging.info(f"QUOTES REQUEST - Symbol: {symbol.upper()}, Exchange: {exchange.upper()}, API Key: {API_KEY[:5]}...{API_KEY[-5:]}")
-
- # Make the API call with debug=True to log the raw HTTP request
+ logger.info(f"Getting quotes for {symbol.upper()} on {exchange.upper()}")
quote = client.quotes(symbol=symbol.upper(), exchange=exchange.upper())
-
- # Log the success response
- logging.info(f"QUOTES RESPONSE - Success: {quote}")
+ logger.info(f"Successfully retrieved quotes for {symbol}")
return str(quote)
except Exception as e:
- # Log the detailed error
- logging.error(f"QUOTES ERROR - {str(e)}")
- import traceback
- logging.error(f"QUOTES TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting quotes for {symbol}: {str(e)}")
return f"Error getting quotes: {str(e)}"
@mcp.tool()
def get_depth(symbol: str, exchange: str = "NSE") -> str:
+ """Get market depth for a symbol."""
try:
- return str(client.depth(symbol=symbol.upper(), exchange=exchange.upper()))
+ logger.info(f"Getting market depth for {symbol.upper()} on {exchange.upper()}")
+ result = client.depth(symbol=symbol.upper(), exchange=exchange.upper())
+ return str(result)
except Exception as e:
- return f"Error: {str(e)}"
+ logger.error(f"Error getting market depth: {str(e)}")
+ return f"Error getting market depth: {str(e)}"
@mcp.tool()
def get_history(symbol: str, exchange: str, interval: str, start_date: str, end_date: str) -> str:
+ """Get historical data for a symbol."""
try:
- return str(client.history(symbol=symbol.upper(), exchange=exchange.upper(), interval=interval, start_date=start_date, end_date=end_date))
+ logger.info(f"Getting history for {symbol.upper()} from {start_date} to {end_date}")
+ result = client.history(
+ symbol=symbol.upper(),
+ exchange=exchange.upper(),
+ interval=interval,
+ start_date=start_date,
+ end_date=end_date
+ )
+ return str(result)
except Exception as e:
+ logger.error(f"Error fetching history: {str(e)}")
return f"Error fetching history: {str(e)}"
@mcp.tool()
def get_intervals() -> str:
"""Get available intervals for historical data."""
try:
- logging.info("Getting available intervals")
+ logger.info("Getting available intervals")
result = client.intervals()
return str(result)
except Exception as e:
- logging.error(f"Error getting intervals: {str(e)}")
- import traceback
- logging.error(f"INTERVALS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting intervals: {str(e)}")
return f"Error getting intervals: {str(e)}"
@mcp.tool()
def get_symbol_metadata(symbol: str, exchange: str) -> str:
"""Get metadata for a specific symbol."""
try:
- logging.info(f"Getting metadata for {symbol.upper()} on {exchange.upper()}")
+ logger.info(f"Getting metadata for {symbol.upper()} on {exchange.upper()}")
result = client.symbol(symbol=symbol.upper(), exchange=exchange.upper())
return str(result)
except Exception as e:
- logging.error(f"Error getting symbol metadata: {str(e)}")
- import traceback
- logging.error(f"SYMBOL METADATA TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting symbol metadata: {str(e)}")
return f"Error getting symbol metadata: {str(e)}"
@mcp.tool()
@@ -190,42 +502,30 @@ def get_all_tickers(exchange: str = None) -> str:
result = client.ticker(**params)
return str(result)
except Exception as e:
- logging.error(f"Error fetching tickers: {str(e)}")
+ logger.error(f"Error fetching tickers: {str(e)}")
return f"Error fetching tickers: {str(e)}"
@mcp.tool()
def get_funds() -> str:
- """
- Get available funds and margin information.
- """
+ """Get available funds and margin information."""
try:
- # Log the request
- logging.info(f"FUNDS REQUEST - API Key: {API_KEY[:5]}...{API_KEY[-5:]}")
-
- # Make the API call
+ logger.info("Getting funds information")
result = client.funds()
-
- # Log the success response
- logging.info(f"FUNDS RESPONSE - Success: {result}")
+ logger.info("Successfully retrieved funds information")
return str(result)
except Exception as e:
- # Log the detailed error
- logging.error(f"FUNDS ERROR - {str(e)}")
- import traceback
- logging.error(f"FUNDS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting funds: {str(e)}")
return f"Error getting funds: {str(e)}"
@mcp.tool()
def get_orders() -> str:
"""Get all orders for the current strategy."""
try:
- logging.info("Getting orders for strategy Python")
+ logger.info("Getting orders")
result = client.orderbook()
return str(result)
except Exception as e:
- logging.error(f"Error getting orders: {str(e)}")
- import traceback
- logging.error(f"ORDERS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting orders: {str(e)}")
return f"Error getting orders: {str(e)}"
@mcp.tool()
@@ -249,7 +549,7 @@ def modify_order(order_id: str, symbol: str, quantity: int, price: float, action
params = {
"order_id": order_id,
"strategy": strategy,
- "symbol": symbol,
+ "symbol": symbol.upper(),
"quantity": quantity,
"price": price
}
@@ -266,124 +566,110 @@ def modify_order(order_id: str, symbol: str, quantity: int, price: float, action
if trigger_price is not None:
params["trigger_price"] = trigger_price
- logging.info(f"Modifying order {order_id}: {params}")
+ logger.info(f"Modifying order {order_id}")
result = client.modifyorder(**params)
return str(result)
except Exception as e:
- logging.error(f"Error modifying order: {str(e)}")
+ logger.error(f"Error modifying order: {str(e)}")
return f"Error modifying order: {str(e)}"
@mcp.tool()
def cancel_order(order_id: str) -> str:
"""Cancel a specific order by ID."""
try:
- logging.info(f"Cancelling order {order_id}")
+ logger.info(f"Cancelling order {order_id}")
result = client.cancelorder(order_id=order_id, strategy="Python")
return str(result)
except Exception as e:
- logging.error(f"Error cancelling order: {str(e)}")
- import traceback
- logging.error(f"CANCEL ORDER TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error cancelling order: {str(e)}")
return f"Error cancelling order: {str(e)}"
@mcp.tool()
def cancel_all_orders() -> str:
"""Cancel all open orders for the current strategy."""
try:
- logging.info("Cancelling all orders for strategy Python")
+ logger.info("Cancelling all orders")
result = client.cancelallorder(strategy="Python")
return str(result)
except Exception as e:
- logging.error(f"Error cancelling all orders: {str(e)}")
- import traceback
- logging.error(f"CANCEL ALL ORDERS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error cancelling all orders: {str(e)}")
return f"Error cancelling all orders: {str(e)}"
@mcp.tool()
def get_order_status(order_id: str) -> str:
"""Get status of a specific order by ID."""
try:
- logging.info(f"Getting status for order {order_id}")
+ logger.info(f"Getting status for order {order_id}")
result = client.orderstatus(order_id=order_id, strategy="Python")
return str(result)
except Exception as e:
- logging.error(f"Error getting order status: {str(e)}")
- import traceback
- logging.error(f"ORDER STATUS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting order status: {str(e)}")
return f"Error getting order status: {str(e)}"
@mcp.tool()
def get_open_position(symbol: str, exchange: str, product: str) -> str:
"""Get details of an open position for a specific symbol."""
try:
- logging.info(f"Getting open position for {symbol} on {exchange} with product {product}")
+ logger.info(f"Getting open position for {symbol}")
result = client.openposition(strategy="Python", symbol=symbol, exchange=exchange, product=product)
return str(result)
except Exception as e:
- logging.error(f"Error getting open position: {str(e)}")
- import traceback
- logging.error(f"OPEN POSITION TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting open position: {str(e)}")
return f"Error getting open position: {str(e)}"
@mcp.tool()
def close_all_positions() -> str:
"""Close all open positions for the current strategy."""
try:
- logging.info("Closing all positions for strategy Python")
+ logger.info("Closing all positions")
result = client.closeposition(strategy="Python")
return str(result)
except Exception as e:
- logging.error(f"Error closing all positions: {str(e)}")
- import traceback
- logging.error(f"CLOSE POSITIONS TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error closing all positions: {str(e)}")
return f"Error closing all positions: {str(e)}"
@mcp.tool()
def get_position_book() -> str:
"""Get details of all current positions."""
try:
- logging.info("Getting position book")
+ logger.info("Getting position book")
result = client.positionbook()
return str(result)
except Exception as e:
- logging.error(f"Error getting position book: {str(e)}")
- import traceback
- logging.error(f"POSITION BOOK TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting position book: {str(e)}")
return f"Error getting position book: {str(e)}"
@mcp.tool()
def get_order_book() -> str:
"""Get details of all orders."""
try:
- logging.info("Getting order book")
+ logger.info("Getting order book")
result = client.orderbook()
return str(result)
except Exception as e:
- logging.error(f"Error getting order book: {str(e)}")
- import traceback
- logging.error(f"ORDER BOOK TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting order book: {str(e)}")
return f"Error getting order book: {str(e)}"
@mcp.tool()
def get_trade_book() -> str:
"""Get details of all executed trades."""
try:
- logging.info("Getting trade book")
+ logger.info("Getting trade book")
result = client.tradebook()
return str(result)
except Exception as e:
- logging.error(f"Error getting trade book: {str(e)}")
- import traceback
- logging.error(f"TRADE BOOK TRACEBACK: {traceback.format_exc()}")
+ logger.error(f"Error getting trade book: {str(e)}")
return f"Error getting trade book: {str(e)}"
@mcp.tool()
def get_holdings() -> str:
+ """Get portfolio holdings."""
try:
+ logger.info("Getting holdings")
result = client.holdings()
return str(result)
except Exception as e:
- logging.error(f"Error fetching holdings: {str(e)}")
+ logger.error(f"Error fetching holdings: {str(e)}")
return f"Error fetching holdings: {str(e)}"
@mcp.tool()
@@ -392,61 +678,23 @@ def place_basket_order(orders: list) -> str:
Place multiple orders at once using basket order functionality.
Args:
- orders: List of order dictionaries with required fields:
- - symbol: Trading symbol
- - exchange: Exchange (NSE, BSE, etc.)
- - action: BUY or SELL
- - quantity: Order quantity
- - pricetype: MARKET, LIMIT, SL, SL-M
- - product: MIS, CNC, NRML
-
- Example input format:
- [
- {
- "symbol": "SBIN",
- "exchange": "NSE",
- "action": "BUY",
- "quantity": 1,
- "pricetype": "MARKET",
- "product": "MIS"
- },
- {
- "symbol": "RELIANCE",
- "exchange": "NSE",
- "action": "SELL",
- "quantity": 1,
- "pricetype": "MARKET",
- "product": "MIS"
- }
- ]
+ orders: List of order dictionaries with required fields
"""
try:
- logging.info(f"Placing basket order with {len(orders)} orders")
+ logger.info(f"Placing basket order with {len(orders)} orders")
response = client.basketorder(orders=orders)
return str(response)
except Exception as e:
- logging.error(f"Error placing basket order: {str(e)}")
+ logger.error(f"Error placing basket order: {str(e)}")
return f"Error placing basket order: {str(e)}"
@mcp.tool()
def place_split_order(symbol: str, exchange: str, action: str, quantity: int, splitsize: int, price_type: str = "MARKET", product: str = "MIS", price: float = 0, trigger_price: float = 0, strategy: str = "Python") -> str:
"""
Split a large order into multiple smaller orders to reduce market impact.
-
- Args:
- symbol: Trading symbol
- exchange: Exchange (NSE, BSE, etc.)
- action: BUY or SELL
- quantity: Total order quantity
- splitsize: Size of each split order
- price_type: MARKET, LIMIT, SL, SL-M
- product: MIS, CNC, NRML
- price: Order price (for LIMIT orders)
- trigger_price: Trigger price (for SL orders)
- strategy: Strategy name (default: Python)
"""
try:
- logging.info(f"Placing split order: {action} {quantity} {symbol} (split size: {splitsize})")
+ logger.info(f"Placing split order: {action} {quantity} {symbol} (split size: {splitsize})")
params = {
"symbol": symbol.upper(),
"exchange": exchange.upper(),
@@ -454,7 +702,8 @@ def place_split_order(symbol: str, exchange: str, action: str, quantity: int, sp
"quantity": quantity,
"splitsize": splitsize,
"price_type": price_type.upper(),
- "product": product.upper()
+ "product": product.upper(),
+ "strategy": strategy
}
# Add optional parameters if relevant
@@ -462,32 +711,20 @@ def place_split_order(symbol: str, exchange: str, action: str, quantity: int, sp
params["price"] = price
if trigger_price and price_type.upper() in ["SL", "SL-M"]:
params["trigger_price"] = trigger_price
- if strategy:
- params["strategy"] = strategy
response = client.splitorder(**params)
return str(response)
except Exception as e:
- logging.error(f"Error placing split order: {str(e)}")
+ logger.error(f"Error placing split order: {str(e)}")
return f"Error placing split order: {str(e)}"
@mcp.tool()
def place_smart_order(symbol: str, action: str, quantity: int, position_size: int, exchange: str = "NSE", price_type: str = "MARKET", product: str = "MIS", strategy: str = "Python") -> str:
"""
Place a smart order that considers the current position size.
-
- Args:
- symbol: Trading symbol
- action: BUY or SELL
- quantity: Order quantity
- position_size: Current position size
- exchange: Exchange (NSE, BSE, etc.)
- price_type: MARKET, LIMIT, SL, SL-M
- product: MIS, CNC, NRML
- strategy: Strategy name (default: Python)
"""
try:
- logging.info(f"Placing smart order: {action} {quantity} {symbol} with position size {position_size}")
+ logger.info(f"Placing smart order: {action} {quantity} {symbol}")
response = client.placesmartorder(
strategy=strategy,
symbol=symbol.upper(),
@@ -500,12 +737,11 @@ def place_smart_order(symbol: str, action: str, quantity: int, position_size: in
)
return str(response)
except Exception as e:
- logging.error(f"Error placing smart order: {str(e)}")
+ logger.error(f"Error placing smart order: {str(e)}")
return f"Error placing smart order: {str(e)}"
# Create a Starlette app for the SSE transport
if MODE == 'sse':
- from starlette.routing import Route
from mcp.server.sse import SseServerTransport
# Create an SSE transport on the /messages/ endpoint
@@ -513,43 +749,53 @@ def place_smart_order(symbol: str, action: str, quantity: int, position_size: in
# Define an async SSE handler that will process incoming connections
async def handle_sse(request):
- logging.info(f"New SSE connection from {request.client}")
- async with sse.connect_sse(request.scope, request.receive, request._send) as streams:
- await mcp._mcp_server.run(
- streams[0],
- streams[1],
- mcp._mcp_server.create_initialization_options(),
- )
+ logger.info(f"New SSE connection from {request.client}")
+ try:
+ async with sse.connect_sse(request.scope, request.receive, request._send) as streams:
+ await mcp._mcp_server.run(
+ streams[0],
+ streams[1],
+ mcp._mcp_server.create_initialization_options(),
+ )
+ except Exception as e:
+ logger.error(f"Error in SSE handler: {str(e)}")
+ raise
+
+ # Add health check endpoint
+ async def health_check(request):
+ from starlette.responses import JSONResponse
+ return JSONResponse({"status": "ok", "message": "OpenAlgo MCP Server is running"})
# Set up Starlette app with both SSE connection and message posting endpoints
app = Starlette(
- debug=True,
+ debug=DEBUG,
routes=[
Route("/sse", endpoint=handle_sse),
+ Route("/health", endpoint=health_check),
Mount("/messages/", app=sse.handle_post_message),
],
on_startup=[
- lambda: logging.info("OpenAlgo MCP Server started")
+ lambda: logger.info("OpenAlgo MCP Server started")
],
on_shutdown=[
- lambda: logging.info("OpenAlgo MCP Server shutting down")
+ lambda: logger.info("OpenAlgo MCP Server shutting down")
]
)
# Run the server
if __name__ == "__main__":
- logging.info("Starting OpenAlgo MCP Server...")
+ logger.info("Starting OpenAlgo MCP Server...")
if MODE == 'stdio':
# Run in stdio mode for terminal/command line usage
mcp.run(transport="stdio")
else:
# Run in SSE mode with Uvicorn for web interface
+ logger.info(f"Starting SSE server on port {PORT}")
uvicorn.run(
"server:app",
host="0.0.0.0",
port=PORT,
log_level="info",
- reload=True,
- access_log=True
- )
+ reload=DEBUG
+ )
\ No newline at end of file