ibkr-api-skill
Interactive Brokers (IBKR) API integration for portfolio management, account queries, and trade execution across multiple account types (Roth IRA, personal brokerage, business). Use when the user mentions IBKR, Interactive Brokers, IB Gateway, TWS API, Client Portal API, brokerage API, portfolio positions, account balances, placing trades via API, multi-account trading, IRA trading restrictions, or wants to build/debug code that connects to Interactive Brokers. Also triggers on "ib_async", "ib_insync", "ibapi", or any IBKR endpoint reference.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install scientiacapital-skills-ibkr-api-skill
Repository
Skill path: active/ibkr-api-skill
Interactive Brokers (IBKR) API integration for portfolio management, account queries, and trade execution across multiple account types (Roth IRA, personal brokerage, business). Use when the user mentions IBKR, Interactive Brokers, IB Gateway, TWS API, Client Portal API, brokerage API, portfolio positions, account balances, placing trades via API, multi-account trading, IRA trading restrictions, or wants to build/debug code that connects to Interactive Brokers. Also triggers on "ib_async", "ib_insync", "ibapi", or any IBKR endpoint reference.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend, Testing, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: scientiacapital.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install ibkr-api-skill into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/scientiacapital/skills before adding ibkr-api-skill to shared team environments
- Use ibkr-api-skill for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: ibkr-api-skill
description: Interactive Brokers (IBKR) API integration for portfolio management, account queries, and trade execution across multiple account types (Roth IRA, personal brokerage, business). Use when the user mentions IBKR, Interactive Brokers, IB Gateway, TWS API, Client Portal API, brokerage API, portfolio positions, account balances, placing trades via API, multi-account trading, IRA trading restrictions, or wants to build/debug code that connects to Interactive Brokers. Also triggers on "ib_async", "ib_insync", "ibapi", or any IBKR endpoint reference.
---
<objective>
Build and manage Interactive Brokers (IBKR) integrations for portfolio queries, trade execution, and multi-account management across Roth IRA, personal brokerage, and business accounts using TWS API (ib_async) or Client Portal REST API.
</objective>
<quick_start>
1. Install: `pip install ib_async`
2. Start IB Gateway on port 7497 (paper) or 7496 (live)
3. Connect: `ib = IB(); await ib.connectAsync('127.0.0.1', 7497, clientId=1)`
4. Query: `positions = ib.positions()` / `summary = await ib.accountSummaryAsync()`
</quick_start>
<success_criteria>
- API connection established and authenticated
- Portfolio positions and balances retrieved across all linked accounts
- IRA restrictions enforced on write operations
- Credentials stored securely (never hardcoded)
</success_criteria>
## Quick Decision: Which API?
| Criterion | TWS API (Recommended) | Client Portal REST API |
|-----------|----------------------|----------------------|
| **Best for** | Automated trading, portfolio mgmt | Web dashboards, light usage |
| **Auth** | Local login via TWS/IB Gateway | OAuth 2.0 JWT |
| **Performance** | Async, low latency, high throughput | REST, slower |
| **Data quality** | Tick-by-tick available | Level 1 only |
| **Multi-account** | All accounts simultaneously | Per-request |
| **Infrastructure** | Local Java app (port 7496/7497) | HTTPS REST calls |
| **Python library** | ib_async (recommended) | requests + OAuth |
| **Cost** | Free | Free |
**Default recommendation**: TWS API via **ib_async** library for all programmatic work.
## Account Architecture
Tim's setup: Roth IRA + Personal Brokerage + THK Enterprises (future business account)
- All linked under single IBKR username/password
- Single API session accesses all linked accounts
- Use `reqLinkedAccounts()` to enumerate account IDs
- Specify account ID per order placement
- Market data subscriptions charged once across all linked accounts
- **One active session per username** — connecting elsewhere closes current session
### IRA-Specific Restrictions (Critical)
| Restriction | Impact |
|-------------|--------|
| No short selling | `placeOrder()` will reject short orders |
| No margin borrowing | Cash-only (no debit balances) |
| No foreign currency borrowing | Must execute FX trade first |
| Futures margin 2x higher | Position sizing affected |
| MLPs/UBTI prohibited | Filter these from IRA order flow |
| Withdrawals USD only | Informational |
## Core API Operations
### Read Operations (Safe — use for all account types)
```python
# Key TWS API functions for portfolio queries
reqLinkedAccounts() # List all account IDs
reqAccountSummary() # Balances, buying power, equity (all accounts)
reqPositions() # Current positions (up to 50 sub-accounts)
reqPositionsMulti() # Per-account positions (>50 sub-accounts)
reqAccountUpdates() # Stream account + position data (single account)
reqMktData() # Real-time Level 1 market data
reqHistoricalData() # Historical price data
```
### Write Operations (Use with caution — respect IRA restrictions)
```python
placeOrder(account_id, contract, order) # Place order on specific account
cancelOrder(order_id) # Cancel pending order
reqGlobalCancel() # Cancel all open orders
```
### Client Portal REST Endpoints (Alternative)
```
GET /iserver/accounts # List accounts
GET /iserver/account/{id}/positions # Positions
GET /iserver/account/{id}/summary # Balances
POST /iserver/account/{id}/orders # Place order
GET /market/candle # Historical candles
```
## Python Library: ib_async
**Install**: `pip install ib_async`
**Why ib_async over alternatives:**
- Modern successor to ib_insync (original creator's project continued)
- Native asyncio support
- Implements IBKR binary protocol internally (no need for official ibapi)
- Active maintenance (GitHub: ib-api-reloaded/ib_async)
**Alternatives** (use only if ib_async doesn't meet needs):
- `ib_insync` — Legacy, stable but unmaintained since early 2024
- `ibapi` — Official IBKR library, cumbersome event loop
### Reference: Connection Pattern
See `reference/connection-patterns.md` for:
- IB Gateway setup and configuration
- Connection/reconnection handling
- Session timeout management (6-min ping for CP API)
- Multi-account query patterns
- Error handling and rate limit management
### Reference: Trading Patterns
See `reference/trading-patterns.md` for:
- Order types (market, limit, stop, bracket, IB algos)
- IRA-safe order validation
- Multi-account order routing
- Position sizing with account-type awareness
- Greeks-aware options order flow
## Infrastructure Requirements
1. **IB Gateway** (lightweight) or **TWS** (full UI) running locally
2. **Java 8+** installed
3. **API enabled** in TWS/Gateway settings
4. **Ports**: 7496 (live) / 7497 (paper trading)
5. **Credentials**: Stored in OS credential manager (never hardcode)
## Security Best Practices
- Run IB Gateway on localhost only (no internet exposure)
- Use read-only login for portfolio queries when trading not needed
- Store credentials in macOS Keychain / Linux secret-service
- Implement session timeout handling
- Validate market data subscriptions before placing orders
- Log all order attempts with account ID + timestamp
## Cost Structure
| Item | Cost |
|------|------|
| API access | Free |
| Market data | $5-50/month per exchange subscription |
| Trading commissions | Standard IBKR rates (varies by asset) |
| Account minimums | $500 per account |
| Estimated total | ~$1,500 aggregate minimum; $15-50/month data |
## Integration with Trading-Signals Skill
This skill complements the `trading-signals-skill`:
- **trading-signals** → generates signals, confluence scores, regime detection
- **ibkr-api** → executes trades, queries positions, manages accounts
- Pipeline: Signal generation → Position sizing → IRA validation → Order execution
## IBKR MCP Server (Installed)
**ArjunDivecha/ibkr-mcp-server** is installed and configured:
- **Location:** `~/Desktop/tk_projects/ibkr-mcp-server/`
- **Claude Code:** Added to `~/.claude.json` (user scope)
- **Claude Desktop:** Added to `claude_desktop_config.json`
- **Mode:** Paper trading (port 7497), live trading disabled
- **Safety:** Order cap 1,000 shares, confirmation required
### Available MCP Tools
| Tool | Purpose | Account Types |
|------|---------|---------------|
| `get_portfolio` | Positions + P&L | All accounts |
| `get_account_summary` | Balances, margin, buying power | All accounts |
| `switch_account` | Toggle Roth IRA / Personal / THK | Multi-account |
| `get_market_data` | Real-time quotes | N/A |
| `get_historical_data` | Historical OHLCV | N/A |
| `place_order` | Orders with safety checks | All (IRA restrictions enforced) |
| `check_shortable_shares` | Short availability | Personal/Business only |
| `get_margin_requirements` | Margin needs per security | Personal/Business only |
| `get_borrow_rates` | Borrow costs for shorts | Personal/Business only |
| `short_selling_analysis` | Full short analysis package | Personal/Business only |
| `get_connection_status` | IB Gateway health check | N/A |
### To Activate
1. Start IB Gateway → port 7497 (paper) or 7496 (live)
2. Enable API: Config → API → Settings → "ActiveX and Socket Clients"
3. Add 127.0.0.1 to Trusted IPs
4. Restart Claude Code / Claude Desktop
### Other Community MCP Servers
- `code-rabi/interactive-brokers-mcp` — Client Portal REST API
- `xiao81/IBKR-MCP-Server` — TWS API focused
- `Hellek1/ib-mcp` — Read-only via ib_async (safest)
## Multi-Broker Aggregation
For unified view across IBKR + Robinhood:
- **SnapTrade MCP** (`dangelov/mcp-snaptrade`) — Read-only aggregator, 15+ brokerages, OAuth-based (safe)
- **Alpaca MCP** (official) — Alternative broker with production-ready MCP
- Manual CSV import from Robinhood as fallback (ToS-safe)
See `reference/multi-broker-strategy.md` for aggregation patterns.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### reference/connection-patterns.md
```markdown
# IBKR Connection Patterns
## Table of Contents
1. [IB Gateway Setup](#ib-gateway-setup)
2. [ib_async Connection](#ib_async-connection)
3. [Multi-Account Queries](#multi-account-queries)
4. [Session Management](#session-management)
5. [Error Handling](#error-handling)
6. [Client Portal API Auth](#client-portal-api-auth)
---
## IB Gateway Setup
### Prerequisites
```bash
# Install IB Gateway (lightweight, headless — preferred over TWS for automation)
# Download from: https://www.interactivebrokers.com/en/trading/ibgateway-stable.php
# Verify Java 8+
java -version
```
### Configuration
1. Launch IB Gateway → Login with IBKR credentials
2. Configure → Settings → API:
- Enable ActiveX and Socket Clients
- Socket port: `7496` (live) or `7497` (paper)
- Allow connections from localhost only
- Read-Only API: Enable for portfolio-only queries
3. Master Client ID: Set to `0` for primary connection
---
## ib_async Connection
### Basic Connection
```python
import asyncio
from ib_async import IB, Stock, util
async def connect_ib() -> IB:
"""Connect to IB Gateway with retry logic."""
ib = IB()
# Connection params
host = '127.0.0.1'
port = 7496 # 7497 for paper trading
client_id = 1 # Unique per concurrent connection
try:
await ib.connectAsync(host, port, clientId=client_id)
print(f"Connected. Server version: {ib.client.serverVersion()}")
return ib
except Exception as e:
print(f"Connection failed: {e}")
raise
async def main():
ib = await connect_ib()
try:
# Your logic here
accounts = ib.managedAccounts()
print(f"Linked accounts: {accounts}")
finally:
ib.disconnect()
if __name__ == '__main__':
asyncio.run(main())
```
### Connection with Auto-Reconnect
```python
from ib_async import IB
import asyncio
class IBConnection:
"""Managed IBKR connection with auto-reconnect."""
def __init__(
self,
host: str = '127.0.0.1',
port: int = 7496,
client_id: int = 1,
max_retries: int = 5,
retry_delay: float = 3.0
):
self.host = host
self.port = port
self.client_id = client_id
self.max_retries = max_retries
self.retry_delay = retry_delay
self.ib = IB()
# Register disconnect handler
self.ib.disconnectedEvent += self._on_disconnect
async def connect(self) -> IB:
"""Connect with retry logic."""
for attempt in range(self.max_retries):
try:
await self.ib.connectAsync(
self.host, self.port,
clientId=self.client_id,
timeout=10
)
return self.ib
except Exception as e:
if attempt < self.max_retries - 1:
await asyncio.sleep(self.retry_delay * (attempt + 1))
else:
raise ConnectionError(
f"Failed after {self.max_retries} attempts: {e}"
)
def _on_disconnect(self):
"""Handle unexpected disconnection."""
print("WARNING: Disconnected from IB Gateway")
# Could trigger auto-reconnect here
async def disconnect(self):
"""Clean disconnect."""
if self.ib.isConnected():
self.ib.disconnect()
```
---
## Multi-Account Queries
### List All Linked Accounts
```python
async def get_all_accounts(ib: IB) -> dict[str, str]:
"""
Get all linked accounts with their types.
Returns: {'U1234567': 'Individual', 'U7654321': 'IRA', ...}
"""
accounts = ib.managedAccounts()
account_info = {}
for acct in accounts:
# Request account type via account summary
tags = await ib.accountSummaryAsync(acct)
for tag in tags:
if tag.tag == 'AccountType':
account_info[acct] = tag.value
break
return account_info
```
### Query Positions Across All Accounts
```python
async def get_all_positions(ib: IB) -> dict[str, list]:
"""Get positions grouped by account."""
positions = ib.positions()
by_account: dict[str, list] = {}
for pos in positions:
acct = pos.account
if acct not in by_account:
by_account[acct] = []
by_account[acct].append({
'symbol': pos.contract.symbol,
'quantity': pos.position,
'avg_cost': pos.avgCost,
'contract': pos.contract,
})
return by_account
```
### Account Summary (All Accounts)
```python
async def get_account_summaries(ib: IB) -> dict[str, dict]:
"""Get key financial metrics for all linked accounts."""
summary_tags = [
'NetLiquidation',
'TotalCashValue',
'BuyingPower',
'GrossPositionValue',
'MaintMarginReq',
'AvailableFunds',
'ExcessLiquidity',
]
summaries = {}
for item in ib.accountSummary():
acct = item.account
if acct not in summaries:
summaries[acct] = {}
if item.tag in summary_tags:
summaries[acct][item.tag] = float(item.value)
return summaries
```
---
## Session Management
### Keep-Alive (Client Portal API)
```python
import httpx
import asyncio
async def keep_session_alive(base_url: str = "https://localhost:5000/v1/api"):
"""Ping Client Portal API every 5 min to prevent 6-min timeout."""
while True:
try:
async with httpx.AsyncClient(verify=False) as client:
resp = await client.post(f"{base_url}/tickle")
if resp.status_code != 200:
print(f"Session ping failed: {resp.status_code}")
except Exception as e:
print(f"Keep-alive error: {e}")
await asyncio.sleep(300) # 5 minutes
```
### Session Exclusivity Warning
```
CRITICAL: Only ONE active brokerage session per IBKR username.
- Logging in via TWS/Gateway on another device = disconnects current session
- Logging in via web/mobile = disconnects API session
- Solution: Dedicate one machine to API access
```
---
## Error Handling
### Common Error Codes
```python
IBKR_ERRORS = {
200: "No security definition found",
201: "Order rejected — check IRA restrictions",
202: "Order cancelled",
300: "Socket connection dropped — reconnect",
502: "Couldn't connect to TWS — is IB Gateway running?",
504: "Not connected — call connect() first",
1100: "Connectivity between IB and TWS lost",
1101: "Connectivity restored (data lost)",
1102: "Connectivity restored (data maintained)",
2104: "Market data farm connection OK",
2106: "HMDS data farm connection OK",
2158: "Sec-def data farm connection OK",
}
def handle_error(req_id: int, error_code: int, error_string: str):
"""Route IBKR errors to appropriate handler."""
if error_code in (1100, 300, 502):
# Connection issues — trigger reconnect
raise ConnectionError(f"IBKR connection error {error_code}: {error_string}")
elif error_code == 201:
# Order rejected — likely IRA restriction
raise ValueError(f"Order rejected (check IRA rules): {error_string}")
elif error_code >= 2100:
# Informational messages
print(f"IBKR info [{error_code}]: {error_string}")
else:
print(f"IBKR error [{error_code}]: {error_string}")
```
---
## Client Portal API Auth
### OAuth 2.0 Flow (JWT Client Assertion)
```python
import jwt
import time
import httpx
async def get_cp_access_token(
client_id: str,
private_key_path: str,
token_url: str = "https://api.ibkr.com/v1/api/oauth/token"
) -> str:
"""
Get Client Portal API access token using OAuth 2.0
with private key JWT assertion (RFC 7521/7523).
"""
# Load private key
with open(private_key_path, 'r') as f:
private_key = f.read()
# Create JWT assertion
now = int(time.time())
claims = {
'iss': client_id,
'sub': client_id,
'aud': token_url,
'exp': now + 300, # 5 min expiry
'iat': now,
}
assertion = jwt.encode(claims, private_key, algorithm='RS256')
# Exchange for access token
async with httpx.AsyncClient() as client:
resp = await client.post(token_url, data={
'grant_type': 'client_credentials',
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': assertion,
})
resp.raise_for_status()
return resp.json()['access_token']
```
### Session Duration
- Access tokens: Up to 24 hours (reset at midnight NY/Zug/HK)
- Inactivity timeout: 6 minutes without requests
- Must call `/tickle` endpoint every 5 minutes to maintain session
```
### reference/trading-patterns.md
```markdown
# IBKR Trading Patterns
## Table of Contents
1. [Order Types](#order-types)
2. [IRA-Safe Order Validation](#ira-safe-order-validation)
3. [Multi-Account Order Routing](#multi-account-order-routing)
4. [Options Order Flow](#options-order-flow)
5. [Position Sizing](#position-sizing)
---
## Order Types
### Basic Orders via ib_async
```python
from ib_async import IB, Stock, Option, Order, LimitOrder, MarketOrder, StopOrder
# Stock contract
aapl = Stock('AAPL', 'SMART', 'USD')
# Market order
market_order = MarketOrder('BUY', 100)
# Limit order
limit_order = LimitOrder('BUY', 100, limitPrice=175.50)
# Stop order
stop_order = StopOrder('SELL', 100, stopPrice=170.00)
# Bracket order (entry + take-profit + stop-loss)
bracket = ib.bracketOrder(
action='BUY',
quantity=100,
limitPrice=175.50,
takeProfitPrice=185.00,
stopLossPrice=170.00
)
```
### Advanced Order Types
```python
# Trailing stop (percentage)
from ib_async import Order
trailing_stop = Order(
action='SELL',
orderType='TRAIL',
totalQuantity=100,
trailingPercent=5.0, # 5% trailing stop
)
# Adaptive algo (IBKR smart routing)
adaptive_order = Order(
action='BUY',
orderType='LMT',
totalQuantity=100,
lmtPrice=175.50,
algoStrategy='Adaptive',
algoParams=[{'tag': 'adaptivePriority', 'value': 'Normal'}],
)
# TWAP (Time-Weighted Average Price)
twap_order = Order(
action='BUY',
orderType='LMT',
totalQuantity=1000,
lmtPrice=175.50,
algoStrategy='Twap',
algoParams=[
{'tag': 'startTime', 'value': '09:30:00 US/Eastern'},
{'tag': 'endTime', 'value': '16:00:00 US/Eastern'},
],
)
```
---
## IRA-Safe Order Validation
### Pre-Order Validation
```python
from dataclasses import dataclass
from typing import Optional
@dataclass
class AccountRules:
"""Trading rules by account type."""
can_short: bool
can_use_margin: bool
can_borrow_currency: bool
can_trade_mlps: bool
futures_margin_multiplier: float
ACCOUNT_RULES = {
'IRA': AccountRules(
can_short=False,
can_use_margin=False,
can_borrow_currency=False,
can_trade_mlps=False,
futures_margin_multiplier=2.0,
),
'Individual': AccountRules(
can_short=True,
can_use_margin=True,
can_borrow_currency=True,
can_trade_mlps=True,
futures_margin_multiplier=1.0,
),
'Business': AccountRules(
can_short=True,
can_use_margin=True,
can_borrow_currency=True,
can_trade_mlps=True,
futures_margin_multiplier=1.0,
),
}
def validate_order_for_account(
action: str,
contract,
account_type: str,
quantity: float,
) -> tuple[bool, Optional[str]]:
"""
Validate order against account-type restrictions.
Returns (is_valid, rejection_reason).
"""
rules = ACCOUNT_RULES.get(account_type)
if not rules:
return False, f"Unknown account type: {account_type}"
# Check short selling
if action == 'SELL' and quantity < 0 and not rules.can_short:
return False, f"Short selling not allowed in {account_type} accounts"
# Check MLP restriction
if hasattr(contract, 'secType') and contract.secType == 'STK':
# Would need MLP lookup table
pass
return True, None
```
---
## Multi-Account Order Routing
### Route Order to Specific Account
```python
async def place_order_on_account(
ib: IB,
account_id: str,
contract,
order: Order,
account_type: str = 'Individual',
dry_run: bool = True,
) -> Optional[dict]:
"""
Place order on specific account with validation.
Set dry_run=True for paper verification without execution.
"""
# Validate against account rules
is_valid, reason = validate_order_for_account(
order.action, contract, account_type, order.totalQuantity
)
if not is_valid:
return {'status': 'REJECTED', 'reason': reason}
# Set account on order
order.account = account_id
if dry_run:
# Use whatIf to simulate without executing
what_if = await ib.whatIfOrderAsync(contract, order)
return {
'status': 'SIMULATED',
'init_margin_change': what_if.initMarginChange,
'maint_margin_change': what_if.maintMarginChange,
'equity_with_loan': what_if.equityWithLoanValue,
'commission': what_if.commission,
}
# Place live order
trade = ib.placeOrder(contract, order)
return {
'status': 'SUBMITTED',
'order_id': trade.order.orderId,
'trade': trade,
}
```
### Cross-Account Rebalance
```python
async def rebalance_across_accounts(
ib: IB,
target_allocations: dict[str, dict[str, float]],
accounts: dict[str, str], # {account_id: account_type}
) -> list[dict]:
"""
Generate rebalance orders across multiple accounts.
target_allocations: {account_id: {symbol: target_pct}}
"""
orders = []
for acct_id, targets in target_allocations.items():
acct_type = accounts[acct_id]
# Get current positions
positions = [p for p in ib.positions() if p.account == acct_id]
# Get account NAV
summary = [s for s in ib.accountSummary()
if s.account == acct_id and s.tag == 'NetLiquidation']
nav = float(summary[0].value) if summary else 0
for symbol, target_pct in targets.items():
target_value = nav * target_pct
# Find current position
current_pos = next(
(p for p in positions if p.contract.symbol == symbol), None
)
current_qty = current_pos.position if current_pos else 0
# Get current price (simplified)
contract = Stock(symbol, 'SMART', 'USD')
[ticker] = await ib.reqTickersAsync(contract)
price = ticker.marketPrice()
if price and price > 0:
target_qty = int(target_value / price)
delta = target_qty - current_qty
if abs(delta) > 0:
action = 'BUY' if delta > 0 else 'SELL'
order_info = {
'account': acct_id,
'account_type': acct_type,
'symbol': symbol,
'action': action,
'quantity': abs(delta),
'current_qty': current_qty,
'target_qty': target_qty,
}
# Validate before adding
is_valid, reason = validate_order_for_account(
action, contract, acct_type, delta
)
order_info['valid'] = is_valid
order_info['rejection_reason'] = reason
orders.append(order_info)
return orders
```
---
## Options Order Flow
### Options Contract Construction
```python
from ib_async import Option
# Single option
aapl_call = Option('AAPL', '20260320', 180, 'C', 'SMART')
# Request option chain
chains = await ib.reqSecDefOptParamsAsync(
underlyingSymbol='AAPL',
futFopExchange='',
underlyingSecType='STK',
underlyingConId=265598, # AAPL conId
)
# Filter for specific expiry and strikes
for chain in chains:
if chain.exchange == 'SMART':
expirations = chain.expirations # List of date strings
strikes = chain.strikes # List of strike prices
```
### Spread Orders
```python
from ib_async import Contract, ComboLeg, Order
# Vertical spread (bull call)
combo = Contract()
combo.symbol = 'AAPL'
combo.secType = 'BAG'
combo.currency = 'USD'
combo.exchange = 'SMART'
# Buy lower strike call, sell higher strike call
combo.comboLegs = [
ComboLeg(conId=long_call_conid, ratio=1, action='BUY', exchange='SMART'),
ComboLeg(conId=short_call_conid, ratio=1, action='SELL', exchange='SMART'),
]
# Net debit order for the spread
spread_order = LimitOrder('BUY', 1, limitPrice=2.50)
```
---
## Position Sizing
### Risk-Based Position Sizing
```python
def calculate_position_size(
account_nav: float,
entry_price: float,
stop_price: float,
risk_pct: float = 0.02, # 2% max risk per trade
account_type: str = 'Individual',
) -> dict:
"""
Calculate position size based on risk management rules.
Integrates with trading-signals-skill risk parameters.
"""
# Max risk amount
risk_amount = account_nav * risk_pct
# Per-share risk
per_share_risk = abs(entry_price - stop_price)
if per_share_risk <= 0:
return {'error': 'Stop price must differ from entry'}
# Raw position size
raw_qty = int(risk_amount / per_share_risk)
# IRA adjustment: no margin, so check cash available
max_by_cash = int(account_nav / entry_price) if account_type == 'IRA' else raw_qty * 2
final_qty = min(raw_qty, max_by_cash)
return {
'quantity': final_qty,
'risk_amount': final_qty * per_share_risk,
'risk_pct_actual': (final_qty * per_share_risk) / account_nav,
'position_value': final_qty * entry_price,
'position_pct_of_nav': (final_qty * entry_price) / account_nav,
'account_type': account_type,
'margin_adjusted': account_type == 'IRA',
}
```
```
### reference/multi-broker-strategy.md
```markdown
# Multi-Broker Strategy: IBKR + Robinhood
## Table of Contents
1. [Architecture Decision](#architecture-decision)
2. [Robinhood Integration Options](#robinhood-integration-options)
3. [Unified Portfolio View](#unified-portfolio-view)
4. [MCP Server Options](#mcp-server-options)
5. [Recommended Setup](#recommended-setup)
---
## Architecture Decision
### The Reality
- **IBKR**: Official API, production-ready, full trading support
- **Robinhood**: No official stocks/options API. Unofficial libraries violate ToS.
### Decision Matrix
| Approach | IBKR | Robinhood | Risk |
|----------|------|-----------|------|
| Full API both | Official ib_async | robin_stocks (unofficial) | HIGH — RH account suspension |
| API + Read-only | Official ib_async | robin_stocks read-only | MEDIUM — still violates RH ToS |
| API + Aggregator | Official ib_async | SnapTrade OAuth | LOW — officially blessed |
| API + Manual | Official ib_async | CSV export | ZERO — fully compliant |
**Recommendation**: IBKR official API + SnapTrade for Robinhood read-only data.
---
## Robinhood Integration Options
### Option 1: SnapTrade (Recommended — Low Risk)
```
Type: OAuth-based aggregator (read-only)
Security: SOC 2 Type 2 compliant, bank-level encryption
Your credentials: NOT shared with SnapTrade
Capabilities: Positions, balances, order history
Limitations: No trading, no real-time data
MCP Server: dangelov/mcp-snaptrade (GitHub)
```
### Option 2: robin_stocks Library (High Risk)
```
Library: robin_stocks (pip install robin-stocks)
GitHub: jmfernandes/robin_stocks (~2,000 stars)
Capabilities: Full trading, positions, balances, options
Auth: Username + password + 2FA (TOTP)
Risk: ToS violation, account suspension possible
Breakage: 1-3x per year when RH changes endpoints
```
### Option 3: CSV Export (Zero Risk)
```
Method: Account → History → Export All (web app)
Format: CSV with date range selection
Automation: Manual only
Use case: Monthly portfolio snapshots, tax prep
```
### Option 4: Robinhood Crypto API (Official, Crypto Only)
```
Type: Official API (launched 2024)
Scope: Crypto trading ONLY (no stocks/options)
Docs: docs.robinhood.com
Use case: Only if you need programmatic crypto on RH
```
---
## Unified Portfolio View
### Data Model
```python
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class UnifiedPosition:
"""Cross-broker position representation."""
broker: str # 'IBKR' or 'Robinhood'
account_id: str
account_type: str # 'Roth IRA', 'Individual', 'Business'
symbol: str
quantity: float
avg_cost: float
current_price: Optional[float]
market_value: Optional[float]
unrealized_pnl: Optional[float]
last_updated: datetime
@dataclass
class UnifiedAccountSummary:
"""Cross-broker account summary."""
broker: str
account_id: str
account_type: str
net_liquidation: float
total_cash: float
buying_power: float
positions_value: float
day_pnl: Optional[float]
last_updated: datetime
def merge_portfolios(
ibkr_positions: list[UnifiedPosition],
rh_positions: list[UnifiedPosition],
) -> dict[str, list[UnifiedPosition]]:
"""
Merge positions from both brokers, grouped by symbol.
Enables cross-broker concentration analysis.
"""
merged: dict[str, list[UnifiedPosition]] = {}
for pos in ibkr_positions + rh_positions:
if pos.symbol not in merged:
merged[pos.symbol] = []
merged[pos.symbol].append(pos)
return merged
```
---
## MCP Server Options
### For Claude Desktop / Cowork Integration
| Server | Broker | Type | Status | GitHub |
|--------|--------|------|--------|--------|
| interactive-brokers-mcp | IBKR | Full trading | Experimental | code-rabi/interactive-brokers-mcp |
| ib-mcp | IBKR | Read-only | Experimental | Hellek1/ib-mcp |
| ibkr-mcp-server | IBKR | Multi-account | Experimental | ArjunDivecha/ibkr-mcp-server |
| mcp-snaptrade | Multi-broker | Read-only | Stable | dangelov/mcp-snaptrade |
| robinhood-mcp | Robinhood | Read-only | Experimental | verygoodplugins/robinhood-mcp |
| alpaca-mcp-server | Alpaca | Full trading | Production | alpacahq/alpaca-mcp-server |
### Claude Desktop MCP Config Example
```json
{
"mcpServers": {
"ibkr": {
"command": "python",
"args": ["-m", "ibkr_mcp_server"],
"env": {
"IB_HOST": "127.0.0.1",
"IB_PORT": "7496",
"IB_CLIENT_ID": "10"
}
},
"snaptrade": {
"command": "npx",
"args": ["mcp-snaptrade"],
"env": {
"SNAPTRADE_CLIENT_ID": "${SNAPTRADE_CLIENT_ID}",
"SNAPTRADE_CONSUMER_KEY": "${SNAPTRADE_CONSUMER_KEY}"
}
}
}
}
```
---
## Recommended Setup
### Phase 1: IBKR Only (Immediate)
1. Install IB Gateway on local machine
2. Install ib_async (`pip install ib_async`)
3. Connect to all 3 linked accounts (Roth IRA, Personal, THK Enterprises)
4. Build portfolio dashboard with unified view
5. Integrate with trading-signals-skill for signal → execution pipeline
### Phase 2: Add Robinhood Read-Only (When Ready)
1. Set up SnapTrade account (free tier available)
2. Connect Robinhood via SnapTrade OAuth
3. Pull positions/balances into unified data model
4. OR: Use monthly CSV export as simpler alternative
### Phase 3: MCP Integration (Optional)
1. Evaluate community IBKR MCP servers
2. Consider building custom MCP server wrapping ib_async
3. Add SnapTrade MCP for Robinhood data in Claude Desktop
4. Enables natural language portfolio queries in Cowork
### Cost Comparison
| Component | Monthly Cost |
|-----------|-------------|
| IBKR API | Free |
| IBKR market data | $5-50 |
| SnapTrade (free tier) | Free |
| SnapTrade (paid) | ~$50/mo |
| robin_stocks | Free (but risk) |
| CSV export | Free |
| **Total (recommended)** | **$5-50/mo** |
```