Skip to content

Commit

Permalink
added ibkr support and trading view scanners
Browse files Browse the repository at this point in the history
  • Loading branch information
itay-verkh-lightricks committed Dec 3, 2024
1 parent 6b094d8 commit eecf2f4
Show file tree
Hide file tree
Showing 22 changed files with 844 additions and 102 deletions.
213 changes: 166 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,200 @@
# portfolio_service MCP server
# Portfolio Service MCP Server

A comprehensive Model Context Protocol (MCP) server that integrates popular brokerage APIs for portfolio management, market data access, and trading operations.

## Features

- **Multi-Broker Support**: Seamlessly integrate with multiple brokers
- Interactive Brokers (IBKR)
- TradeStation
- **Comprehensive Market Data**: Access to real-time and historical market data
- **Advanced Trading Capabilities**: Execute and manage trades programmatically
- **News Integration**: Get real-time market news and full articles
- **Options Trading Support**: Access options chains and manage options positions
- **TradingView Integration**: Built-in market scanning capabilities

## TradingView Integration

### Market Scanning Tools

1. **tradingview_scan_for_stocks**
- Execute custom stock screening queries using TradingView's extensive database
- Access to over 3000 technical and fundamental indicators
- Flexible query building with SQL-like syntax
- Example queries:
```python
# Basic stock screening
Query().select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI')

# Advanced filtering
Query().select('close', 'volume', 'EMA5', 'EMA20').where(
Column('close').between(Column('EMA5'), Column('EMA20')),
Column('type').isin(['stock', 'fund'])
)

# Complex market scanning
Query().select('name', 'close', 'volume').where(
Column('market_cap_basic').between(1_000_000, 50_000_000),
Column('relative_volume_10d_calc') > 1.2,
Column('MACD.macd') >= Column('MACD.signal')
).order_by('volume', ascending=False)
```

2. **tradingview_scan_from_scanner**
- Use pre-built market scanners
- Available scanners:
- premarket_gainers
- premarket_losers
- premarket_most_active
- premarket_gappers
- postmarket_gainers
- postmarket_losers
- postmarket_most_active

## Interactive Brokers (IBKR) Integration

> note: you need to have a running IBKR TWS instance to use these endpoints
### Market Data Tools

1. **ibkr_get_bars**
- Get OHLCV (Open, High, Low, Close, Volume) data for stocks and indices
- Supports multiple timeframes from 1 second to monthly bars
- Configurable for regular trading hours or extended hours
- Customizable date ranges and bar sizes
```python
# Example timeframes
- Bar sizes: "1 min", "5 mins", "1 hour", "1 day", "1 week", "1 month"
- Durations: "60 S", "30 D", "13 W", "6 M", "10 Y"
```

### Order Management Tools

1. **ibkr_place_new_order**
- Place market or limit orders for stocks
- Support for both buy and sell orders
- Optional take profit and stop loss parameters
- Bracket order capability

2. **ibkr_modify_order**
- Modify existing orders
- Update order prices and other parameters
- Real-time order status tracking

### News Integration

1. **ibkr_get_news_headlines**
- Fetch recent news headlines for any symbol
- Configurable time range
- Multiple news providers support (Dow Jones, Reuters, etc.)

2. **ibkr_get_news_article**
- Retrieve full article content
- Direct access to news sources
- Historical news archive access

### Options Trading Support

1. **ibkr_get_option_expirations**
- List all available expiration dates for options
- Support for stock, index, and futures options

2. **ibkr_read_option_chain**
- Access complete option chains
- View all strikes and expiration dates
- Real-time options data

### Account Resources

Access key account information through these endpoints:

1. **brokerage://ibkr/account_summary**
- Complete account overview
- Key metrics: Net Liquidation Value, Buying Power, etc.
- Real-time account updates

2. **brokerage://ibkr/portfolio**
- Current positions and holdings
- Real-time position updates
- Detailed position information

3. **brokerage://ibkr/orders**
- All trades from current session
- Comprehensive order history
- Order status tracking

4. **brokerage://ibkr/open_orders**
- Active order monitoring
- Real-time order status
- Open order management

A Model Context Protocol server implementing popular broker api's

## Supported APIs

- Currently only TradeStation API is supported *(partially)*

## Components

### Tools

The server implements five tools that are based on TradeStation API:
1. tradestation_get_bars - wraps the output of [TradeStation GetBars](https://api.tradestation.com/docs/specification#tag/MarketData/operation/GetBars) in a dataframe and returns it's `__str__`
2. tradestation_place_buy_order - Same as [TradeStation PlaceOrder](https://api.tradestation.com/docs/specification#tag/Order-Execution/operation/PlaceOrder) but only for buy orders
3. tradestation_place_sell_order - Same as [TradeStation PlaceOrder](https://api.tradestation.com/docs/specification#tag/Order-Execution/operation/PlaceOrder) but only for sell orders
4. tradestation_get_positions - Same as [TradeStation GetPositions](https://api.tradestation.com/docs/specification/#tag/Brokerage/operation/GetPositions)
5. tradestation_get_balances - Same as [TradeStation GetBalances](https://api.tradestation.com/docs/specification/#tag/Brokerage/operation/GetBalances)

see the exact schemas in the definition file [here](./portfolio_service/brokers/tradestation/tools.py)
## Configuration

The server is configured via environment variables:
Configure the server using environment variables:

```bash
```bash
# TradeStation Configuration
TRADESTATION_API_KEY="your_api_key"
TRADESTATION_API_SECRET="your_api_secret"
TS_REFRESH_TOKEN="your_refresh_token"
TS_ACCOUNT_ID="your_account_id"

# Interactive Brokers Configuration
IBKR_ACCOUNT_ID="your_account_id"
```
If you are unsure about how to get the values of the environment variables,
I have a blog post that explains it [here](https://medium.com/@itay1542/how-to-get-free-historical-intraday-equity-prices-with-code-examples-8f36fc57e1aa)

## Quickstart
For detailed instructions on obtaining API credentials, visit our [setup guide](https://medium.com/@itay1542/how-to-get-free-historical-intraday-equity-prices-with-code-examples-8f36fc57e1aa).

### Install
## Installation

#### Claude Desktop
### Claude Desktop Integration

On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
#### MacOS
Configure in: `~/Library/Application\ Support/Claude/claude_desktop_config.json`

On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
#### Windows
Configure in: `%APPDATA%/Claude/claude_desktop_config.json`

```
"mcpServers": {
"portfolio_service": {
"command": "uv",
"args": [
"--directory",
"<path_to_project>/portfolio_service",
"run",
"portfolio_service"
]
```json
{
"mcpServers": {
"portfolio_service": {
"command": "uv",
"args": [
"--directory",
"<path_to_project>/portfolio_service",
"run",
"portfolio_service"
]
}
}
}
```

## Development

### Building and Publishing

### Dependencies Management

1. Sync dependencies and update lockfile:
```bash
# Sync dependencies and update lockfile
uv sync
```

### Debugging

Since MCP servers run over stdio, debugging can be challenging. For the best debugging
experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).


You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
For optimal debugging experience, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

```bash
# Launch MCP Inspector
npx @modelcontextprotocol/inspector uv --directory <path_to_project>/portfolio_service run portfolio-service
```

The Inspector provides a web interface for real-time debugging and monitoring of the MCP server.

## Contributing

Contributions are welcome! Please feel free to submit pull requests or create issues for bugs and feature requests.

## License

Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
This project is open-source and available under the MIT license.
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"aiohttp>=3.11.8",
"ib-insync>=0.9.86",
"mcp>=1.0.0",
"nest-asyncio>=1.6.0",
"pandas>=2.2.3",
"pydantic>=2.10.2",
"python-dotenv>=1.0.1",
"requests>=2.32.3",
"retry>=0.9.2",
"tradingview-screener>=2.5.0",
]
[[project.authors]]
name = "Itay Verkh"
Expand All @@ -22,4 +25,4 @@ requires = [ "hatchling",]
build-backend = "hatchling.build"

[project.scripts]
portfolio-service = "portfolio_service.server:main"
portfolio-service = "portfolio_service:main"
4 changes: 2 additions & 2 deletions src/portfolio_service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import anyio
import asyncio

from . import server


def main():
"""Main entry point for the package."""
anyio.run(server.main)
asyncio.run(server.main())

# Optionally expose other important items at package level
__all__ = ['main', 'server']
3 changes: 0 additions & 3 deletions src/portfolio_service/__main__.py

This file was deleted.

30 changes: 29 additions & 1 deletion src/portfolio_service/brokers/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import dataclasses
import json
from datetime import datetime
from typing import Callable, Any

from ib_insync import util
from mcp import Resource, Tool
from pydantic import BaseModel, AnyUrl
from pytz import timezone
Expand All @@ -19,4 +22,29 @@ class BrokerTools(BaseModel):

def is_market_open():
now = datetime.now(tz=timezone("US/Eastern"))
return now.weekday() < 5 and 9 <= now.hour < 16
return now.weekday() < 5 and 9 <= now.hour < 16

def unpack(obj):
if isinstance(obj, dict):
return {key: unpack(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [unpack(value) for value in obj]
elif util.isnamedtupleinstance(obj):
return {key: unpack(value) for key, value in obj._asdict().items()}
elif isinstance(obj, tuple):
return tuple(unpack(value) for value in obj)
elif util.is_dataclass(obj):
return dataclasses.asdict(obj)
else:
return obj

def list_items(items):
list_ = []
for item in items:
if util.isnamedtupleinstance(item):
list_.append(unpack(item))
elif util.is_dataclass(item):
list_.append(dataclasses.asdict(item))
else:
list_.append(item)
return json.dumps(list_, indent=2, default=str)
3 changes: 3 additions & 0 deletions src/portfolio_service/brokers/ibkr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .resources import resources
from .tools import tools
from .client import ib
8 changes: 8 additions & 0 deletions src/portfolio_service/brokers/ibkr/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import os

import pytz
from ib_insync import IB

ib = IB()
ib.TimezoneTWS = pytz.timezone("US/Eastern")
ib.connect('127.0.0.1', 7496, clientId=0, account=os.getenv("IBKR_ACCOUNT"), timeout=30)
1 change: 1 addition & 0 deletions src/portfolio_service/brokers/ibkr/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ibkr_tool_prefix = "ibkr"
27 changes: 27 additions & 0 deletions src/portfolio_service/brokers/ibkr/global_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import asyncio

from ib_insync import Contract

from .client import ib

orders = {}

class ContractCache(dict):
def __init__(self, ib_client):
super().__init__()
self.contracts = {}
self.ib_client = ib_client

async def get(self, contract: Contract) -> Contract:
if contract.conId in self.contracts:
return self.contracts[contract.conId]
else:
self.contracts[contract.conId] = (await self.ib_client.qualifyContractsAsync(contract))[0]
return self.contracts[contract.conId]

cache = ContractCache(ib)

async def qualify_contracts(*contracts: Contract) -> list[Contract]:
return await asyncio.gather(*[cache.get(c) for c in contracts])

news_providers: list[str] = []
Loading

0 comments on commit eecf2f4

Please sign in to comment.