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 @@ +[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/marketcalls-openalgo-mcp-badge.png)](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 = ` +
+
+ +
+
+
+ ${isUser ? 'You' : 'Trading Assistant'} + +
+
+ ${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 = ` +
+
+
+ +
+
+
+ Trading Assistant + +
+
+ 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}
+
+
+
+ ${item.change} +
+ +
+
+ `; + + 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 + + + + + + + + + + + + +
+ + + + +
+
+ +
+ +
+
+
+ + 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