Back to skills
SkillHub ClubShip Full StackFull StackBackendTesting

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.

Stars
6
Hot score
82
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install scientiacapital-skills-ibkr-api-skill

Repository

scientiacapital/skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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** |

```

ibkr-api-skill | SkillHub